swift-code-reviewer-skill 1.1.1 → 1.2.1
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/CHANGELOG.md +44 -162
- package/README.md +91 -21
- package/SKILL.md +107 -725
- package/bin/install.js +87 -22
- package/package.json +16 -2
- package/references/companion-skills.md +70 -0
- package/skills/README.md +43 -0
- package/skills/swift-concurrency/NOTICE.md +18 -0
- package/skills/swift-concurrency/SKILL.md +235 -0
- package/skills/swift-concurrency/references/actors.md +640 -0
- package/skills/swift-concurrency/references/async-await-basics.md +249 -0
- package/skills/swift-concurrency/references/async-sequences.md +635 -0
- package/skills/swift-concurrency/references/core-data.md +533 -0
- package/skills/swift-concurrency/references/glossary.md +96 -0
- package/skills/swift-concurrency/references/linting.md +38 -0
- package/skills/swift-concurrency/references/memory-management.md +542 -0
- package/skills/swift-concurrency/references/migration.md +721 -0
- package/skills/swift-concurrency/references/performance.md +574 -0
- package/skills/swift-concurrency/references/sendable.md +578 -0
- package/skills/swift-concurrency/references/tasks.md +604 -0
- package/skills/swift-concurrency/references/testing.md +565 -0
- package/skills/swift-concurrency/references/threading.md +452 -0
- package/skills/swift-expert/NOTICE.md +18 -0
- package/skills/swift-expert/SKILL.md +226 -0
- package/skills/swift-expert/references/async-concurrency.md +363 -0
- package/skills/swift-expert/references/memory-performance.md +380 -0
- package/skills/swift-expert/references/protocol-oriented.md +357 -0
- package/skills/swift-expert/references/swiftui-patterns.md +294 -0
- package/skills/swift-expert/references/testing-patterns.md +402 -0
- package/skills/swift-testing/NOTICE.md +18 -0
- package/skills/swift-testing/SKILL.md +295 -0
- package/skills/swift-testing/references/async-testing.md +245 -0
- package/skills/swift-testing/references/dump-snapshot-testing.md +265 -0
- package/skills/swift-testing/references/fixtures.md +193 -0
- package/skills/swift-testing/references/integration-testing.md +189 -0
- package/skills/swift-testing/references/migration-xctest.md +301 -0
- package/skills/swift-testing/references/parameterized-tests.md +171 -0
- package/skills/swift-testing/references/snapshot-testing.md +201 -0
- package/skills/swift-testing/references/test-doubles.md +243 -0
- package/skills/swift-testing/references/test-organization.md +231 -0
- package/skills/swiftui-expert-skill/NOTICE.md +18 -0
- package/skills/swiftui-expert-skill/SKILL.md +281 -0
- package/skills/swiftui-expert-skill/references/accessibility-patterns.md +151 -0
- package/skills/swiftui-expert-skill/references/animation-advanced.md +403 -0
- package/skills/swiftui-expert-skill/references/animation-basics.md +284 -0
- package/skills/swiftui-expert-skill/references/animation-transitions.md +326 -0
- package/skills/swiftui-expert-skill/references/charts-accessibility.md +135 -0
- package/skills/swiftui-expert-skill/references/charts.md +602 -0
- package/skills/swiftui-expert-skill/references/image-optimization.md +203 -0
- package/skills/swiftui-expert-skill/references/latest-apis.md +464 -0
- package/skills/swiftui-expert-skill/references/layout-best-practices.md +266 -0
- package/skills/swiftui-expert-skill/references/liquid-glass.md +414 -0
- package/skills/swiftui-expert-skill/references/list-patterns.md +394 -0
- package/skills/swiftui-expert-skill/references/macos-scenes.md +318 -0
- package/skills/swiftui-expert-skill/references/macos-views.md +357 -0
- package/skills/swiftui-expert-skill/references/macos-window-styling.md +303 -0
- package/skills/swiftui-expert-skill/references/performance-patterns.md +403 -0
- package/skills/swiftui-expert-skill/references/scroll-patterns.md +293 -0
- package/skills/swiftui-expert-skill/references/sheet-navigation-patterns.md +363 -0
- package/skills/swiftui-expert-skill/references/state-management.md +417 -0
- package/skills/swiftui-expert-skill/references/view-structure.md +389 -0
- package/skills/swiftui-ui-patterns/NOTICE.md +18 -0
- package/skills/swiftui-ui-patterns/SKILL.md +95 -0
- package/skills/swiftui-ui-patterns/references/app-wiring.md +201 -0
- package/skills/swiftui-ui-patterns/references/async-state.md +96 -0
- package/skills/swiftui-ui-patterns/references/components-index.md +50 -0
- package/skills/swiftui-ui-patterns/references/controls.md +57 -0
- package/skills/swiftui-ui-patterns/references/deeplinks.md +66 -0
- package/skills/swiftui-ui-patterns/references/focus.md +90 -0
- package/skills/swiftui-ui-patterns/references/form.md +97 -0
- package/skills/swiftui-ui-patterns/references/grids.md +71 -0
- package/skills/swiftui-ui-patterns/references/haptics.md +71 -0
- package/skills/swiftui-ui-patterns/references/input-toolbar.md +51 -0
- package/skills/swiftui-ui-patterns/references/lightweight-clients.md +93 -0
- package/skills/swiftui-ui-patterns/references/list.md +86 -0
- package/skills/swiftui-ui-patterns/references/loading-placeholders.md +38 -0
- package/skills/swiftui-ui-patterns/references/macos-settings.md +71 -0
- package/skills/swiftui-ui-patterns/references/matched-transitions.md +59 -0
- package/skills/swiftui-ui-patterns/references/media.md +73 -0
- package/skills/swiftui-ui-patterns/references/menu-bar.md +101 -0
- package/skills/swiftui-ui-patterns/references/navigationstack.md +159 -0
- package/skills/swiftui-ui-patterns/references/overlay.md +45 -0
- package/skills/swiftui-ui-patterns/references/performance.md +62 -0
- package/skills/swiftui-ui-patterns/references/previews.md +48 -0
- package/skills/swiftui-ui-patterns/references/scroll-reveal.md +133 -0
- package/skills/swiftui-ui-patterns/references/scrollview.md +87 -0
- package/skills/swiftui-ui-patterns/references/searchable.md +71 -0
- package/skills/swiftui-ui-patterns/references/sheets.md +155 -0
- package/skills/swiftui-ui-patterns/references/split-views.md +72 -0
- package/skills/swiftui-ui-patterns/references/tabview.md +114 -0
- package/skills/swiftui-ui-patterns/references/theming.md +71 -0
- package/skills/swiftui-ui-patterns/references/title-menus.md +93 -0
- package/skills/swiftui-ui-patterns/references/top-bar.md +49 -0
- package/templates/agents/swift-code-reviewer.md +78 -0
- package/templates/commands/review.md +56 -0
|
@@ -0,0 +1,326 @@
|
|
|
1
|
+
# SwiftUI Transitions
|
|
2
|
+
|
|
3
|
+
Transitions for view insertion/removal, custom transitions, and the Animatable protocol.
|
|
4
|
+
|
|
5
|
+
## Table of Contents
|
|
6
|
+
- [Property Animations vs Transitions](#property-animations-vs-transitions)
|
|
7
|
+
- [Basic Transitions](#basic-transitions)
|
|
8
|
+
- [Asymmetric Transitions](#asymmetric-transitions)
|
|
9
|
+
- [Custom Transitions](#custom-transitions)
|
|
10
|
+
- [Identity and Transitions](#identity-and-transitions)
|
|
11
|
+
- [The Animatable Protocol](#the-animatable-protocol)
|
|
12
|
+
|
|
13
|
+
---
|
|
14
|
+
|
|
15
|
+
## Property Animations vs Transitions
|
|
16
|
+
|
|
17
|
+
**Property animations**: Interpolate values on views that exist before AND after state change.
|
|
18
|
+
|
|
19
|
+
**Transitions**: Animate views being inserted or removed from the render tree.
|
|
20
|
+
|
|
21
|
+
```swift
|
|
22
|
+
// Property animation - same view, different properties
|
|
23
|
+
Rectangle()
|
|
24
|
+
.frame(width: isExpanded ? 200 : 100, height: 50)
|
|
25
|
+
.animation(.spring, value: isExpanded)
|
|
26
|
+
|
|
27
|
+
// Transition - view inserted/removed
|
|
28
|
+
if showDetail {
|
|
29
|
+
DetailView()
|
|
30
|
+
.transition(.scale)
|
|
31
|
+
}
|
|
32
|
+
```
|
|
33
|
+
|
|
34
|
+
---
|
|
35
|
+
|
|
36
|
+
## Basic Transitions
|
|
37
|
+
|
|
38
|
+
### Critical: Transitions Require Animation Context
|
|
39
|
+
|
|
40
|
+
```swift
|
|
41
|
+
// GOOD - animation outside conditional
|
|
42
|
+
VStack {
|
|
43
|
+
Button("Toggle") { showDetail.toggle() }
|
|
44
|
+
if showDetail {
|
|
45
|
+
DetailView()
|
|
46
|
+
.transition(.slide)
|
|
47
|
+
}
|
|
48
|
+
}
|
|
49
|
+
.animation(.spring, value: showDetail)
|
|
50
|
+
|
|
51
|
+
// GOOD - explicit animation
|
|
52
|
+
Button("Toggle") {
|
|
53
|
+
withAnimation(.spring) {
|
|
54
|
+
showDetail.toggle()
|
|
55
|
+
}
|
|
56
|
+
}
|
|
57
|
+
if showDetail {
|
|
58
|
+
DetailView()
|
|
59
|
+
.transition(.scale.combined(with: .opacity))
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
// BAD - animation inside conditional (removed with view!)
|
|
63
|
+
if showDetail {
|
|
64
|
+
DetailView()
|
|
65
|
+
.transition(.slide)
|
|
66
|
+
.animation(.spring, value: showDetail) // Won't work on removal!
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
// BAD - no animation context
|
|
70
|
+
Button("Toggle") {
|
|
71
|
+
showDetail.toggle() // No animation
|
|
72
|
+
}
|
|
73
|
+
if showDetail {
|
|
74
|
+
DetailView()
|
|
75
|
+
.transition(.slide) // Ignored - just appears/disappears
|
|
76
|
+
}
|
|
77
|
+
```
|
|
78
|
+
|
|
79
|
+
### Built-in Transitions
|
|
80
|
+
|
|
81
|
+
| Transition | Effect |
|
|
82
|
+
|------------|--------|
|
|
83
|
+
| `.opacity` | Fade in/out (default) |
|
|
84
|
+
| `.scale` | Scale up/down |
|
|
85
|
+
| `.slide` | Slide from leading edge |
|
|
86
|
+
| `.move(edge:)` | Move from specific edge |
|
|
87
|
+
| `.offset(x:y:)` | Move by offset amount |
|
|
88
|
+
|
|
89
|
+
### Combining Transitions
|
|
90
|
+
|
|
91
|
+
```swift
|
|
92
|
+
// Parallel - both simultaneously
|
|
93
|
+
.transition(.slide.combined(with: .opacity))
|
|
94
|
+
|
|
95
|
+
// Chained
|
|
96
|
+
.transition(.scale.combined(with: .opacity).combined(with: .offset(y: 20)))
|
|
97
|
+
```
|
|
98
|
+
|
|
99
|
+
---
|
|
100
|
+
|
|
101
|
+
## Asymmetric Transitions
|
|
102
|
+
|
|
103
|
+
Different animations for insertion vs removal.
|
|
104
|
+
|
|
105
|
+
```swift
|
|
106
|
+
// GOOD - different animations for insert/remove
|
|
107
|
+
if showCard {
|
|
108
|
+
CardView()
|
|
109
|
+
.transition(
|
|
110
|
+
.asymmetric(
|
|
111
|
+
insertion: .scale.combined(with: .opacity),
|
|
112
|
+
removal: .move(edge: .bottom).combined(with: .opacity)
|
|
113
|
+
)
|
|
114
|
+
)
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
// BAD - same transition when different behaviors needed
|
|
118
|
+
if showCard {
|
|
119
|
+
CardView()
|
|
120
|
+
.transition(.slide) // Same both ways - may feel awkward
|
|
121
|
+
}
|
|
122
|
+
```
|
|
123
|
+
|
|
124
|
+
---
|
|
125
|
+
|
|
126
|
+
## Custom Transitions
|
|
127
|
+
|
|
128
|
+
### Pre-iOS 17
|
|
129
|
+
|
|
130
|
+
```swift
|
|
131
|
+
struct BlurModifier: ViewModifier {
|
|
132
|
+
var radius: CGFloat
|
|
133
|
+
func body(content: Content) -> some View {
|
|
134
|
+
content.blur(radius: radius)
|
|
135
|
+
}
|
|
136
|
+
}
|
|
137
|
+
|
|
138
|
+
extension AnyTransition {
|
|
139
|
+
static func blur(radius: CGFloat) -> AnyTransition {
|
|
140
|
+
.modifier(
|
|
141
|
+
active: BlurModifier(radius: radius),
|
|
142
|
+
identity: BlurModifier(radius: 0)
|
|
143
|
+
)
|
|
144
|
+
}
|
|
145
|
+
}
|
|
146
|
+
|
|
147
|
+
// Usage
|
|
148
|
+
.transition(.blur(radius: 10))
|
|
149
|
+
```
|
|
150
|
+
|
|
151
|
+
### iOS 17+ (Transition Protocol)
|
|
152
|
+
|
|
153
|
+
```swift
|
|
154
|
+
struct BlurTransition: Transition {
|
|
155
|
+
var radius: CGFloat
|
|
156
|
+
|
|
157
|
+
func body(content: Content, phase: TransitionPhase) -> some View {
|
|
158
|
+
content
|
|
159
|
+
.blur(radius: phase.isIdentity ? 0 : radius)
|
|
160
|
+
.opacity(phase.isIdentity ? 1 : 0)
|
|
161
|
+
}
|
|
162
|
+
}
|
|
163
|
+
|
|
164
|
+
// Usage
|
|
165
|
+
.transition(BlurTransition(radius: 10))
|
|
166
|
+
```
|
|
167
|
+
|
|
168
|
+
### Good vs Bad Custom Transitions
|
|
169
|
+
|
|
170
|
+
```swift
|
|
171
|
+
// GOOD - reusable transition
|
|
172
|
+
if showContent {
|
|
173
|
+
ContentView()
|
|
174
|
+
.transition(BlurTransition(radius: 10))
|
|
175
|
+
}
|
|
176
|
+
|
|
177
|
+
// BAD - inline logic (won't animate on removal!)
|
|
178
|
+
if showContent {
|
|
179
|
+
ContentView()
|
|
180
|
+
.blur(radius: showContent ? 0 : 10) // Not a transition
|
|
181
|
+
.opacity(showContent ? 1 : 0)
|
|
182
|
+
}
|
|
183
|
+
```
|
|
184
|
+
|
|
185
|
+
---
|
|
186
|
+
|
|
187
|
+
## Identity and Transitions
|
|
188
|
+
|
|
189
|
+
View identity changes trigger transitions, not property animations.
|
|
190
|
+
|
|
191
|
+
```swift
|
|
192
|
+
// Triggers transition - different branches have different identities
|
|
193
|
+
if isExpanded {
|
|
194
|
+
Rectangle().frame(width: 200, height: 50)
|
|
195
|
+
} else {
|
|
196
|
+
Rectangle().frame(width: 100, height: 50)
|
|
197
|
+
}
|
|
198
|
+
|
|
199
|
+
// Triggers transition - .id() changes identity
|
|
200
|
+
Rectangle()
|
|
201
|
+
.id(flag) // Different identity when flag changes
|
|
202
|
+
.transition(.scale)
|
|
203
|
+
|
|
204
|
+
// Property animation - same view, same identity
|
|
205
|
+
Rectangle()
|
|
206
|
+
.frame(width: isExpanded ? 200 : 100, height: 50)
|
|
207
|
+
.animation(.spring, value: isExpanded)
|
|
208
|
+
```
|
|
209
|
+
|
|
210
|
+
---
|
|
211
|
+
|
|
212
|
+
## The Animatable Protocol
|
|
213
|
+
|
|
214
|
+
Enables custom property interpolation during animations.
|
|
215
|
+
|
|
216
|
+
### Protocol Definition
|
|
217
|
+
|
|
218
|
+
```swift
|
|
219
|
+
protocol Animatable {
|
|
220
|
+
associatedtype AnimatableData: VectorArithmetic
|
|
221
|
+
var animatableData: AnimatableData { get set }
|
|
222
|
+
}
|
|
223
|
+
```
|
|
224
|
+
|
|
225
|
+
### Basic Implementation
|
|
226
|
+
|
|
227
|
+
```swift
|
|
228
|
+
// GOOD - explicit animatableData
|
|
229
|
+
struct ShakeModifier: ViewModifier, Animatable {
|
|
230
|
+
var shakeCount: Double
|
|
231
|
+
|
|
232
|
+
var animatableData: Double {
|
|
233
|
+
get { shakeCount }
|
|
234
|
+
set { shakeCount = newValue }
|
|
235
|
+
}
|
|
236
|
+
|
|
237
|
+
func body(content: Content) -> some View {
|
|
238
|
+
content.offset(x: sin(shakeCount * .pi * 2) * 10)
|
|
239
|
+
}
|
|
240
|
+
}
|
|
241
|
+
|
|
242
|
+
extension View {
|
|
243
|
+
func shake(count: Int) -> some View {
|
|
244
|
+
modifier(ShakeModifier(shakeCount: Double(count)))
|
|
245
|
+
}
|
|
246
|
+
}
|
|
247
|
+
|
|
248
|
+
// Usage
|
|
249
|
+
Button("Shake") { shakeCount += 3 }
|
|
250
|
+
.shake(count: shakeCount)
|
|
251
|
+
.animation(.default, value: shakeCount)
|
|
252
|
+
|
|
253
|
+
// BAD - missing animatableData (silent failure!)
|
|
254
|
+
struct BadShakeModifier: ViewModifier {
|
|
255
|
+
var shakeCount: Double
|
|
256
|
+
// Missing animatableData! Uses EmptyAnimatableData
|
|
257
|
+
|
|
258
|
+
func body(content: Content) -> some View {
|
|
259
|
+
content.offset(x: sin(shakeCount * .pi * 2) * 10)
|
|
260
|
+
}
|
|
261
|
+
}
|
|
262
|
+
// Animation jumps to final value instead of interpolating
|
|
263
|
+
```
|
|
264
|
+
|
|
265
|
+
### Multiple Properties with AnimatablePair
|
|
266
|
+
|
|
267
|
+
```swift
|
|
268
|
+
// GOOD - AnimatablePair for two properties
|
|
269
|
+
struct ComplexModifier: ViewModifier, Animatable {
|
|
270
|
+
var scale: CGFloat
|
|
271
|
+
var rotation: Double
|
|
272
|
+
|
|
273
|
+
var animatableData: AnimatablePair<CGFloat, Double> {
|
|
274
|
+
get { AnimatablePair(scale, rotation) }
|
|
275
|
+
set {
|
|
276
|
+
scale = newValue.first
|
|
277
|
+
rotation = newValue.second
|
|
278
|
+
}
|
|
279
|
+
}
|
|
280
|
+
|
|
281
|
+
func body(content: Content) -> some View {
|
|
282
|
+
content
|
|
283
|
+
.scaleEffect(scale)
|
|
284
|
+
.rotationEffect(.degrees(rotation))
|
|
285
|
+
}
|
|
286
|
+
}
|
|
287
|
+
|
|
288
|
+
// GOOD - nested AnimatablePair for 3+ properties
|
|
289
|
+
struct ThreePropertyModifier: ViewModifier, Animatable {
|
|
290
|
+
var x: CGFloat
|
|
291
|
+
var y: CGFloat
|
|
292
|
+
var rotation: Double
|
|
293
|
+
|
|
294
|
+
var animatableData: AnimatablePair<AnimatablePair<CGFloat, CGFloat>, Double> {
|
|
295
|
+
get { AnimatablePair(AnimatablePair(x, y), rotation) }
|
|
296
|
+
set {
|
|
297
|
+
x = newValue.first.first
|
|
298
|
+
y = newValue.first.second
|
|
299
|
+
rotation = newValue.second
|
|
300
|
+
}
|
|
301
|
+
}
|
|
302
|
+
|
|
303
|
+
func body(content: Content) -> some View {
|
|
304
|
+
content
|
|
305
|
+
.offset(x: x, y: y)
|
|
306
|
+
.rotationEffect(.degrees(rotation))
|
|
307
|
+
}
|
|
308
|
+
}
|
|
309
|
+
```
|
|
310
|
+
|
|
311
|
+
---
|
|
312
|
+
|
|
313
|
+
## Quick Reference
|
|
314
|
+
|
|
315
|
+
### Do
|
|
316
|
+
- Place transitions outside conditional structures
|
|
317
|
+
- Use `withAnimation` or `.animation` outside the `if`
|
|
318
|
+
- Implement `animatableData` explicitly for custom Animatable
|
|
319
|
+
- Use `AnimatablePair` for multiple animated properties
|
|
320
|
+
- Use asymmetric transitions when insert/remove need different effects
|
|
321
|
+
|
|
322
|
+
### Don't
|
|
323
|
+
- Put animation modifiers inside conditionals for transitions
|
|
324
|
+
- Forget `animatableData` implementation (silent failure)
|
|
325
|
+
- Use inline blur/opacity instead of proper transitions
|
|
326
|
+
- Expect property animation when view identity changes
|
|
@@ -0,0 +1,135 @@
|
|
|
1
|
+
# Swift Charts Accessibility, Fallback, and Resources
|
|
2
|
+
|
|
3
|
+
## Table of Contents
|
|
4
|
+
|
|
5
|
+
- [Accessibility](#accessibility)
|
|
6
|
+
- [Meaningful Labels](#meaningful-labels)
|
|
7
|
+
- [Custom Audio Graphs](#custom-audio-graphs)
|
|
8
|
+
- [Composite Example](#composite-example)
|
|
9
|
+
- [Fallback Strategies](#fallback-strategies)
|
|
10
|
+
- [Version Breakdown](#version-breakdown)
|
|
11
|
+
- [WWDC Sessions](#wwdc-sessions)
|
|
12
|
+
- [Summary Checklist](#summary-checklist)
|
|
13
|
+
|
|
14
|
+
---
|
|
15
|
+
|
|
16
|
+
## Accessibility
|
|
17
|
+
|
|
18
|
+
Swift Charts provides built-in accessibility support. VoiceOver users get three rotor actions automatically:
|
|
19
|
+
|
|
20
|
+
- **Describe Chart** — overview of axes and data series
|
|
21
|
+
- **Audio Graph** — sonification where pitch represents data values
|
|
22
|
+
- **Chart Detail** — interactive mode for exploring individual data points
|
|
23
|
+
|
|
24
|
+
### Meaningful Labels
|
|
25
|
+
|
|
26
|
+
**Always** use clear, descriptive strings in `.value(_, _)` calls. These labels are read by VoiceOver and used in the Audio Graph.
|
|
27
|
+
|
|
28
|
+
```swift
|
|
29
|
+
// Good — descriptive labels
|
|
30
|
+
LineMark(
|
|
31
|
+
x: .value("Date", entry.date),
|
|
32
|
+
y: .value("Daily Steps", entry.count)
|
|
33
|
+
)
|
|
34
|
+
|
|
35
|
+
// Bad — generic labels
|
|
36
|
+
LineMark(
|
|
37
|
+
x: .value("X", entry.date),
|
|
38
|
+
y: .value("Y", entry.count)
|
|
39
|
+
)
|
|
40
|
+
```
|
|
41
|
+
|
|
42
|
+
### Custom Audio Graphs
|
|
43
|
+
|
|
44
|
+
For advanced accessibility, conform your chart view to `AXChartDescriptorRepresentable` and implement `makeChartDescriptor()`. Attach it with `.accessibilityChartDescriptor(self)`.
|
|
45
|
+
|
|
46
|
+
```swift
|
|
47
|
+
struct StepsChart: View, AXChartDescriptorRepresentable {
|
|
48
|
+
let steps: [DailySteps]
|
|
49
|
+
|
|
50
|
+
var body: some View {
|
|
51
|
+
Chart(steps) { day in
|
|
52
|
+
LineMark(x: .value("Date", day.date), y: .value("Steps", day.count))
|
|
53
|
+
}
|
|
54
|
+
.accessibilityChartDescriptor(self)
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
func makeChartDescriptor() -> AXChartDescriptor {
|
|
58
|
+
guard let first = steps.first, let last = steps.last else {
|
|
59
|
+
return AXChartDescriptor(title: "Daily Step Count", summary: nil,
|
|
60
|
+
xAxis: AXNumericDataAxisDescriptor(title: "Date", range: 0...1, gridlinePositions: []) { "\($0)" },
|
|
61
|
+
yAxis: AXNumericDataAxisDescriptor(title: "Steps", range: 0...1, gridlinePositions: []) { "\($0)" },
|
|
62
|
+
additionalAxes: [], series: [])
|
|
63
|
+
}
|
|
64
|
+
let xAxis = AXDateDataAxisDescriptor(
|
|
65
|
+
title: "Date", range: first.date...last.date, gridlinePositions: [])
|
|
66
|
+
let yAxis = AXNumericDataAxisDescriptor(
|
|
67
|
+
title: "Steps", range: 0...Double(steps.map(\.count).max() ?? 0),
|
|
68
|
+
gridlinePositions: []) { "\(Int($0)) steps" }
|
|
69
|
+
let series = AXDataSeriesDescriptor(
|
|
70
|
+
name: "Daily Steps", isContinuous: true,
|
|
71
|
+
dataPoints: steps.map { .init(x: $0.date, y: Double($0.count)) })
|
|
72
|
+
return AXChartDescriptor(title: "Daily Step Count", summary: nil,
|
|
73
|
+
xAxis: xAxis, yAxis: yAxis, additionalAxes: [], series: [series])
|
|
74
|
+
}
|
|
75
|
+
}
|
|
76
|
+
```
|
|
77
|
+
|
|
78
|
+
## Composite Example
|
|
79
|
+
|
|
80
|
+
A scrollable bar chart with range selection combining multiple iOS 17+ APIs:
|
|
81
|
+
|
|
82
|
+
```swift
|
|
83
|
+
@State private var selectedRange: ClosedRange<Int>?
|
|
84
|
+
|
|
85
|
+
Chart(weeklyRevenue) { week in
|
|
86
|
+
BarMark(x: .value("Week", week.index), y: .value("Revenue", week.revenue))
|
|
87
|
+
.foregroundStyle(by: .value("Region", week.region))
|
|
88
|
+
}
|
|
89
|
+
.chartScrollableAxes(.horizontal)
|
|
90
|
+
.chartXVisibleDomain(length: 8)
|
|
91
|
+
.chartXSelection(range: $selectedRange)
|
|
92
|
+
.chartXAxis {
|
|
93
|
+
AxisMarks(values: .stride(by: 1)) {
|
|
94
|
+
AxisGridLine()
|
|
95
|
+
AxisValueLabel { Text("W\($0.as(Int.self) ?? 0)") }
|
|
96
|
+
}
|
|
97
|
+
}
|
|
98
|
+
```
|
|
99
|
+
|
|
100
|
+
## Fallback Strategies
|
|
101
|
+
|
|
102
|
+
Gate advanced APIs with `#available` and provide a fallback chart without the gated features. Because chart modifiers like `.chartXSelection` change the return type, you must duplicate the entire `Chart` — you cannot conditionally apply the modifier:
|
|
103
|
+
|
|
104
|
+
### Version Breakdown
|
|
105
|
+
|
|
106
|
+
- iOS 16+: `Chart`, custom axes, scales, `BarMark`, `LineMark`, `AreaMark`, `PointMark`, `RectangleMark`, `RuleMark`, `ChartProxy`, `chartOverlay`, `chartBackground`
|
|
107
|
+
- iOS 17+: `SectorMark`, `chartXSelection`, `chartYSelection`, `chartAngleSelection`, `chartScrollableAxes`, visible-domain scrolling APIs, `chartGesture`
|
|
108
|
+
- iOS 18+: `AreaPlot`, `BarPlot`, `LinePlot`, `PointPlot`, `RectanglePlot`, `RulePlot`, `SectorPlot`, function plotting
|
|
109
|
+
- iOS 26+: `Chart3D`, `SurfacePlot`, Z-axis marks, 3D camera and pose APIs
|
|
110
|
+
|
|
111
|
+
## WWDC Sessions
|
|
112
|
+
|
|
113
|
+
- [Hello Swift Charts](https://developer.apple.com/videos/play/wwdc2022/10136/) (WWDC 2022) — introduction to the framework
|
|
114
|
+
- [Swift Charts: Raise the bar](https://developer.apple.com/videos/play/wwdc2022/10137/) (WWDC 2022) — marks, composition, customization
|
|
115
|
+
- [Design an effective chart](https://developer.apple.com/videos/play/wwdc2022/110340/) (WWDC 2022) — chart design principles
|
|
116
|
+
- [Design app experiences with charts](https://developer.apple.com/videos/play/wwdc2022/110342/) (WWDC 2022) — integrating charts into app UX
|
|
117
|
+
- [Explore pie charts and interactivity in Swift Charts](https://developer.apple.com/videos/play/wwdc2023/10037/) (WWDC 2023) — SectorMark, selection, scrolling
|
|
118
|
+
- [Swift Charts: Vectorized and function plots](https://developer.apple.com/videos/play/wwdc2024/10155/) (WWDC 2024) — LinePlot, AreaPlot, function plotting
|
|
119
|
+
- [Bring Swift Charts to the third dimension](https://developer.apple.com/videos/play/wwdc2025/313/) (WWDC 2025) — Chart3D, SurfacePlot, 3D marks
|
|
120
|
+
|
|
121
|
+
## Summary Checklist
|
|
122
|
+
|
|
123
|
+
- [ ] `import Charts` is present in files using chart types
|
|
124
|
+
- [ ] Deployment target matches the APIs used (`Chart` on iOS 16+, selection and `SectorMark` on iOS 17+, plot types on iOS 18+, `Chart3D` on iOS 26+)
|
|
125
|
+
- [ ] Chart data models use `Identifiable` (or `Chart(data, id:)` is provided)
|
|
126
|
+
- [ ] All chart families are represented with the correct mark type
|
|
127
|
+
- [ ] Axes use `AxisMarks` when default ticks are too dense or unclear
|
|
128
|
+
- [ ] `chartXScale` or `chartYScale` is set when fixed domains matter
|
|
129
|
+
- [ ] Chart-wide modifiers are applied to `Chart`, not individual marks
|
|
130
|
+
- [ ] `foregroundStyle(by:)` used for categorical series (not manual per-mark colors)
|
|
131
|
+
- [ ] Single-value selection uses `chartXSelection(value:)` or `chartYSelection(value:)`
|
|
132
|
+
- [ ] Range selection uses `chartXSelection(range:)` or `chartYSelection(range:)`
|
|
133
|
+
- [ ] `SectorMark` selection uses `chartAngleSelection(value:)`
|
|
134
|
+
- [ ] iOS 17+, iOS 18+, and iOS 26+ APIs are guarded with `#available`
|
|
135
|
+
- [ ] `.value()` labels are descriptive for VoiceOver and Audio Graph accessibility
|