claudecode-omc 5.6.7 → 5.6.8
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/.local/skills/THIRD_PARTY_LICENSES/AvdLee-SwiftUI-Agent-Skill.LICENSE +21 -0
- package/.local/skills/THIRD_PARTY_LICENSES/Dimillian-Skills.LICENSE +21 -0
- package/.local/skills/THIRD_PARTY_LICENSES/README.md +36 -0
- package/.local/skills/THIRD_PARTY_LICENSES/twostraws-swiftui-agent-skill.LICENSE +21 -0
- package/.local/skills/ios-debugger-agent/SKILL.md +51 -0
- package/.local/skills/ios-debugger-agent/agents/openai.yaml +4 -0
- package/.local/skills/swift-concurrency-expert/SKILL.md +105 -0
- package/.local/skills/swift-concurrency-expert/agents/openai.yaml +4 -0
- package/.local/skills/swift-concurrency-expert/references/approachable-concurrency.md +63 -0
- package/.local/skills/swift-concurrency-expert/references/swift-6-2-concurrency.md +272 -0
- package/.local/skills/swift-concurrency-expert/references/swiftui-concurrency-tour-wwdc.md +33 -0
- package/.local/skills/swiftui-expert-skill/SKILL.md +162 -0
- package/.local/skills/swiftui-expert-skill/references/accessibility-patterns.md +215 -0
- package/.local/skills/swiftui-expert-skill/references/animation-advanced.md +403 -0
- package/.local/skills/swiftui-expert-skill/references/animation-basics.md +284 -0
- package/.local/skills/swiftui-expert-skill/references/animation-transitions.md +326 -0
- package/.local/skills/swiftui-expert-skill/references/charts-accessibility.md +135 -0
- package/.local/skills/swiftui-expert-skill/references/charts.md +602 -0
- package/.local/skills/swiftui-expert-skill/references/focus-patterns.md +299 -0
- package/.local/skills/swiftui-expert-skill/references/image-optimization.md +203 -0
- package/.local/skills/swiftui-expert-skill/references/latest-apis.md +488 -0
- package/.local/skills/swiftui-expert-skill/references/layout-best-practices.md +266 -0
- package/.local/skills/swiftui-expert-skill/references/liquid-glass.md +423 -0
- package/.local/skills/swiftui-expert-skill/references/list-patterns.md +446 -0
- package/.local/skills/swiftui-expert-skill/references/macos-scenes.md +318 -0
- package/.local/skills/swiftui-expert-skill/references/macos-views.md +357 -0
- package/.local/skills/swiftui-expert-skill/references/macos-window-styling.md +303 -0
- package/.local/skills/swiftui-expert-skill/references/performance-patterns.md +403 -0
- package/.local/skills/swiftui-expert-skill/references/scroll-patterns.md +293 -0
- package/.local/skills/swiftui-expert-skill/references/sheet-navigation-patterns.md +363 -0
- package/.local/skills/swiftui-expert-skill/references/state-management.md +388 -0
- package/.local/skills/swiftui-expert-skill/references/text-patterns.md +32 -0
- package/.local/skills/swiftui-expert-skill/references/trace-analysis.md +295 -0
- package/.local/skills/swiftui-expert-skill/references/trace-recording.md +134 -0
- package/.local/skills/swiftui-expert-skill/references/view-structure.md +780 -0
- package/.local/skills/swiftui-expert-skill/scripts/__pycache__/analyze_trace.cpython-313.pyc +0 -0
- package/.local/skills/swiftui-expert-skill/scripts/__pycache__/record_trace.cpython-313.pyc +0 -0
- package/.local/skills/swiftui-expert-skill/scripts/analyze_trace.py +301 -0
- package/.local/skills/swiftui-expert-skill/scripts/instruments_parser/__init__.py +1 -0
- package/.local/skills/swiftui-expert-skill/scripts/instruments_parser/__pycache__/__init__.cpython-313.pyc +0 -0
- package/.local/skills/swiftui-expert-skill/scripts/instruments_parser/__pycache__/causes.cpython-313.pyc +0 -0
- package/.local/skills/swiftui-expert-skill/scripts/instruments_parser/__pycache__/correlate.cpython-313.pyc +0 -0
- package/.local/skills/swiftui-expert-skill/scripts/instruments_parser/__pycache__/events.cpython-313.pyc +0 -0
- package/.local/skills/swiftui-expert-skill/scripts/instruments_parser/__pycache__/hangs.cpython-313.pyc +0 -0
- package/.local/skills/swiftui-expert-skill/scripts/instruments_parser/__pycache__/hitches.cpython-313.pyc +0 -0
- package/.local/skills/swiftui-expert-skill/scripts/instruments_parser/__pycache__/summary.cpython-313.pyc +0 -0
- package/.local/skills/swiftui-expert-skill/scripts/instruments_parser/__pycache__/swiftui.cpython-313.pyc +0 -0
- package/.local/skills/swiftui-expert-skill/scripts/instruments_parser/__pycache__/time_profiler.cpython-313.pyc +0 -0
- package/.local/skills/swiftui-expert-skill/scripts/instruments_parser/__pycache__/xctrace.cpython-313.pyc +0 -0
- package/.local/skills/swiftui-expert-skill/scripts/instruments_parser/__pycache__/xml_utils.cpython-313.pyc +0 -0
- package/.local/skills/swiftui-expert-skill/scripts/instruments_parser/causes.py +187 -0
- package/.local/skills/swiftui-expert-skill/scripts/instruments_parser/correlate.py +179 -0
- package/.local/skills/swiftui-expert-skill/scripts/instruments_parser/events.py +291 -0
- package/.local/skills/swiftui-expert-skill/scripts/instruments_parser/hangs.py +108 -0
- package/.local/skills/swiftui-expert-skill/scripts/instruments_parser/hitches.py +145 -0
- package/.local/skills/swiftui-expert-skill/scripts/instruments_parser/summary.py +243 -0
- package/.local/skills/swiftui-expert-skill/scripts/instruments_parser/swiftui.py +195 -0
- package/.local/skills/swiftui-expert-skill/scripts/instruments_parser/time_profiler.py +135 -0
- package/.local/skills/swiftui-expert-skill/scripts/instruments_parser/xctrace.py +117 -0
- package/.local/skills/swiftui-expert-skill/scripts/instruments_parser/xml_utils.py +224 -0
- package/.local/skills/swiftui-expert-skill/scripts/record_trace.py +252 -0
- package/.local/skills/swiftui-liquid-glass/SKILL.md +90 -0
- package/.local/skills/swiftui-liquid-glass/agents/openai.yaml +4 -0
- package/.local/skills/swiftui-liquid-glass/references/liquid-glass.md +280 -0
- package/.local/skills/swiftui-performance-audit/SKILL.md +106 -0
- package/.local/skills/swiftui-performance-audit/agents/openai.yaml +4 -0
- package/.local/skills/swiftui-performance-audit/references/code-smells.md +150 -0
- package/.local/skills/swiftui-performance-audit/references/demystify-swiftui-performance-wwdc23.md +46 -0
- package/.local/skills/swiftui-performance-audit/references/optimizing-swiftui-performance-instruments.md +29 -0
- package/.local/skills/swiftui-performance-audit/references/profiling-intake.md +44 -0
- package/.local/skills/swiftui-performance-audit/references/report-template.md +47 -0
- package/.local/skills/swiftui-performance-audit/references/understanding-hangs-in-your-app.md +33 -0
- package/.local/skills/swiftui-performance-audit/references/understanding-improving-swiftui-performance.md +52 -0
- package/.local/skills/swiftui-pro/SKILL.md +108 -0
- package/.local/skills/swiftui-pro/agents/openai.yaml +10 -0
- package/.local/skills/swiftui-pro/assets/swiftui-pro-icon.png +0 -0
- package/.local/skills/swiftui-pro/assets/swiftui-pro-icon.svg +29 -0
- package/.local/skills/swiftui-pro/references/accessibility.md +13 -0
- package/.local/skills/swiftui-pro/references/api.md +39 -0
- package/.local/skills/swiftui-pro/references/data.md +43 -0
- package/.local/skills/swiftui-pro/references/design.md +32 -0
- package/.local/skills/swiftui-pro/references/hygiene.md +9 -0
- package/.local/skills/swiftui-pro/references/navigation.md +14 -0
- package/.local/skills/swiftui-pro/references/performance.md +46 -0
- package/.local/skills/swiftui-pro/references/swift.md +56 -0
- package/.local/skills/swiftui-pro/references/views.md +36 -0
- package/.local/skills/swiftui-ui-patterns/SKILL.md +95 -0
- package/.local/skills/swiftui-ui-patterns/agents/openai.yaml +4 -0
- package/.local/skills/swiftui-ui-patterns/references/app-wiring.md +201 -0
- package/.local/skills/swiftui-ui-patterns/references/async-state.md +96 -0
- package/.local/skills/swiftui-ui-patterns/references/components-index.md +50 -0
- package/.local/skills/swiftui-ui-patterns/references/controls.md +57 -0
- package/.local/skills/swiftui-ui-patterns/references/deeplinks.md +66 -0
- package/.local/skills/swiftui-ui-patterns/references/focus.md +90 -0
- package/.local/skills/swiftui-ui-patterns/references/form.md +97 -0
- package/.local/skills/swiftui-ui-patterns/references/grids.md +71 -0
- package/.local/skills/swiftui-ui-patterns/references/haptics.md +71 -0
- package/.local/skills/swiftui-ui-patterns/references/input-toolbar.md +51 -0
- package/.local/skills/swiftui-ui-patterns/references/lightweight-clients.md +93 -0
- package/.local/skills/swiftui-ui-patterns/references/list.md +86 -0
- package/.local/skills/swiftui-ui-patterns/references/loading-placeholders.md +38 -0
- package/.local/skills/swiftui-ui-patterns/references/macos-settings.md +71 -0
- package/.local/skills/swiftui-ui-patterns/references/matched-transitions.md +59 -0
- package/.local/skills/swiftui-ui-patterns/references/media.md +73 -0
- package/.local/skills/swiftui-ui-patterns/references/menu-bar.md +101 -0
- package/.local/skills/swiftui-ui-patterns/references/navigationstack.md +159 -0
- package/.local/skills/swiftui-ui-patterns/references/overlay.md +45 -0
- package/.local/skills/swiftui-ui-patterns/references/performance.md +62 -0
- package/.local/skills/swiftui-ui-patterns/references/previews.md +48 -0
- package/.local/skills/swiftui-ui-patterns/references/scroll-reveal.md +133 -0
- package/.local/skills/swiftui-ui-patterns/references/scrollview.md +87 -0
- package/.local/skills/swiftui-ui-patterns/references/searchable.md +71 -0
- package/.local/skills/swiftui-ui-patterns/references/sheets.md +155 -0
- package/.local/skills/swiftui-ui-patterns/references/split-views.md +72 -0
- package/.local/skills/swiftui-ui-patterns/references/tabview.md +114 -0
- package/.local/skills/swiftui-ui-patterns/references/theming.md +71 -0
- package/.local/skills/swiftui-ui-patterns/references/title-menus.md +93 -0
- package/.local/skills/swiftui-ui-patterns/references/top-bar.md +49 -0
- package/.local/skills/swiftui-view-refactor/SKILL.md +202 -0
- package/.local/skills/swiftui-view-refactor/agents/openai.yaml +4 -0
- package/.local/skills/swiftui-view-refactor/references/mv-patterns.md +161 -0
- package/bundled/manifest.json +1 -1
- package/package.json +1 -1
|
@@ -0,0 +1,446 @@
|
|
|
1
|
+
# SwiftUI List Patterns Reference
|
|
2
|
+
|
|
3
|
+
## Table of Contents
|
|
4
|
+
|
|
5
|
+
- [ForEach Identity and Stability](#foreach-identity-and-stability)
|
|
6
|
+
- [Enumerated Sequences](#enumerated-sequences)
|
|
7
|
+
- [List with Custom Styling](#list-with-custom-styling)
|
|
8
|
+
- [List with Pull-to-Refresh](#list-with-pull-to-refresh)
|
|
9
|
+
- [Empty States with ContentUnavailableView (iOS 17+)](#empty-states-with-contentunavailableview-ios-17)
|
|
10
|
+
- [Custom List Backgrounds](#custom-list-backgrounds)
|
|
11
|
+
- [Table](#table)
|
|
12
|
+
- [Summary Checklist](#summary-checklist)
|
|
13
|
+
|
|
14
|
+
## ForEach Identity and Stability
|
|
15
|
+
|
|
16
|
+
**Always provide stable identity for `ForEach`.** Never use `.indices` for dynamic content.
|
|
17
|
+
|
|
18
|
+
```swift
|
|
19
|
+
// Good - stable identity via Identifiable
|
|
20
|
+
extension User: Identifiable {
|
|
21
|
+
var id: String { userId }
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
ForEach(users) { user in
|
|
25
|
+
UserRow(user: user)
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
// Good - stable identity via keypath
|
|
29
|
+
ForEach(users, id: \.userId) { user in
|
|
30
|
+
UserRow(user: user)
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
// Wrong - indices create static content
|
|
34
|
+
ForEach(users.indices, id: \.self) { index in
|
|
35
|
+
UserRow(user: users[index]) // Can crash on removal!
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
// Wrong - unstable identity
|
|
39
|
+
ForEach(users, id: \.self) { user in
|
|
40
|
+
UserRow(user: user) // Only works if User is Hashable and stable
|
|
41
|
+
}
|
|
42
|
+
```
|
|
43
|
+
|
|
44
|
+
**Critical**: Ensure **constant number of views per element** in `ForEach`:
|
|
45
|
+
|
|
46
|
+
```swift
|
|
47
|
+
// Good - consistent view count
|
|
48
|
+
ForEach(items) { item in
|
|
49
|
+
ItemRow(item: item)
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
// Bad - variable view count breaks identity
|
|
53
|
+
ForEach(items) { item in
|
|
54
|
+
if item.isSpecial {
|
|
55
|
+
SpecialRow(item: item)
|
|
56
|
+
DetailRow(item: item)
|
|
57
|
+
} else {
|
|
58
|
+
RegularRow(item: item)
|
|
59
|
+
}
|
|
60
|
+
}
|
|
61
|
+
```
|
|
62
|
+
|
|
63
|
+
**Avoid inline filtering:**
|
|
64
|
+
|
|
65
|
+
```swift
|
|
66
|
+
// Bad - unstable identity, changes on every update
|
|
67
|
+
ForEach(items.filter { $0.isEnabled }) { item in
|
|
68
|
+
ItemRow(item: item)
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
// Good - prefilter and cache
|
|
72
|
+
@State private var enabledItems: [Item] = []
|
|
73
|
+
|
|
74
|
+
var body: some View {
|
|
75
|
+
ForEach(enabledItems) { item in
|
|
76
|
+
ItemRow(item: item)
|
|
77
|
+
}
|
|
78
|
+
.onChange(of: items) { _, newItems in
|
|
79
|
+
enabledItems = newItems.filter { $0.isEnabled }
|
|
80
|
+
}
|
|
81
|
+
}
|
|
82
|
+
```
|
|
83
|
+
|
|
84
|
+
**Avoid `AnyView` in list rows:**
|
|
85
|
+
|
|
86
|
+
```swift
|
|
87
|
+
// Bad - hides identity, increases cost
|
|
88
|
+
ForEach(items) { item in
|
|
89
|
+
AnyView(item.isSpecial ? SpecialRow(item: item) : RegularRow(item: item))
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
// Good - Create a unified row view
|
|
93
|
+
ForEach(items) { item in
|
|
94
|
+
ItemRow(item: item)
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
struct ItemRow: View {
|
|
98
|
+
let item: Item
|
|
99
|
+
|
|
100
|
+
var body: some View {
|
|
101
|
+
if item.isSpecial {
|
|
102
|
+
SpecialRow(item: item)
|
|
103
|
+
} else {
|
|
104
|
+
RegularRow(item: item)
|
|
105
|
+
}
|
|
106
|
+
}
|
|
107
|
+
}
|
|
108
|
+
```
|
|
109
|
+
|
|
110
|
+
**Why**: Stable identity is critical for performance and animations. Unstable identity causes excessive diffing, broken animations, and potential crashes.
|
|
111
|
+
|
|
112
|
+
### Identifiable ID Must Be Truly Unique
|
|
113
|
+
|
|
114
|
+
Non-unique IDs cause SwiftUI to treat different items as identical, leading to duplicate rendering or missing views:
|
|
115
|
+
|
|
116
|
+
```swift
|
|
117
|
+
// Bug -- two articles with the same URL show identical content
|
|
118
|
+
struct Article: Identifiable {
|
|
119
|
+
let title: String
|
|
120
|
+
let url: URL
|
|
121
|
+
var id: String { url.absoluteString } // Not unique if URLs repeat!
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
// Fix -- use a genuinely unique identifier
|
|
125
|
+
struct Article: Identifiable {
|
|
126
|
+
let id: UUID
|
|
127
|
+
let title: String
|
|
128
|
+
let url: URL
|
|
129
|
+
}
|
|
130
|
+
```
|
|
131
|
+
|
|
132
|
+
**Classes get a default `ObjectIdentifier`-based `id`** when conforming to `Identifiable` without providing one. This is only unique for the object's lifetime and can be recycled after deallocation.
|
|
133
|
+
|
|
134
|
+
## Enumerated Sequences
|
|
135
|
+
|
|
136
|
+
**Always convert enumerated sequences to arrays. To be able to use them in a ForEach.**
|
|
137
|
+
|
|
138
|
+
```swift
|
|
139
|
+
let items = ["A", "B", "C"]
|
|
140
|
+
|
|
141
|
+
// Correct
|
|
142
|
+
ForEach(Array(items.enumerated()), id: \.offset) { index, item in
|
|
143
|
+
Text("\(index): \(item)")
|
|
144
|
+
}
|
|
145
|
+
|
|
146
|
+
// Wrong - Doesn't compile, enumerated() isn't an array
|
|
147
|
+
ForEach(items.enumerated(), id: \.offset) { index, item in
|
|
148
|
+
Text("\(index): \(item)")
|
|
149
|
+
}
|
|
150
|
+
```
|
|
151
|
+
|
|
152
|
+
## List with Custom Styling
|
|
153
|
+
|
|
154
|
+
```swift
|
|
155
|
+
// Remove default background and separators
|
|
156
|
+
List(items) { item in
|
|
157
|
+
ItemRow(item: item)
|
|
158
|
+
.listRowInsets(EdgeInsets(top: 8, leading: 16, bottom: 8, trailing: 16))
|
|
159
|
+
.listRowSeparator(.hidden)
|
|
160
|
+
}
|
|
161
|
+
.listStyle(.plain)
|
|
162
|
+
.scrollContentBackground(.hidden)
|
|
163
|
+
.background(Color.customBackground)
|
|
164
|
+
.environment(\.defaultMinListRowHeight, 1) // Allows custom row heights
|
|
165
|
+
```
|
|
166
|
+
|
|
167
|
+
## List with Pull-to-Refresh
|
|
168
|
+
|
|
169
|
+
```swift
|
|
170
|
+
List(items) { item in
|
|
171
|
+
ItemRow(item: item)
|
|
172
|
+
}
|
|
173
|
+
.refreshable {
|
|
174
|
+
await loadItems()
|
|
175
|
+
}
|
|
176
|
+
```
|
|
177
|
+
|
|
178
|
+
## Empty States with ContentUnavailableView (iOS 17+)
|
|
179
|
+
|
|
180
|
+
Use `ContentUnavailableView` for empty list/search states. The built-in `.search` variant is auto-localized:
|
|
181
|
+
|
|
182
|
+
```swift
|
|
183
|
+
List {
|
|
184
|
+
ForEach(searchResults) { item in
|
|
185
|
+
ItemRow(item: item)
|
|
186
|
+
}
|
|
187
|
+
}
|
|
188
|
+
.overlay {
|
|
189
|
+
if searchResults.isEmpty, !searchText.isEmpty {
|
|
190
|
+
ContentUnavailableView.search(text: searchText)
|
|
191
|
+
}
|
|
192
|
+
}
|
|
193
|
+
```
|
|
194
|
+
|
|
195
|
+
For non-search empty states, use a custom instance:
|
|
196
|
+
|
|
197
|
+
```swift
|
|
198
|
+
ContentUnavailableView(
|
|
199
|
+
"No Articles",
|
|
200
|
+
systemImage: "doc.richtext.fill",
|
|
201
|
+
description: Text("Articles you save will appear here.")
|
|
202
|
+
)
|
|
203
|
+
```
|
|
204
|
+
|
|
205
|
+
## Custom List Backgrounds
|
|
206
|
+
|
|
207
|
+
Use `.scrollContentBackground(.hidden)` to replace the default list background:
|
|
208
|
+
|
|
209
|
+
```swift
|
|
210
|
+
List(items) { item in
|
|
211
|
+
ItemRow(item: item)
|
|
212
|
+
}
|
|
213
|
+
.scrollContentBackground(.hidden)
|
|
214
|
+
.background(Color.customBackground)
|
|
215
|
+
```
|
|
216
|
+
|
|
217
|
+
Without `.scrollContentBackground(.hidden)`, a custom `.background()` has no visible effect on `List`.
|
|
218
|
+
|
|
219
|
+
## Table
|
|
220
|
+
|
|
221
|
+
> **Availability:** iOS 16.0+, iPadOS 16.0+, visionOS 1.0+
|
|
222
|
+
|
|
223
|
+
A multi-column data container that presents rows of `Identifiable` data with sortable, selectable columns. On compact size classes (iPhone, iPad Slide Over), columns after the first are automatically hidden.
|
|
224
|
+
|
|
225
|
+
### Basic Table
|
|
226
|
+
|
|
227
|
+
```swift
|
|
228
|
+
struct Person: Identifiable {
|
|
229
|
+
let givenName: String
|
|
230
|
+
let familyName: String
|
|
231
|
+
let emailAddress: String
|
|
232
|
+
let id = UUID()
|
|
233
|
+
var fullName: String { givenName + " " + familyName }
|
|
234
|
+
}
|
|
235
|
+
|
|
236
|
+
struct PeopleTable: View {
|
|
237
|
+
@State private var people: [Person] = [ /* ... */ ]
|
|
238
|
+
|
|
239
|
+
var body: some View {
|
|
240
|
+
Table(people) {
|
|
241
|
+
TableColumn("Given Name", value: \.givenName)
|
|
242
|
+
TableColumn("Family Name", value: \.familyName)
|
|
243
|
+
TableColumn("E-Mail Address", value: \.emailAddress)
|
|
244
|
+
}
|
|
245
|
+
}
|
|
246
|
+
}
|
|
247
|
+
```
|
|
248
|
+
|
|
249
|
+
### Table with Selection
|
|
250
|
+
|
|
251
|
+
Bind to a single `ID` for single-selection, or a `Set<ID>` for multi-selection:
|
|
252
|
+
|
|
253
|
+
```swift
|
|
254
|
+
struct SelectableTable: View {
|
|
255
|
+
@State private var people: [Person] = [ /* ... */ ]
|
|
256
|
+
@State private var selectedPeople = Set<Person.ID>()
|
|
257
|
+
|
|
258
|
+
var body: some View {
|
|
259
|
+
Table(people, selection: $selectedPeople) {
|
|
260
|
+
TableColumn("Given Name", value: \.givenName)
|
|
261
|
+
TableColumn("Family Name", value: \.familyName)
|
|
262
|
+
TableColumn("E-Mail Address", value: \.emailAddress)
|
|
263
|
+
}
|
|
264
|
+
Text("\(selectedPeople.count) people selected")
|
|
265
|
+
}
|
|
266
|
+
}
|
|
267
|
+
```
|
|
268
|
+
|
|
269
|
+
### Sortable Table
|
|
270
|
+
|
|
271
|
+
Provide a binding to `[KeyPathComparator]` and re-sort the data in `.onChange(of:)`:
|
|
272
|
+
|
|
273
|
+
```swift
|
|
274
|
+
struct SortableTable: View {
|
|
275
|
+
@State private var people: [Person] = [ /* ... */ ]
|
|
276
|
+
@State private var sortOrder = [KeyPathComparator(\Person.givenName)]
|
|
277
|
+
|
|
278
|
+
var body: some View {
|
|
279
|
+
Table(people, sortOrder: $sortOrder) {
|
|
280
|
+
TableColumn("Given Name", value: \.givenName)
|
|
281
|
+
TableColumn("Family Name", value: \.familyName)
|
|
282
|
+
TableColumn("E-Mail Address", value: \.emailAddress)
|
|
283
|
+
}
|
|
284
|
+
.onChange(of: sortOrder) { _, newOrder in
|
|
285
|
+
people.sort(using: newOrder)
|
|
286
|
+
}
|
|
287
|
+
}
|
|
288
|
+
}
|
|
289
|
+
```
|
|
290
|
+
|
|
291
|
+
**Important:** The table does **not** sort data itself — you must re-sort the collection when `sortOrder` changes.
|
|
292
|
+
|
|
293
|
+
### Adaptive Table for Compact Size Classes
|
|
294
|
+
|
|
295
|
+
On iPhone or iPad in Slide Over, only the first column is shown. Customize it to display combined information:
|
|
296
|
+
|
|
297
|
+
```swift
|
|
298
|
+
struct AdaptiveTable: View {
|
|
299
|
+
@Environment(\.horizontalSizeClass) private var horizontalSizeClass
|
|
300
|
+
private var isCompact: Bool { horizontalSizeClass == .compact }
|
|
301
|
+
|
|
302
|
+
@State private var people: [Person] = [ /* ... */ ]
|
|
303
|
+
@State private var sortOrder = [KeyPathComparator(\Person.givenName)]
|
|
304
|
+
|
|
305
|
+
var body: some View {
|
|
306
|
+
Table(people, sortOrder: $sortOrder) {
|
|
307
|
+
TableColumn("Given Name", value: \.givenName) { person in
|
|
308
|
+
VStack(alignment: .leading) {
|
|
309
|
+
Text(isCompact ? person.fullName : person.givenName)
|
|
310
|
+
if isCompact {
|
|
311
|
+
Text(person.emailAddress)
|
|
312
|
+
.foregroundStyle(.secondary)
|
|
313
|
+
}
|
|
314
|
+
}
|
|
315
|
+
}
|
|
316
|
+
TableColumn("Family Name", value: \.familyName)
|
|
317
|
+
TableColumn("E-Mail Address", value: \.emailAddress)
|
|
318
|
+
}
|
|
319
|
+
.onChange(of: sortOrder) { _, newOrder in
|
|
320
|
+
people.sort(using: newOrder)
|
|
321
|
+
}
|
|
322
|
+
}
|
|
323
|
+
}
|
|
324
|
+
```
|
|
325
|
+
|
|
326
|
+
### Table with Static Rows
|
|
327
|
+
|
|
328
|
+
Use `init(of:columns:rows:)` when rows are known at compile time:
|
|
329
|
+
|
|
330
|
+
```swift
|
|
331
|
+
struct Purchase: Identifiable {
|
|
332
|
+
let price: Decimal
|
|
333
|
+
let id = UUID()
|
|
334
|
+
}
|
|
335
|
+
|
|
336
|
+
struct TipTable: View {
|
|
337
|
+
let currencyStyle = Decimal.FormatStyle.Currency(code: "USD")
|
|
338
|
+
|
|
339
|
+
var body: some View {
|
|
340
|
+
Table(of: Purchase.self) {
|
|
341
|
+
TableColumn("Base price") { purchase in
|
|
342
|
+
Text(purchase.price, format: currencyStyle)
|
|
343
|
+
}
|
|
344
|
+
TableColumn("With 15% tip") { purchase in
|
|
345
|
+
Text(purchase.price * 1.15, format: currencyStyle)
|
|
346
|
+
}
|
|
347
|
+
TableColumn("With 20% tip") { purchase in
|
|
348
|
+
Text(purchase.price * 1.2, format: currencyStyle)
|
|
349
|
+
}
|
|
350
|
+
} rows: {
|
|
351
|
+
TableRow(Purchase(price: 20))
|
|
352
|
+
TableRow(Purchase(price: 50))
|
|
353
|
+
TableRow(Purchase(price: 75))
|
|
354
|
+
}
|
|
355
|
+
}
|
|
356
|
+
}
|
|
357
|
+
```
|
|
358
|
+
|
|
359
|
+
### Table with Dynamic Number of Columns
|
|
360
|
+
|
|
361
|
+
> **Availability:** iOS 17.4+, iPadOS 17.4+, Mac Catalyst 17.4+, macOS 14.4+, visionOS 1.1+
|
|
362
|
+
|
|
363
|
+
If the number of columns is not known at runtime use `TableColumnForEach` to create columns based on a `RandomAccessCollection` of some data type. Either the collection’s elements must conform to `Identifiable` or you need to provide an id parameter to the `TableColumnForEach` initializer.
|
|
364
|
+
|
|
365
|
+
This can be mixed with static compile time known `TableColumn` usage.
|
|
366
|
+
|
|
367
|
+
```swift
|
|
368
|
+
struct AudioChannel: Identifiable {
|
|
369
|
+
let name: String
|
|
370
|
+
let id: UUID
|
|
371
|
+
}
|
|
372
|
+
|
|
373
|
+
struct AudioSample: Identifiable {
|
|
374
|
+
let id: UUID
|
|
375
|
+
let timestamp: TimeInterval
|
|
376
|
+
func level(channel: AudioChannel.ID) -> Double {
|
|
377
|
+
1
|
|
378
|
+
}
|
|
379
|
+
}
|
|
380
|
+
|
|
381
|
+
@Observable
|
|
382
|
+
class AudioSampleTrack {
|
|
383
|
+
let channels: [AudioChannel]
|
|
384
|
+
var samples: [AudioSample]
|
|
385
|
+
}
|
|
386
|
+
|
|
387
|
+
struct ContentView: View {
|
|
388
|
+
var track: AudioSampleTrack
|
|
389
|
+
|
|
390
|
+
var body: some View {
|
|
391
|
+
Table(track.samples) {
|
|
392
|
+
TableColumn("Timestamp (ms)") { sample in
|
|
393
|
+
Text(sample.timestamp, format: .number.scale(1000))
|
|
394
|
+
.monospacedDigit()
|
|
395
|
+
}
|
|
396
|
+
TableColumnForEach(track.channels) { channel in
|
|
397
|
+
TableColumn(channel.name) { sample in
|
|
398
|
+
Text(sample.level(channel: channel.id),
|
|
399
|
+
format: .number.precision(.fractionLength(2))
|
|
400
|
+
)
|
|
401
|
+
.monospacedDigit()
|
|
402
|
+
}
|
|
403
|
+
.width(ideal: 70)
|
|
404
|
+
.alignment(.numeric)
|
|
405
|
+
}
|
|
406
|
+
}
|
|
407
|
+
}
|
|
408
|
+
}
|
|
409
|
+
```
|
|
410
|
+
|
|
411
|
+
### Table Styles
|
|
412
|
+
|
|
413
|
+
```swift
|
|
414
|
+
// Inset (no borders)
|
|
415
|
+
Table(people) { /* columns */ }
|
|
416
|
+
.tableStyle(.inset)
|
|
417
|
+
|
|
418
|
+
// Hide column headers
|
|
419
|
+
Table(people) { /* columns */ }
|
|
420
|
+
.tableColumnHeaders(.hidden)
|
|
421
|
+
```
|
|
422
|
+
|
|
423
|
+
### Platform Behavior
|
|
424
|
+
|
|
425
|
+
| Platform | Behavior |
|
|
426
|
+
|----------|----------|
|
|
427
|
+
| **iPadOS (regular)** | Full multi-column layout; headers and all columns visible |
|
|
428
|
+
| **iPadOS (compact)** | Only the first column shown; headers hidden |
|
|
429
|
+
| **iPhone (all sizes)** | Only the first column shown; headers hidden; list-like appearance |
|
|
430
|
+
|
|
431
|
+
> **Best Practice:** Prefer handling the compact size class by showing combined info in the first column. This provides a seamless transition when the size class changes (e.g., entering/exiting Slide Over on iPad).
|
|
432
|
+
|
|
433
|
+
## Summary Checklist
|
|
434
|
+
|
|
435
|
+
- [ ] ForEach uses stable identity (never `.indices` for dynamic content)
|
|
436
|
+
- [ ] Identifiable IDs are truly unique across all items
|
|
437
|
+
- [ ] Constant number of views per ForEach element
|
|
438
|
+
- [ ] No inline filtering in ForEach (prefilter and cache instead)
|
|
439
|
+
- [ ] No `AnyView` in list rows
|
|
440
|
+
- [ ] Don't convert enumerated sequences to arrays
|
|
441
|
+
- [ ] Use `.refreshable` for pull-to-refresh
|
|
442
|
+
- [ ] Use `ContentUnavailableView` for empty states (iOS 17+)
|
|
443
|
+
- [ ] Use `.scrollContentBackground(.hidden)` for custom list backgrounds
|
|
444
|
+
- [ ] `Table` adapts for compact size classes (first column shows combined info)
|
|
445
|
+
- [ ] `Table` sorting re-sorts data in `.onChange(of: sortOrder)` (table doesn't sort itself)
|
|
446
|
+
- [ ] `Table` data conforms to `Identifiable`
|