opencodekit 0.16.14 → 0.16.17
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/dist/index.js +1 -1
- package/dist/template/.opencode/AGENTS.md +1 -1
- package/dist/template/.opencode/agent/plan.md +77 -161
- package/dist/template/.opencode/command/create.md +75 -307
- package/dist/template/.opencode/command/design.md +53 -589
- package/dist/template/.opencode/command/handoff.md +76 -180
- package/dist/template/.opencode/command/init.md +45 -211
- package/dist/template/.opencode/command/plan.md +62 -514
- package/dist/template/.opencode/command/pr.md +56 -226
- package/dist/template/.opencode/command/research.md +55 -266
- package/dist/template/.opencode/command/resume.md +33 -138
- package/dist/template/.opencode/command/review-codebase.md +54 -202
- package/dist/template/.opencode/command/ship.md +78 -127
- package/dist/template/.opencode/command/start.md +47 -577
- package/dist/template/.opencode/command/status.md +55 -354
- package/dist/template/.opencode/command/ui-review.md +52 -298
- package/dist/template/.opencode/command/verify.md +36 -250
- package/dist/template/.opencode/dcp.jsonc +10 -9
- package/dist/template/.opencode/memory.db-shm +0 -0
- package/dist/template/.opencode/memory.db-wal +0 -0
- package/dist/template/.opencode/plugin/README.md +8 -4
- package/dist/template/.opencode/plugin/swarm-enforcer.ts +182 -27
- package/dist/template/.opencode/skill/augment-context-engine/SKILL.md +112 -0
- package/dist/template/.opencode/skill/augment-context-engine/mcp.json +6 -0
- package/dist/template/.opencode/skill/core-data-expert/SKILL.md +82 -0
- package/dist/template/.opencode/skill/core-data-expert/references/batch-operations.md +543 -0
- package/dist/template/.opencode/skill/core-data-expert/references/cloudkit-integration.md +259 -0
- package/dist/template/.opencode/skill/core-data-expert/references/concurrency.md +522 -0
- package/dist/template/.opencode/skill/core-data-expert/references/fetch-requests.md +643 -0
- package/dist/template/.opencode/skill/core-data-expert/references/glossary.md +233 -0
- package/dist/template/.opencode/skill/core-data-expert/references/migration.md +393 -0
- package/dist/template/.opencode/skill/core-data-expert/references/model-configuration.md +597 -0
- package/dist/template/.opencode/skill/core-data-expert/references/performance.md +300 -0
- package/dist/template/.opencode/skill/core-data-expert/references/persistent-history.md +553 -0
- package/dist/template/.opencode/skill/core-data-expert/references/project-audit.md +60 -0
- package/dist/template/.opencode/skill/core-data-expert/references/saving.md +574 -0
- package/dist/template/.opencode/skill/core-data-expert/references/stack-setup.md +625 -0
- package/dist/template/.opencode/skill/core-data-expert/references/testing.md +300 -0
- package/dist/template/.opencode/skill/core-data-expert/references/threading.md +589 -0
- package/dist/template/.opencode/skill/swift-concurrency/SKILL.md +246 -0
- package/dist/template/.opencode/skill/swift-concurrency/references/actors.md +640 -0
- package/dist/template/.opencode/skill/swift-concurrency/references/async-algorithms.md +822 -0
- package/dist/template/.opencode/skill/swift-concurrency/references/async-await-basics.md +249 -0
- package/dist/template/.opencode/skill/swift-concurrency/references/async-sequences.md +670 -0
- package/dist/template/.opencode/skill/swift-concurrency/references/core-data.md +533 -0
- package/dist/template/.opencode/skill/swift-concurrency/references/glossary.md +128 -0
- package/dist/template/.opencode/skill/swift-concurrency/references/linting.md +142 -0
- package/dist/template/.opencode/skill/swift-concurrency/references/memory-management.md +542 -0
- package/dist/template/.opencode/skill/swift-concurrency/references/migration.md +1076 -0
- package/dist/template/.opencode/skill/swift-concurrency/references/performance.md +574 -0
- package/dist/template/.opencode/skill/swift-concurrency/references/sendable.md +578 -0
- package/dist/template/.opencode/skill/swift-concurrency/references/tasks.md +604 -0
- package/dist/template/.opencode/skill/swift-concurrency/references/testing.md +565 -0
- package/dist/template/.opencode/skill/swift-concurrency/references/threading.md +452 -0
- package/dist/template/.opencode/skill/swiftui-expert-skill/SKILL.md +290 -0
- package/dist/template/.opencode/skill/swiftui-expert-skill/references/animation-advanced.md +351 -0
- package/dist/template/.opencode/skill/swiftui-expert-skill/references/animation-basics.md +284 -0
- package/dist/template/.opencode/skill/swiftui-expert-skill/references/animation-transitions.md +326 -0
- package/dist/template/.opencode/skill/swiftui-expert-skill/references/image-optimization.md +286 -0
- package/dist/template/.opencode/skill/swiftui-expert-skill/references/layout-best-practices.md +312 -0
- package/dist/template/.opencode/skill/swiftui-expert-skill/references/liquid-glass.md +377 -0
- package/dist/template/.opencode/skill/swiftui-expert-skill/references/list-patterns.md +153 -0
- package/dist/template/.opencode/skill/swiftui-expert-skill/references/modern-apis.md +400 -0
- package/dist/template/.opencode/skill/swiftui-expert-skill/references/performance-patterns.md +377 -0
- package/dist/template/.opencode/skill/swiftui-expert-skill/references/scroll-patterns.md +305 -0
- package/dist/template/.opencode/skill/swiftui-expert-skill/references/sheet-navigation-patterns.md +292 -0
- package/dist/template/.opencode/skill/swiftui-expert-skill/references/state-management.md +447 -0
- package/dist/template/.opencode/skill/swiftui-expert-skill/references/text-formatting.md +285 -0
- package/dist/template/.opencode/skill/swiftui-expert-skill/references/view-structure.md +276 -0
- package/package.json +1 -1
|
@@ -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
|
package/dist/template/.opencode/skill/swiftui-expert-skill/references/animation-transitions.md
ADDED
|
@@ -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
|