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,403 @@
|
|
|
1
|
+
# SwiftUI Advanced Animations
|
|
2
|
+
|
|
3
|
+
Transactions, phase animations (iOS 17+), keyframe animations (iOS 17+), completion handlers (iOS 17+), and `@Animatable` macro (iOS 26+).
|
|
4
|
+
|
|
5
|
+
## Table of Contents
|
|
6
|
+
- [Transactions](#transactions)
|
|
7
|
+
- [Phase Animations (iOS 17+)](#phase-animations-ios-17)
|
|
8
|
+
- [Keyframe Animations (iOS 17+)](#keyframe-animations-ios-17)
|
|
9
|
+
- [Animation Completion Handlers (iOS 17+)](#animation-completion-handlers-ios-17)
|
|
10
|
+
- [@Animatable Macro (iOS 26+)](#animatable-macro-ios-26)
|
|
11
|
+
|
|
12
|
+
---
|
|
13
|
+
|
|
14
|
+
## Transactions
|
|
15
|
+
|
|
16
|
+
The underlying mechanism for all animations in SwiftUI.
|
|
17
|
+
|
|
18
|
+
### Basic Usage
|
|
19
|
+
|
|
20
|
+
```swift
|
|
21
|
+
// withAnimation is shorthand for withTransaction
|
|
22
|
+
withAnimation(.default) { flag.toggle() }
|
|
23
|
+
|
|
24
|
+
// Equivalent explicit transaction
|
|
25
|
+
var transaction = Transaction(animation: .default)
|
|
26
|
+
withTransaction(transaction) { flag.toggle() }
|
|
27
|
+
```
|
|
28
|
+
|
|
29
|
+
### The .transaction Modifier
|
|
30
|
+
|
|
31
|
+
```swift
|
|
32
|
+
Rectangle()
|
|
33
|
+
.frame(width: flag ? 100 : 50, height: 50)
|
|
34
|
+
.transaction { t in
|
|
35
|
+
t.animation = .default
|
|
36
|
+
}
|
|
37
|
+
```
|
|
38
|
+
|
|
39
|
+
**Note:** This behaves like the deprecated `.animation(_:)` without value parameter - it animates on every state change.
|
|
40
|
+
|
|
41
|
+
### Animation Precedence
|
|
42
|
+
|
|
43
|
+
**Implicit animations override explicit animations** (later in view tree wins).
|
|
44
|
+
|
|
45
|
+
```swift
|
|
46
|
+
Button("Tap") {
|
|
47
|
+
withAnimation(.linear) { flag.toggle() }
|
|
48
|
+
}
|
|
49
|
+
.animation(.bouncy, value: flag) // .bouncy wins!
|
|
50
|
+
```
|
|
51
|
+
|
|
52
|
+
### Disabling Animations
|
|
53
|
+
|
|
54
|
+
```swift
|
|
55
|
+
// Prevent implicit animations from overriding
|
|
56
|
+
.transaction { t in
|
|
57
|
+
t.disablesAnimations = true
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
// Remove animation entirely
|
|
61
|
+
.transaction { $0.animation = nil }
|
|
62
|
+
```
|
|
63
|
+
|
|
64
|
+
### Custom Transaction Keys (iOS 17+)
|
|
65
|
+
|
|
66
|
+
Pass metadata through transactions.
|
|
67
|
+
|
|
68
|
+
```swift
|
|
69
|
+
struct ChangeSourceKey: TransactionKey {
|
|
70
|
+
static let defaultValue: String = "unknown"
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
extension Transaction {
|
|
74
|
+
var changeSource: String {
|
|
75
|
+
get { self[ChangeSourceKey.self] }
|
|
76
|
+
set { self[ChangeSourceKey.self] = newValue }
|
|
77
|
+
}
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
// Set source
|
|
81
|
+
var transaction = Transaction(animation: .default)
|
|
82
|
+
transaction.changeSource = "server"
|
|
83
|
+
withTransaction(transaction) { flag.toggle() }
|
|
84
|
+
|
|
85
|
+
// Read in view tree
|
|
86
|
+
.transaction { t in
|
|
87
|
+
if t.changeSource == "server" {
|
|
88
|
+
t.animation = .smooth
|
|
89
|
+
} else {
|
|
90
|
+
t.animation = .bouncy
|
|
91
|
+
}
|
|
92
|
+
}
|
|
93
|
+
```
|
|
94
|
+
|
|
95
|
+
---
|
|
96
|
+
|
|
97
|
+
## Phase Animations (iOS 17+)
|
|
98
|
+
|
|
99
|
+
Cycle through discrete phases automatically. Each phase change is a separate animation.
|
|
100
|
+
|
|
101
|
+
### Basic Usage
|
|
102
|
+
|
|
103
|
+
```swift
|
|
104
|
+
// GOOD - triggered phase animation
|
|
105
|
+
Button("Shake") { trigger += 1 }
|
|
106
|
+
.phaseAnimator(
|
|
107
|
+
[0.0, -10.0, 10.0, -5.0, 5.0, 0.0],
|
|
108
|
+
trigger: trigger
|
|
109
|
+
) { content, offset in
|
|
110
|
+
content.offset(x: offset)
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
// Infinite loop (no trigger)
|
|
114
|
+
Circle()
|
|
115
|
+
.phaseAnimator([1.0, 1.2, 1.0]) { content, scale in
|
|
116
|
+
content.scaleEffect(scale)
|
|
117
|
+
}
|
|
118
|
+
```
|
|
119
|
+
|
|
120
|
+
### Enum Phases (Recommended for Clarity)
|
|
121
|
+
|
|
122
|
+
```swift
|
|
123
|
+
// GOOD - enum phases are self-documenting
|
|
124
|
+
enum BouncePhase: CaseIterable {
|
|
125
|
+
case initial, up, down, settle
|
|
126
|
+
|
|
127
|
+
var scale: CGFloat {
|
|
128
|
+
switch self {
|
|
129
|
+
case .initial: 1.0
|
|
130
|
+
case .up: 1.2
|
|
131
|
+
case .down: 0.9
|
|
132
|
+
case .settle: 1.0
|
|
133
|
+
}
|
|
134
|
+
}
|
|
135
|
+
}
|
|
136
|
+
|
|
137
|
+
Circle()
|
|
138
|
+
.phaseAnimator(BouncePhase.allCases, trigger: trigger) { content, phase in
|
|
139
|
+
content.scaleEffect(phase.scale)
|
|
140
|
+
}
|
|
141
|
+
```
|
|
142
|
+
|
|
143
|
+
### Custom Timing Per Phase
|
|
144
|
+
|
|
145
|
+
```swift
|
|
146
|
+
.phaseAnimator([0, -20, 20], trigger: trigger) { content, offset in
|
|
147
|
+
content.offset(x: offset)
|
|
148
|
+
} animation: { phase in
|
|
149
|
+
switch phase {
|
|
150
|
+
case -20: .bouncy
|
|
151
|
+
case 20: .linear
|
|
152
|
+
default: .smooth
|
|
153
|
+
}
|
|
154
|
+
}
|
|
155
|
+
```
|
|
156
|
+
|
|
157
|
+
### Good vs Bad
|
|
158
|
+
|
|
159
|
+
```swift
|
|
160
|
+
// GOOD - use phaseAnimator for multi-step sequences
|
|
161
|
+
.phaseAnimator([0, -10, 10, 0], trigger: trigger) { content, offset in
|
|
162
|
+
content.offset(x: offset)
|
|
163
|
+
}
|
|
164
|
+
|
|
165
|
+
// BAD - manual DispatchQueue sequencing
|
|
166
|
+
Button("Animate") {
|
|
167
|
+
withAnimation(.easeOut(duration: 0.1)) { offset = -10 }
|
|
168
|
+
DispatchQueue.main.asyncAfter(deadline: .now() + 0.1) {
|
|
169
|
+
withAnimation { offset = 10 }
|
|
170
|
+
}
|
|
171
|
+
DispatchQueue.main.asyncAfter(deadline: .now() + 0.2) {
|
|
172
|
+
withAnimation { offset = 0 }
|
|
173
|
+
}
|
|
174
|
+
}
|
|
175
|
+
```
|
|
176
|
+
|
|
177
|
+
---
|
|
178
|
+
|
|
179
|
+
## Keyframe Animations (iOS 17+)
|
|
180
|
+
|
|
181
|
+
Precise timing control with exact values at specific times.
|
|
182
|
+
|
|
183
|
+
### Basic Usage
|
|
184
|
+
|
|
185
|
+
```swift
|
|
186
|
+
Button("Bounce") { trigger += 1 }
|
|
187
|
+
.keyframeAnimator(
|
|
188
|
+
initialValue: AnimationValues(),
|
|
189
|
+
trigger: trigger
|
|
190
|
+
) { content, value in
|
|
191
|
+
content
|
|
192
|
+
.scaleEffect(value.scale)
|
|
193
|
+
.offset(y: value.verticalOffset)
|
|
194
|
+
} keyframes: { _ in
|
|
195
|
+
KeyframeTrack(\.scale) {
|
|
196
|
+
SpringKeyframe(1.2, duration: 0.15)
|
|
197
|
+
SpringKeyframe(0.9, duration: 0.1)
|
|
198
|
+
SpringKeyframe(1.0, duration: 0.15)
|
|
199
|
+
}
|
|
200
|
+
KeyframeTrack(\.verticalOffset) {
|
|
201
|
+
LinearKeyframe(-20, duration: 0.15)
|
|
202
|
+
LinearKeyframe(0, duration: 0.25)
|
|
203
|
+
}
|
|
204
|
+
}
|
|
205
|
+
|
|
206
|
+
struct AnimationValues {
|
|
207
|
+
var scale: CGFloat = 1.0
|
|
208
|
+
var verticalOffset: CGFloat = 0
|
|
209
|
+
}
|
|
210
|
+
```
|
|
211
|
+
|
|
212
|
+
### Keyframe Types
|
|
213
|
+
|
|
214
|
+
| Type | Behavior |
|
|
215
|
+
|------|----------|
|
|
216
|
+
| `CubicKeyframe` | Smooth interpolation |
|
|
217
|
+
| `LinearKeyframe` | Straight-line interpolation |
|
|
218
|
+
| `SpringKeyframe` | Spring physics |
|
|
219
|
+
| `MoveKeyframe` | Instant jump (no interpolation) |
|
|
220
|
+
|
|
221
|
+
### Multiple Synchronized Tracks
|
|
222
|
+
|
|
223
|
+
Tracks run **in parallel**, each animating one property.
|
|
224
|
+
|
|
225
|
+
```swift
|
|
226
|
+
// GOOD - bell shake with synchronized rotation and scale
|
|
227
|
+
struct BellAnimation {
|
|
228
|
+
var rotation: Double = 0
|
|
229
|
+
var scale: CGFloat = 1.0
|
|
230
|
+
}
|
|
231
|
+
|
|
232
|
+
Image(systemName: "bell.fill")
|
|
233
|
+
.keyframeAnimator(
|
|
234
|
+
initialValue: BellAnimation(),
|
|
235
|
+
trigger: trigger
|
|
236
|
+
) { content, value in
|
|
237
|
+
content
|
|
238
|
+
.rotationEffect(.degrees(value.rotation))
|
|
239
|
+
.scaleEffect(value.scale)
|
|
240
|
+
} keyframes: { _ in
|
|
241
|
+
KeyframeTrack(\.rotation) {
|
|
242
|
+
CubicKeyframe(15, duration: 0.1)
|
|
243
|
+
CubicKeyframe(-15, duration: 0.1)
|
|
244
|
+
CubicKeyframe(10, duration: 0.1)
|
|
245
|
+
CubicKeyframe(-10, duration: 0.1)
|
|
246
|
+
CubicKeyframe(0, duration: 0.1)
|
|
247
|
+
}
|
|
248
|
+
KeyframeTrack(\.scale) {
|
|
249
|
+
CubicKeyframe(1.1, duration: 0.25)
|
|
250
|
+
CubicKeyframe(1.0, duration: 0.25)
|
|
251
|
+
}
|
|
252
|
+
}
|
|
253
|
+
|
|
254
|
+
// BAD - manual timer-based animation
|
|
255
|
+
Image(systemName: "bell.fill")
|
|
256
|
+
.onTapGesture {
|
|
257
|
+
withAnimation(.easeOut(duration: 0.1)) { rotation = 15 }
|
|
258
|
+
DispatchQueue.main.asyncAfter(deadline: .now() + 0.1) {
|
|
259
|
+
withAnimation { rotation = -15 }
|
|
260
|
+
}
|
|
261
|
+
// ... more manual timing - error prone
|
|
262
|
+
}
|
|
263
|
+
```
|
|
264
|
+
|
|
265
|
+
### KeyframeTimeline (iOS 17+)
|
|
266
|
+
|
|
267
|
+
Query animation values directly for testing or non-SwiftUI use.
|
|
268
|
+
|
|
269
|
+
```swift
|
|
270
|
+
let timeline = KeyframeTimeline(initialValue: AnimationValues()) {
|
|
271
|
+
KeyframeTrack(\.scale) {
|
|
272
|
+
CubicKeyframe(1.2, duration: 0.25)
|
|
273
|
+
CubicKeyframe(1.0, duration: 0.25)
|
|
274
|
+
}
|
|
275
|
+
}
|
|
276
|
+
|
|
277
|
+
let midpoint = timeline.value(time: 0.25)
|
|
278
|
+
print(midpoint.scale) // Value at 0.25 seconds
|
|
279
|
+
```
|
|
280
|
+
|
|
281
|
+
---
|
|
282
|
+
|
|
283
|
+
## Animation Completion Handlers (iOS 17+)
|
|
284
|
+
|
|
285
|
+
Execute code when animations finish.
|
|
286
|
+
|
|
287
|
+
### With withAnimation
|
|
288
|
+
|
|
289
|
+
```swift
|
|
290
|
+
// GOOD - completion with withAnimation
|
|
291
|
+
Button("Animate") {
|
|
292
|
+
withAnimation(.spring) {
|
|
293
|
+
isExpanded.toggle()
|
|
294
|
+
} completion: {
|
|
295
|
+
showNextStep = true
|
|
296
|
+
}
|
|
297
|
+
}
|
|
298
|
+
```
|
|
299
|
+
|
|
300
|
+
### With Transaction (For Reexecution)
|
|
301
|
+
|
|
302
|
+
```swift
|
|
303
|
+
// GOOD - completion fires on every trigger change
|
|
304
|
+
Circle()
|
|
305
|
+
.scaleEffect(bounceCount % 2 == 0 ? 1.0 : 1.2)
|
|
306
|
+
.transaction(value: bounceCount) { transaction in
|
|
307
|
+
transaction.animation = .spring
|
|
308
|
+
transaction.addAnimationCompletion {
|
|
309
|
+
message = "Bounce \(bounceCount) complete"
|
|
310
|
+
}
|
|
311
|
+
}
|
|
312
|
+
|
|
313
|
+
// BAD - completion only fires ONCE (no value parameter)
|
|
314
|
+
Circle()
|
|
315
|
+
.scaleEffect(bounceCount % 2 == 0 ? 1.0 : 1.2)
|
|
316
|
+
.animation(.spring, value: bounceCount)
|
|
317
|
+
.transaction { transaction in // No value!
|
|
318
|
+
transaction.addAnimationCompletion {
|
|
319
|
+
completionCount += 1 // Only fires once, ever
|
|
320
|
+
}
|
|
321
|
+
}
|
|
322
|
+
```
|
|
323
|
+
|
|
324
|
+
---
|
|
325
|
+
|
|
326
|
+
## @Animatable Macro (iOS 26+)
|
|
327
|
+
|
|
328
|
+
The `@Animatable` macro auto-synthesizes `animatableData` from all animatable stored properties, eliminating verbose manual conformance. Use `@AnimatableIgnored` to exclude properties that should not animate.
|
|
329
|
+
|
|
330
|
+
### Before (Manual)
|
|
331
|
+
|
|
332
|
+
```swift
|
|
333
|
+
struct Wedge: Shape {
|
|
334
|
+
var startAngle: Angle
|
|
335
|
+
var endAngle: Angle
|
|
336
|
+
var drawClockwise: Bool
|
|
337
|
+
|
|
338
|
+
var animatableData: AnimatablePair<Double, Double> {
|
|
339
|
+
get { AnimatablePair(startAngle.radians, endAngle.radians) }
|
|
340
|
+
set {
|
|
341
|
+
startAngle = .radians(newValue.first)
|
|
342
|
+
endAngle = .radians(newValue.second)
|
|
343
|
+
}
|
|
344
|
+
}
|
|
345
|
+
|
|
346
|
+
func path(in rect: CGRect) -> Path { /* ... */ }
|
|
347
|
+
}
|
|
348
|
+
```
|
|
349
|
+
|
|
350
|
+
### After (@Animatable)
|
|
351
|
+
|
|
352
|
+
```swift
|
|
353
|
+
@Animatable
|
|
354
|
+
struct Wedge: Shape {
|
|
355
|
+
var startAngle: Angle
|
|
356
|
+
var endAngle: Angle
|
|
357
|
+
@AnimatableIgnored var drawClockwise: Bool
|
|
358
|
+
|
|
359
|
+
func path(in rect: CGRect) -> Path { /* ... */ }
|
|
360
|
+
}
|
|
361
|
+
```
|
|
362
|
+
|
|
363
|
+
### When to Use
|
|
364
|
+
- **Prefer `@Animatable`** for any custom `Shape`, `AnimatableModifier`, or type conforming to `Animatable` with multiple properties
|
|
365
|
+
- **Use `@AnimatableIgnored`** for properties that control behavior but should not interpolate (e.g., directions, flags, identifiers)
|
|
366
|
+
- The macro works with any type conforming to `Animatable`, not just `Shape`
|
|
367
|
+
|
|
368
|
+
> Source: "What's new in SwiftUI" (WWDC25, session 256)
|
|
369
|
+
|
|
370
|
+
---
|
|
371
|
+
|
|
372
|
+
## Quick Reference
|
|
373
|
+
|
|
374
|
+
### Transactions (All iOS versions)
|
|
375
|
+
- `withTransaction` is the explicit form of `withAnimation`
|
|
376
|
+
- Implicit animations override explicit (later in view tree wins)
|
|
377
|
+
- Use `disablesAnimations` to prevent override
|
|
378
|
+
- Use `.transaction { $0.animation = nil }` to remove animation
|
|
379
|
+
|
|
380
|
+
### Custom Transaction Keys (iOS 17+)
|
|
381
|
+
- Pass metadata through animation system via `TransactionKey`
|
|
382
|
+
|
|
383
|
+
### Phase Animations (iOS 17+)
|
|
384
|
+
- Use for multi-step sequences returning to start
|
|
385
|
+
- Prefer enum phases for clarity
|
|
386
|
+
- Each phase change is a separate animation
|
|
387
|
+
- Use `trigger` parameter for one-shot animations
|
|
388
|
+
|
|
389
|
+
### Keyframe Animations (iOS 17+)
|
|
390
|
+
- Use for precise timing control
|
|
391
|
+
- Tracks run in parallel
|
|
392
|
+
- Use `KeyframeTimeline` for testing/advanced use
|
|
393
|
+
- Prefer over manual DispatchQueue timing
|
|
394
|
+
|
|
395
|
+
### Completion Handlers (iOS 17+)
|
|
396
|
+
- Use `withAnimation(.animation) { } completion: { }` for one-shot completion handlers
|
|
397
|
+
- Use `.transaction(value:)` for handlers that should refire on every value change
|
|
398
|
+
- Without `value:` parameter, completion only fires once
|
|
399
|
+
|
|
400
|
+
### @Animatable Macro (iOS 26+)
|
|
401
|
+
- Use `@Animatable` to auto-synthesize `animatableData` from stored properties
|
|
402
|
+
- Use `@AnimatableIgnored` to exclude non-animatable properties
|
|
403
|
+
- Replaces verbose manual `animatableData` getters/setters
|
|
@@ -0,0 +1,284 @@
|
|
|
1
|
+
# SwiftUI Animation Basics
|
|
2
|
+
|
|
3
|
+
Core animation concepts, implicit vs explicit animations, timing curves, and performance patterns.
|
|
4
|
+
|
|
5
|
+
## Table of Contents
|
|
6
|
+
- [Core Concepts](#core-concepts)
|
|
7
|
+
- [Implicit Animations](#implicit-animations)
|
|
8
|
+
- [Explicit Animations](#explicit-animations)
|
|
9
|
+
- [Animation Placement](#animation-placement)
|
|
10
|
+
- [Selective Animation](#selective-animation)
|
|
11
|
+
- [Timing Curves](#timing-curves)
|
|
12
|
+
- [Animation Performance](#animation-performance)
|
|
13
|
+
- [Disabling Animations](#disabling-animations)
|
|
14
|
+
- [Debugging](#debugging)
|
|
15
|
+
|
|
16
|
+
---
|
|
17
|
+
|
|
18
|
+
## Core Concepts
|
|
19
|
+
|
|
20
|
+
State changes trigger view updates. SwiftUI provides mechanisms to animate these changes.
|
|
21
|
+
|
|
22
|
+
**Animation Process:**
|
|
23
|
+
1. State change triggers view tree re-evaluation
|
|
24
|
+
2. SwiftUI compares new tree to current render tree
|
|
25
|
+
3. Animatable properties are identified and interpolated (~60 fps)
|
|
26
|
+
|
|
27
|
+
**Key Characteristics:**
|
|
28
|
+
- Animations are additive and cancelable
|
|
29
|
+
- Always start from current render tree state
|
|
30
|
+
- Blend smoothly when interrupted
|
|
31
|
+
|
|
32
|
+
---
|
|
33
|
+
|
|
34
|
+
## Implicit Animations
|
|
35
|
+
|
|
36
|
+
Use `.animation(_:value:)` to animate when a specific value changes.
|
|
37
|
+
|
|
38
|
+
```swift
|
|
39
|
+
// GOOD - uses value parameter
|
|
40
|
+
Rectangle()
|
|
41
|
+
.frame(width: isExpanded ? 200 : 100, height: 50)
|
|
42
|
+
.animation(.spring, value: isExpanded)
|
|
43
|
+
.onTapGesture { isExpanded.toggle() }
|
|
44
|
+
|
|
45
|
+
// BAD - deprecated, animates all changes unexpectedly
|
|
46
|
+
Rectangle()
|
|
47
|
+
.frame(width: isExpanded ? 200 : 100, height: 50)
|
|
48
|
+
.animation(.spring) // Deprecated!
|
|
49
|
+
```
|
|
50
|
+
|
|
51
|
+
---
|
|
52
|
+
|
|
53
|
+
## Explicit Animations
|
|
54
|
+
|
|
55
|
+
Use `withAnimation` for event-driven state changes.
|
|
56
|
+
|
|
57
|
+
```swift
|
|
58
|
+
// GOOD - explicit animation
|
|
59
|
+
Button("Toggle") {
|
|
60
|
+
withAnimation(.spring) {
|
|
61
|
+
isExpanded.toggle()
|
|
62
|
+
}
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
// BAD - no animation context
|
|
66
|
+
Button("Toggle") {
|
|
67
|
+
isExpanded.toggle() // Abrupt change
|
|
68
|
+
}
|
|
69
|
+
```
|
|
70
|
+
|
|
71
|
+
**When to use which:**
|
|
72
|
+
- **Implicit**: Animations tied to specific value changes, precise view tree scope
|
|
73
|
+
- **Explicit**: Event-driven animations (button taps, gestures)
|
|
74
|
+
|
|
75
|
+
---
|
|
76
|
+
|
|
77
|
+
## Animation Placement
|
|
78
|
+
|
|
79
|
+
Place animation modifiers after the properties they should animate.
|
|
80
|
+
|
|
81
|
+
```swift
|
|
82
|
+
// GOOD - animation after properties
|
|
83
|
+
Rectangle()
|
|
84
|
+
.frame(width: isExpanded ? 200 : 100, height: 50)
|
|
85
|
+
.foregroundStyle(isExpanded ? .blue : .red)
|
|
86
|
+
.animation(.default, value: isExpanded) // Animates both
|
|
87
|
+
|
|
88
|
+
// BAD - animation before properties
|
|
89
|
+
Rectangle()
|
|
90
|
+
.animation(.default, value: isExpanded) // Too early!
|
|
91
|
+
.frame(width: isExpanded ? 200 : 100, height: 50)
|
|
92
|
+
```
|
|
93
|
+
|
|
94
|
+
---
|
|
95
|
+
|
|
96
|
+
## Selective Animation
|
|
97
|
+
|
|
98
|
+
Animate only specific properties using multiple animation modifiers or scoped animations.
|
|
99
|
+
|
|
100
|
+
```swift
|
|
101
|
+
// GOOD - selective animation
|
|
102
|
+
Rectangle()
|
|
103
|
+
.frame(width: isExpanded ? 200 : 100, height: 50)
|
|
104
|
+
.animation(.spring, value: isExpanded) // Animate size
|
|
105
|
+
.foregroundStyle(isExpanded ? .blue : .red)
|
|
106
|
+
.animation(nil, value: isExpanded) // Don't animate color
|
|
107
|
+
|
|
108
|
+
// iOS 17+ scoped animation
|
|
109
|
+
Rectangle()
|
|
110
|
+
.foregroundStyle(isExpanded ? .blue : .red) // Not animated
|
|
111
|
+
.animation(.spring) {
|
|
112
|
+
$0.frame(width: isExpanded ? 200 : 100, height: 50) // Animated
|
|
113
|
+
}
|
|
114
|
+
```
|
|
115
|
+
|
|
116
|
+
---
|
|
117
|
+
|
|
118
|
+
## Timing Curves
|
|
119
|
+
|
|
120
|
+
### Built-in Curves
|
|
121
|
+
|
|
122
|
+
| Curve | Use Case |
|
|
123
|
+
|-------|----------|
|
|
124
|
+
| `.spring` | Interactive elements, most UI |
|
|
125
|
+
| `.easeInOut` | Appearance changes |
|
|
126
|
+
| `.bouncy` | Playful feedback (iOS 17+) |
|
|
127
|
+
| `.linear` | Progress indicators only |
|
|
128
|
+
|
|
129
|
+
### Modifiers
|
|
130
|
+
|
|
131
|
+
```swift
|
|
132
|
+
.animation(.default.speed(2.0), value: flag) // 2x faster
|
|
133
|
+
.animation(.default.delay(0.5), value: flag) // Delayed start
|
|
134
|
+
.animation(.default.repeatCount(3, autoreverses: true), value: flag)
|
|
135
|
+
```
|
|
136
|
+
|
|
137
|
+
### Good vs Bad Timing
|
|
138
|
+
|
|
139
|
+
```swift
|
|
140
|
+
// GOOD - appropriate timing for interaction type
|
|
141
|
+
Button("Tap") {
|
|
142
|
+
withAnimation(.spring(response: 0.3, dampingFraction: 0.7)) {
|
|
143
|
+
isActive.toggle()
|
|
144
|
+
}
|
|
145
|
+
}
|
|
146
|
+
.scaleEffect(isActive ? 0.95 : 1.0)
|
|
147
|
+
|
|
148
|
+
// BAD - too slow for button feedback
|
|
149
|
+
Button("Tap") {
|
|
150
|
+
withAnimation(.easeInOut(duration: 1.0)) { // Way too slow!
|
|
151
|
+
isActive.toggle()
|
|
152
|
+
}
|
|
153
|
+
}
|
|
154
|
+
|
|
155
|
+
// BAD - linear feels robotic
|
|
156
|
+
Rectangle()
|
|
157
|
+
.animation(.linear(duration: 0.5), value: isActive) // Mechanical
|
|
158
|
+
```
|
|
159
|
+
|
|
160
|
+
---
|
|
161
|
+
|
|
162
|
+
## Animation Performance
|
|
163
|
+
|
|
164
|
+
### Prefer Transforms Over Layout
|
|
165
|
+
|
|
166
|
+
```swift
|
|
167
|
+
// GOOD - GPU accelerated transforms
|
|
168
|
+
Rectangle()
|
|
169
|
+
.frame(width: 100, height: 100)
|
|
170
|
+
.scaleEffect(isActive ? 1.5 : 1.0) // Fast
|
|
171
|
+
.offset(x: isActive ? 50 : 0) // Fast
|
|
172
|
+
.rotationEffect(.degrees(isActive ? 45 : 0)) // Fast
|
|
173
|
+
.animation(.spring, value: isActive)
|
|
174
|
+
|
|
175
|
+
// BAD - layout changes are expensive
|
|
176
|
+
Rectangle()
|
|
177
|
+
.frame(width: isActive ? 150 : 100, height: isActive ? 150 : 100) // Expensive
|
|
178
|
+
.padding(isActive ? 50 : 0) // Expensive
|
|
179
|
+
```
|
|
180
|
+
|
|
181
|
+
### Narrow Animation Scope
|
|
182
|
+
|
|
183
|
+
```swift
|
|
184
|
+
// GOOD - animation scoped to specific subview
|
|
185
|
+
VStack {
|
|
186
|
+
HeaderView() // Not affected
|
|
187
|
+
ExpandableContent(isExpanded: isExpanded)
|
|
188
|
+
.animation(.spring, value: isExpanded) // Only this
|
|
189
|
+
FooterView() // Not affected
|
|
190
|
+
}
|
|
191
|
+
|
|
192
|
+
// BAD - animation at root
|
|
193
|
+
VStack {
|
|
194
|
+
HeaderView()
|
|
195
|
+
ExpandableContent(isExpanded: isExpanded)
|
|
196
|
+
FooterView()
|
|
197
|
+
}
|
|
198
|
+
.animation(.spring, value: isExpanded) // Animates everything
|
|
199
|
+
```
|
|
200
|
+
|
|
201
|
+
### Avoid Animation in Hot Paths
|
|
202
|
+
|
|
203
|
+
```swift
|
|
204
|
+
// GOOD - gate by threshold
|
|
205
|
+
.onPreferenceChange(ScrollOffsetKey.self) { offset in
|
|
206
|
+
let shouldShow = offset.y < -50
|
|
207
|
+
if shouldShow != showTitle { // Only when crossing threshold
|
|
208
|
+
withAnimation(.easeOut(duration: 0.2)) {
|
|
209
|
+
showTitle = shouldShow
|
|
210
|
+
}
|
|
211
|
+
}
|
|
212
|
+
}
|
|
213
|
+
|
|
214
|
+
// BAD - animating every scroll change
|
|
215
|
+
.onPreferenceChange(ScrollOffsetKey.self) { offset in
|
|
216
|
+
withAnimation { // Fires constantly!
|
|
217
|
+
self.offset = offset.y
|
|
218
|
+
}
|
|
219
|
+
}
|
|
220
|
+
```
|
|
221
|
+
|
|
222
|
+
---
|
|
223
|
+
|
|
224
|
+
## Disabling Animations
|
|
225
|
+
|
|
226
|
+
```swift
|
|
227
|
+
// GOOD - disable with transaction
|
|
228
|
+
Text("Count: \(count)")
|
|
229
|
+
.transaction { $0.animation = nil }
|
|
230
|
+
|
|
231
|
+
// GOOD - disable from parent context
|
|
232
|
+
DataView()
|
|
233
|
+
.transaction { $0.disablesAnimations = true }
|
|
234
|
+
|
|
235
|
+
// BAD - hacky zero duration
|
|
236
|
+
Text("Count: \(count)")
|
|
237
|
+
.animation(.linear(duration: 0), value: count) // Hacky
|
|
238
|
+
```
|
|
239
|
+
|
|
240
|
+
---
|
|
241
|
+
|
|
242
|
+
## Debugging
|
|
243
|
+
|
|
244
|
+
```swift
|
|
245
|
+
// Slow down for inspection
|
|
246
|
+
#if DEBUG
|
|
247
|
+
.animation(.linear(duration: 3.0).speed(0.2), value: isExpanded)
|
|
248
|
+
#else
|
|
249
|
+
.animation(.spring, value: isExpanded)
|
|
250
|
+
#endif
|
|
251
|
+
|
|
252
|
+
// Debug modifier to log values
|
|
253
|
+
struct AnimationDebugModifier: ViewModifier, Animatable {
|
|
254
|
+
var value: Double
|
|
255
|
+
var animatableData: Double {
|
|
256
|
+
get { value }
|
|
257
|
+
set {
|
|
258
|
+
value = newValue
|
|
259
|
+
print("Animation: \(newValue)")
|
|
260
|
+
}
|
|
261
|
+
}
|
|
262
|
+
func body(content: Content) -> some View {
|
|
263
|
+
content.opacity(value)
|
|
264
|
+
}
|
|
265
|
+
}
|
|
266
|
+
```
|
|
267
|
+
|
|
268
|
+
---
|
|
269
|
+
|
|
270
|
+
## Quick Reference
|
|
271
|
+
|
|
272
|
+
### Do
|
|
273
|
+
- Use `.animation(_:value:)` with value parameter
|
|
274
|
+
- Use `withAnimation` for event-driven animations
|
|
275
|
+
- Prefer transforms over layout changes
|
|
276
|
+
- Scope animations narrowly
|
|
277
|
+
- Choose appropriate timing curves
|
|
278
|
+
|
|
279
|
+
### Don't
|
|
280
|
+
- Use deprecated `.animation(_:)` without value
|
|
281
|
+
- Animate layout properties in hot paths
|
|
282
|
+
- Apply broad animations at root level
|
|
283
|
+
- Use linear timing for UI (feels robotic)
|
|
284
|
+
- Animate on every frame in scroll handlers
|