@mmerterden/multi-agent-pipeline 20.17.1 → 20.17.2
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 +11 -0
- package/docs/facts.json +6 -6
- package/install/catalog-history.json +1 -1
- package/manifest.json +75 -37
- package/package.json +1 -1
- package/pipeline/skills/.skill-manifest.json +33 -13
- package/pipeline/skills/.skills-index.json +66 -11
- package/pipeline/skills/shared/README.md +18 -13
- package/pipeline/skills/shared/external/NOTICE-avdlee-swiftui-agent-skill.md +3 -1
- package/pipeline/skills/shared/external/NOTICE-dimillian-skills.md +2 -1
- package/pipeline/skills/shared/external/hig-app-icons/SKILL.md +159 -0
- package/pipeline/skills/shared/external/hig-app-icons/evals/evals.json +48 -0
- package/pipeline/skills/shared/external/hig-app-icons/evals/trigger-eval.json +22 -0
- package/pipeline/skills/shared/external/hig-app-icons/references/icon-workflow.md +115 -0
- package/pipeline/skills/shared/external/hig-components-layout/SKILL.md +32 -4
- package/pipeline/skills/shared/external/hig-components-layout/evals/evals.json +47 -0
- package/pipeline/skills/shared/external/hig-components-layout/evals/trigger-eval.json +22 -0
- package/pipeline/skills/shared/external/hig-components-layout/references/bars-and-tabs.md +231 -0
- package/pipeline/skills/shared/external/hig-components-layout/references/containers.md +5 -30
- package/pipeline/skills/shared/external/hig-components-status/SKILL.md +1 -1
- package/pipeline/skills/shared/external/hig-foundations/SKILL.md +23 -4
- package/pipeline/skills/shared/external/hig-foundations/evals/evals.json +47 -0
- package/pipeline/skills/shared/external/hig-foundations/evals/trigger-eval.json +21 -0
- package/pipeline/skills/shared/external/hig-foundations/references/foundations-detail.md +34 -26
- package/pipeline/skills/shared/external/hig-foundations/references/type-color-spacing.md +219 -0
- package/pipeline/skills/shared/external/hig-inputs/SKILL.md +5 -4
- package/pipeline/skills/shared/external/hig-patterns/SKILL.md +19 -3
- package/pipeline/skills/shared/external/hig-patterns/references/onboarding-and-states.md +188 -0
- package/pipeline/skills/shared/external/hig-patterns/references/patterns.md +5 -11
- package/pipeline/skills/shared/external/hig-platforms/SKILL.md +17 -5
- package/pipeline/skills/shared/external/hig-platforms/references/ipad-layout.md +218 -0
- package/pipeline/skills/shared/external/ios-accessibility/SKILL.md +4 -1
- package/pipeline/skills/shared/external/ios-accessibility/references/large-content-and-appearance.md +135 -0
- package/pipeline/skills/shared/external/ios-design-review/SKILL.md +146 -0
- package/pipeline/skills/shared/external/ios-design-review/evals/evals.json +48 -0
- package/pipeline/skills/shared/external/ios-design-review/evals/trigger-eval.json +22 -0
- package/pipeline/skills/shared/external/ios-design-review/references/appearance-matrix.md +135 -0
- package/pipeline/skills/shared/external/ios-design-review/references/checklist.md +193 -0
- package/pipeline/skills/shared/external/ios-haptics/SKILL.md +191 -0
- package/pipeline/skills/shared/external/ios-haptics/evals/evals.json +49 -0
- package/pipeline/skills/shared/external/ios-haptics/evals/trigger-eval.json +22 -0
- package/pipeline/skills/shared/external/ios-haptics/references/core-haptics.md +215 -0
- package/pipeline/skills/shared/external/ios-iphone-duo/NOTICE.md +53 -0
- package/pipeline/skills/shared/external/ios-iphone-duo/SKILL.md +170 -0
- package/pipeline/skills/shared/external/ios-iphone-duo/evals/evals.json +50 -0
- package/pipeline/skills/shared/external/ios-iphone-duo/evals/trigger-eval.json +22 -0
- package/pipeline/skills/shared/external/ios-iphone-duo/references/api-map.md +212 -0
- package/pipeline/skills/shared/external/ios-iphone-duo/references/fixed-assumptions.md +145 -0
- package/pipeline/skills/shared/external/ios-iphone-duo/references/poses-and-panes.md +122 -0
- package/pipeline/skills/shared/external/ios-sf-symbols/SKILL.md +177 -0
- package/pipeline/skills/shared/external/ios-sf-symbols/evals/evals.json +49 -0
- package/pipeline/skills/shared/external/ios-sf-symbols/evals/trigger-eval.json +22 -0
- package/pipeline/skills/shared/external/ios-sf-symbols/references/effects-and-custom-symbols.md +135 -0
- package/pipeline/skills/shared/external/swiftui-animation/SKILL.md +38 -50
- package/pipeline/skills/shared/external/swiftui-animation/evals/evals.json +47 -0
- package/pipeline/skills/shared/external/swiftui-animation/evals/trigger-eval.json +22 -0
- package/pipeline/skills/shared/external/swiftui-animation/references/animation-advanced.md +7 -73
- package/pipeline/skills/shared/external/swiftui-animation/references/scroll-effects.md +185 -0
- package/pipeline/skills/shared/external/swiftui-expert-skill/SKILL.md +6 -6
- package/pipeline/skills/shared/external/swiftui-expert-skill/references/animation-advanced.md +3 -253
- package/pipeline/skills/shared/external/swiftui-expert-skill/references/animation-basics.md +4 -47
- package/pipeline/skills/shared/external/swiftui-expert-skill/references/animation-transitions.md +3 -111
- package/pipeline/skills/shared/external/swiftui-expert-skill/references/liquid-glass.md +11 -381
- package/pipeline/skills/shared/external/swiftui-liquid-glass/SKILL.md +184 -224
- package/pipeline/skills/shared/external/swiftui-liquid-glass/evals/evals.json +47 -0
- package/pipeline/skills/shared/external/swiftui-liquid-glass/evals/trigger-eval.json +22 -0
- package/pipeline/skills/shared/external/swiftui-liquid-glass/references/liquid-glass.md +246 -33
- package/pipeline/skills/shared/external/swiftui-liquid-glass/references/uikit-glass.md +227 -0
- package/pipeline/skills/shared/external/swiftui-patterns/SKILL.md +15 -18
- package/pipeline/skills/shared/external/swiftui-ui-patterns/references/components-index.md +1 -1
- package/pipeline/skills/shared/external/swiftui-ui-patterns/references/haptics.md +6 -68
- package/pipeline/skills/skills-index.md +16 -11
- package/pipeline/skills/shared/external/swiftui-patterns/references/design-polish.md +0 -786
|
@@ -0,0 +1,185 @@
|
|
|
1
|
+
# Scroll-driven effects: scrollTransition and visualEffect
|
|
2
|
+
|
|
3
|
+
Companion to `../SKILL.md` for the `swiftui-animation` skill. Both APIs ship
|
|
4
|
+
in iOS 17 (macOS 14, tvOS 17, watchOS 10, visionOS 1).
|
|
5
|
+
|
|
6
|
+
## Contents
|
|
7
|
+
|
|
8
|
+
1. [Which one](#1-which-one)
|
|
9
|
+
2. [scrollTransition](#2-scrolltransition)
|
|
10
|
+
3. [Configurations and thresholds](#3-configurations-and-thresholds)
|
|
11
|
+
4. [visualEffect](#4-visualeffect)
|
|
12
|
+
5. [What a VisualEffect can do](#5-what-a-visualeffect-can-do)
|
|
13
|
+
6. [Concurrency: the closures are Sendable](#6-concurrency-the-closures-are-sendable)
|
|
14
|
+
7. [Reduce Motion](#7-reduce-motion)
|
|
15
|
+
8. [Mistakes](#8-mistakes)
|
|
16
|
+
|
|
17
|
+
## 1. Which one
|
|
18
|
+
|
|
19
|
+
| Need | API |
|
|
20
|
+
|---|---|
|
|
21
|
+
| Items fade, scale or tilt as they enter and leave the visible area of a scroll view | `scrollTransition(_:axis:transition:)` |
|
|
22
|
+
| An effect that is a continuous function of the view's position or size (parallax, a header that shrinks, a stretch at the top) | `visualEffect(_:)` with the `GeometryProxy` |
|
|
23
|
+
| React to scroll position in state (show a button after 300 pt) | `onScrollGeometryChange(for:of:action:)` (iOS 18), not an effect |
|
|
24
|
+
|
|
25
|
+
Both apply effects that do not change layout, so they are cheap to run every
|
|
26
|
+
frame and safe inside lazy stacks.
|
|
27
|
+
|
|
28
|
+
## 2. scrollTransition
|
|
29
|
+
|
|
30
|
+
```swift
|
|
31
|
+
ScrollView {
|
|
32
|
+
LazyVStack(spacing: 16) {
|
|
33
|
+
ForEach(destinations) { destination in
|
|
34
|
+
DestinationCard(destination: destination)
|
|
35
|
+
.scrollTransition { content, phase in
|
|
36
|
+
content
|
|
37
|
+
.opacity(phase.isIdentity ? 1 : 0.4)
|
|
38
|
+
.scaleEffect(phase.isIdentity ? 1 : 0.92)
|
|
39
|
+
}
|
|
40
|
+
}
|
|
41
|
+
}
|
|
42
|
+
.padding(.horizontal)
|
|
43
|
+
}
|
|
44
|
+
```
|
|
45
|
+
|
|
46
|
+
`ScrollTransitionPhase` has three cases:
|
|
47
|
+
|
|
48
|
+
- `.topLeading`: the view is entering or leaving at the top (vertical) or
|
|
49
|
+
leading (horizontal) edge.
|
|
50
|
+
- `.identity`: the view is in the visible region.
|
|
51
|
+
- `.bottomTrailing`: entering or leaving at the bottom or trailing edge.
|
|
52
|
+
|
|
53
|
+
`phase.value` is -1 at `.topLeading`, 0 at `.identity` and 1 at
|
|
54
|
+
`.bottomTrailing`, interpolated in between, so effects can scale with it:
|
|
55
|
+
|
|
56
|
+
```swift
|
|
57
|
+
.scrollTransition(axis: .horizontal) { content, phase in
|
|
58
|
+
content
|
|
59
|
+
.rotation3DEffect(.degrees(phase.value * -25), axis: (x: 0, y: 1, z: 0))
|
|
60
|
+
.offset(x: phase.value * -20)
|
|
61
|
+
}
|
|
62
|
+
```
|
|
63
|
+
|
|
64
|
+
In the `.identity` phase the transition should leave the view unchanged,
|
|
65
|
+
because whatever it applies there stays on for as long as the view is visible.
|
|
66
|
+
|
|
67
|
+
Different behaviour per edge:
|
|
68
|
+
|
|
69
|
+
```swift
|
|
70
|
+
.scrollTransition(topLeading: .interactive, bottomTrailing: .animated(.smooth)) { content, phase in
|
|
71
|
+
content.opacity(phase.isIdentity ? 1 : 0)
|
|
72
|
+
}
|
|
73
|
+
```
|
|
74
|
+
|
|
75
|
+
## 3. Configurations and thresholds
|
|
76
|
+
|
|
77
|
+
| `ScrollTransitionConfiguration` | Behaviour |
|
|
78
|
+
|---|---|
|
|
79
|
+
| `.interactive` (default) | Tracks the scroll position directly |
|
|
80
|
+
| `.interactive(timingCurve:)` | Same, with a `UnitCurve` shaping the progress |
|
|
81
|
+
| `.animated`, `.animated(_:)` | Animates to the new phase when the threshold is crossed |
|
|
82
|
+
| `.identity` | No transition |
|
|
83
|
+
|
|
84
|
+
`.threshold(_:)` decides when a view counts as visible:
|
|
85
|
+
`.visible` (any part visible), `.visible(0.9)` (90% visible), `.centered`,
|
|
86
|
+
`.hidden`, and `.inset(by:)` or `.interpolated(towards:amount:)` to adjust.
|
|
87
|
+
|
|
88
|
+
```swift
|
|
89
|
+
.scrollTransition(.interactive.threshold(.visible(0.9))) { content, phase in
|
|
90
|
+
content.blur(radius: phase.isIdentity ? 0 : 6)
|
|
91
|
+
}
|
|
92
|
+
```
|
|
93
|
+
|
|
94
|
+
## 4. visualEffect
|
|
95
|
+
|
|
96
|
+
`visualEffect(_:)` hands the closure an empty effect and the view's
|
|
97
|
+
`GeometryProxy`. Read position in the scroll view's coordinate space
|
|
98
|
+
(`.scrollView`, iOS 17) and return effects:
|
|
99
|
+
|
|
100
|
+
```swift
|
|
101
|
+
struct ParallaxHeader: View {
|
|
102
|
+
let image: Image
|
|
103
|
+
|
|
104
|
+
var body: some View {
|
|
105
|
+
image
|
|
106
|
+
.resizable()
|
|
107
|
+
.scaledToFill()
|
|
108
|
+
.frame(height: 280)
|
|
109
|
+
.clipped()
|
|
110
|
+
.visualEffect { content, proxy in
|
|
111
|
+
let minY = proxy.frame(in: .scrollView).minY
|
|
112
|
+
let stretch = minY > 0 ? 1 + minY / 600 : 1
|
|
113
|
+
return content
|
|
114
|
+
.offset(y: minY > 0 ? -minY / 2 : 0)
|
|
115
|
+
.scaleEffect(x: stretch, y: stretch, anchor: .bottom)
|
|
116
|
+
}
|
|
117
|
+
}
|
|
118
|
+
}
|
|
119
|
+
```
|
|
120
|
+
|
|
121
|
+
The same pattern fades a title as it reaches the top bar, or dims rows that
|
|
122
|
+
sit under a floating control.
|
|
123
|
+
|
|
124
|
+
## 5. What a VisualEffect can do
|
|
125
|
+
|
|
126
|
+
Effects returned from either closure: `offset`, `scaleEffect`,
|
|
127
|
+
`rotationEffect`, `rotation3DEffect`, `opacity`, `blur(radius:opaque:)`,
|
|
128
|
+
`brightness`, `contrast`, `saturation`, `grayscale`, `hueRotation`,
|
|
129
|
+
`blendMode`, and the shader effects `colorEffect`, `distortionEffect` and
|
|
130
|
+
`layerEffect`. Frames, padding and other layout modifiers are not available,
|
|
131
|
+
by design.
|
|
132
|
+
|
|
133
|
+
## 6. Concurrency: the closures are Sendable
|
|
134
|
+
|
|
135
|
+
Both closures are `@Sendable`. Do not read main-actor state or non-Sendable
|
|
136
|
+
properties inside them; capture plain values first:
|
|
137
|
+
|
|
138
|
+
```swift
|
|
139
|
+
let strength = isEmphasized ? 1.0 : 0.5
|
|
140
|
+
Card()
|
|
141
|
+
.scrollTransition { content, phase in
|
|
142
|
+
content.scaleEffect(1 - abs(phase.value) * 0.1 * strength)
|
|
143
|
+
}
|
|
144
|
+
```
|
|
145
|
+
|
|
146
|
+
## 7. Reduce Motion
|
|
147
|
+
|
|
148
|
+
Scroll-linked motion is still motion. With
|
|
149
|
+
`@Environment(\.accessibilityReduceMotion)` set, keep opacity changes and drop
|
|
150
|
+
movement, rotation and parallax, or pass `.identity`:
|
|
151
|
+
|
|
152
|
+
```swift
|
|
153
|
+
@Environment(\.accessibilityReduceMotion) private var reduceMotion
|
|
154
|
+
|
|
155
|
+
var body: some View {
|
|
156
|
+
let configuration: ScrollTransitionConfiguration = reduceMotion ? .identity : .interactive
|
|
157
|
+
PosterCard(poster: poster)
|
|
158
|
+
.scrollTransition(configuration) { content, phase in
|
|
159
|
+
content
|
|
160
|
+
.opacity(phase.isIdentity ? 1 : 0.5)
|
|
161
|
+
.scaleEffect(phase.isIdentity ? 1 : 0.9)
|
|
162
|
+
}
|
|
163
|
+
}
|
|
164
|
+
```
|
|
165
|
+
|
|
166
|
+
For `visualEffect` parallax, return `content` unchanged when the setting is on;
|
|
167
|
+
capture `reduceMotion` in a local constant before the closure (see section 6).
|
|
168
|
+
|
|
169
|
+
## 8. Mistakes
|
|
170
|
+
|
|
171
|
+
1. **Layout work in the closure.** It runs every frame; compute constants
|
|
172
|
+
outside.
|
|
173
|
+
2. **A visible change in the `.identity` phase.** It stays on permanently.
|
|
174
|
+
3. **Reaching for `GeometryReader` plus `@State` offsets** to build parallax.
|
|
175
|
+
`visualEffect` does it without invalidating the view on every scroll tick.
|
|
176
|
+
4. **Effects that hide content or make text unreadable** at rest; transitions
|
|
177
|
+
belong to the edges.
|
|
178
|
+
5. **Ignoring Reduce Motion** for 3D rotation and parallax.
|
|
179
|
+
|
|
180
|
+
Sources:
|
|
181
|
+
[scrollTransition(_:axis:transition:)](https://developer.apple.com/documentation/swiftui/view/scrolltransition(_:axis:transition:)),
|
|
182
|
+
[ScrollTransitionPhase](https://developer.apple.com/documentation/swiftui/scrolltransitionphase),
|
|
183
|
+
[ScrollTransitionConfiguration](https://developer.apple.com/documentation/swiftui/scrolltransitionconfiguration),
|
|
184
|
+
[visualEffect(_:)](https://developer.apple.com/documentation/swiftui/view/visualeffect(_:)),
|
|
185
|
+
[VisualEffect](https://developer.apple.com/documentation/swiftui/visualeffect).
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
---
|
|
2
2
|
name: swiftui-expert-skill
|
|
3
|
-
description: "Write, review, or improve SwiftUI code following best practices for state
|
|
3
|
+
description: "Write, review, or improve SwiftUI code following best practices for state, view composition, performance, macOS APIs, and iOS 26+ Liquid Glass adoption. Use when building SwiftUI features, refactoring views, reviewing code, or adopting modern patterns; for a findings-only review use swiftui-pro; for Liquid Glass APIs use swiftui-liquid-glass."
|
|
4
4
|
metadata:
|
|
5
5
|
source: "AvdLee/SwiftUI-Agent-Skill (MIT)"
|
|
6
6
|
---
|
|
@@ -14,7 +14,7 @@ metadata:
|
|
|
14
14
|
- Focus on correctness and performance; do not enforce specific architectures (MVVM, VIPER, etc.)
|
|
15
15
|
- Encourage separating business logic from views for testability without mandating how
|
|
16
16
|
- Follow Apple's Human Interface Guidelines and API design patterns
|
|
17
|
-
- Only adopt Liquid Glass when explicitly requested by the user (see `references/liquid-glass.md`)
|
|
17
|
+
- Only adopt Liquid Glass when explicitly requested by the user (see `references/liquid-glass.md`; the API itself is in the `swiftui-liquid-glass` skill)
|
|
18
18
|
- Present performance optimizations as suggestions, not requirements
|
|
19
19
|
- Use `#available` gating with sensible fallbacks for version-specific APIs
|
|
20
20
|
|
|
@@ -60,7 +60,7 @@ Consult the reference file for each topic relevant to the current task:
|
|
|
60
60
|
| Swift Charts | `references/charts.md` |
|
|
61
61
|
| Charts accessibility | `references/charts-accessibility.md` |
|
|
62
62
|
| Image optimization | `references/image-optimization.md` |
|
|
63
|
-
| Liquid Glass (iOS 26+) | `references/liquid-glass.md` |
|
|
63
|
+
| Liquid Glass (iOS 26+) | `references/liquid-glass.md`, then the `swiftui-liquid-glass` skill |
|
|
64
64
|
| macOS scenes | `references/macos-scenes.md` |
|
|
65
65
|
| macOS window styling | `references/macos-window-styling.md` |
|
|
66
66
|
| macOS views | `references/macos-views.md` |
|
|
@@ -91,14 +91,14 @@ These are hard rules -- violations are always bugs:
|
|
|
91
91
|
- `references/layout-best-practices.md` -- Layout patterns, GeometryReader alternatives
|
|
92
92
|
- `references/accessibility-patterns.md` -- VoiceOver, Dynamic Type, grouping, traits
|
|
93
93
|
- `references/animation-basics.md` -- Implicit/explicit animations, timing, performance
|
|
94
|
-
- `references/animation-transitions.md` --
|
|
95
|
-
- `references/animation-advanced.md` --
|
|
94
|
+
- `references/animation-transitions.md` -- Transition animation context, identity, `Animatable` (transition catalog: `swiftui-animation`)
|
|
95
|
+
- `references/animation-advanced.md` -- Transactions, completion handlers (phase, keyframe and `@Animatable`: `swiftui-animation`)
|
|
96
96
|
- `references/charts.md` -- Swift Charts marks, axes, selection, styling, Chart3D (iOS 26+)
|
|
97
97
|
- `references/charts-accessibility.md` -- Charts VoiceOver, Audio Graph, fallback strategies
|
|
98
98
|
- `references/sheet-navigation-patterns.md` -- Sheets, NavigationSplitView, Inspector
|
|
99
99
|
- `references/scroll-patterns.md` -- ScrollViewReader, programmatic scrolling
|
|
100
100
|
- `references/image-optimization.md` -- AsyncImage, downsampling, caching
|
|
101
|
-
- `references/liquid-glass.md` --
|
|
101
|
+
- `references/liquid-glass.md` -- When to adopt Liquid Glass; the API, fallbacks and checklist live in `swiftui-liquid-glass`
|
|
102
102
|
- `references/macos-scenes.md` -- Settings, MenuBarExtra, WindowGroup, multi-window
|
|
103
103
|
- `references/macos-window-styling.md` -- Toolbar styles, window sizing, Commands
|
|
104
104
|
- `references/macos-views.md` -- HSplitView, Table, PasteButton, AppKit interop
|
package/pipeline/skills/shared/external/swiftui-expert-skill/references/animation-advanced.md
CHANGED
|
@@ -1,13 +1,12 @@
|
|
|
1
1
|
# SwiftUI Advanced Animations
|
|
2
2
|
|
|
3
|
-
Transactions
|
|
3
|
+
Transactions and completion handlers (iOS 17+).
|
|
4
|
+
|
|
5
|
+
> Local trim: phase animations, keyframe animations and the `@Animatable` macro are maintained in the `swiftui-animation` skill, which owns SwiftUI motion. This file keeps transactions and completion handlers.
|
|
4
6
|
|
|
5
7
|
## Table of Contents
|
|
6
8
|
- [Transactions](#transactions)
|
|
7
|
-
- [Phase Animations (iOS 17+)](#phase-animations-ios-17)
|
|
8
|
-
- [Keyframe Animations (iOS 17+)](#keyframe-animations-ios-17)
|
|
9
9
|
- [Animation Completion Handlers (iOS 17+)](#animation-completion-handlers-ios-17)
|
|
10
|
-
- [@Animatable Macro (iOS 26+)](#animatable-macro-ios-26)
|
|
11
10
|
|
|
12
11
|
---
|
|
13
12
|
|
|
@@ -94,192 +93,6 @@ withTransaction(transaction) { flag.toggle() }
|
|
|
94
93
|
|
|
95
94
|
---
|
|
96
95
|
|
|
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
96
|
## Animation Completion Handlers (iOS 17+)
|
|
284
97
|
|
|
285
98
|
Execute code when animations finish.
|
|
@@ -323,52 +136,6 @@ Circle()
|
|
|
323
136
|
|
|
324
137
|
---
|
|
325
138
|
|
|
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
139
|
## Quick Reference
|
|
373
140
|
|
|
374
141
|
### Transactions (All iOS versions)
|
|
@@ -380,24 +147,7 @@ struct Wedge: Shape {
|
|
|
380
147
|
### Custom Transaction Keys (iOS 17+)
|
|
381
148
|
- Pass metadata through animation system via `TransactionKey`
|
|
382
149
|
|
|
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
150
|
### Completion Handlers (iOS 17+)
|
|
396
151
|
- Use `withAnimation(.animation) { } completion: { }` for one-shot completion handlers
|
|
397
152
|
- Use `.transaction(value:)` for handlers that should refire on every value change
|
|
398
153
|
- 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
|
|
@@ -1,6 +1,8 @@
|
|
|
1
1
|
# SwiftUI Animation Basics
|
|
2
2
|
|
|
3
|
-
Core animation concepts, implicit vs explicit animations,
|
|
3
|
+
Core animation concepts, implicit vs explicit animations, and performance patterns.
|
|
4
|
+
|
|
5
|
+
> Local trim: Timing curves and springs are maintained in the `swiftui-animation` skill, which owns SwiftUI motion. This file keeps placement, selective animation, performance and debugging.
|
|
4
6
|
|
|
5
7
|
## Table of Contents
|
|
6
8
|
- [Core Concepts](#core-concepts)
|
|
@@ -8,7 +10,6 @@ Core animation concepts, implicit vs explicit animations, timing curves, and per
|
|
|
8
10
|
- [Explicit Animations](#explicit-animations)
|
|
9
11
|
- [Animation Placement](#animation-placement)
|
|
10
12
|
- [Selective Animation](#selective-animation)
|
|
11
|
-
- [Timing Curves](#timing-curves)
|
|
12
13
|
- [Animation Performance](#animation-performance)
|
|
13
14
|
- [Disabling Animations](#disabling-animations)
|
|
14
15
|
- [Debugging](#debugging)
|
|
@@ -115,50 +116,6 @@ Rectangle()
|
|
|
115
116
|
|
|
116
117
|
---
|
|
117
118
|
|
|
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
119
|
## Animation Performance
|
|
163
120
|
|
|
164
121
|
### Prefer Transforms Over Layout
|
|
@@ -274,7 +231,7 @@ struct AnimationDebugModifier: ViewModifier, Animatable {
|
|
|
274
231
|
- Use `withAnimation` for event-driven animations
|
|
275
232
|
- Prefer transforms over layout changes
|
|
276
233
|
- Scope animations narrowly
|
|
277
|
-
- Choose appropriate timing curves
|
|
234
|
+
- Choose appropriate timing curves (see `swiftui-animation`)
|
|
278
235
|
|
|
279
236
|
### Don't
|
|
280
237
|
- Use deprecated `.animation(_:)` without value
|