@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.
Files changed (73) hide show
  1. package/CHANGELOG.md +11 -0
  2. package/docs/facts.json +6 -6
  3. package/install/catalog-history.json +1 -1
  4. package/manifest.json +75 -37
  5. package/package.json +1 -1
  6. package/pipeline/skills/.skill-manifest.json +33 -13
  7. package/pipeline/skills/.skills-index.json +66 -11
  8. package/pipeline/skills/shared/README.md +18 -13
  9. package/pipeline/skills/shared/external/NOTICE-avdlee-swiftui-agent-skill.md +3 -1
  10. package/pipeline/skills/shared/external/NOTICE-dimillian-skills.md +2 -1
  11. package/pipeline/skills/shared/external/hig-app-icons/SKILL.md +159 -0
  12. package/pipeline/skills/shared/external/hig-app-icons/evals/evals.json +48 -0
  13. package/pipeline/skills/shared/external/hig-app-icons/evals/trigger-eval.json +22 -0
  14. package/pipeline/skills/shared/external/hig-app-icons/references/icon-workflow.md +115 -0
  15. package/pipeline/skills/shared/external/hig-components-layout/SKILL.md +32 -4
  16. package/pipeline/skills/shared/external/hig-components-layout/evals/evals.json +47 -0
  17. package/pipeline/skills/shared/external/hig-components-layout/evals/trigger-eval.json +22 -0
  18. package/pipeline/skills/shared/external/hig-components-layout/references/bars-and-tabs.md +231 -0
  19. package/pipeline/skills/shared/external/hig-components-layout/references/containers.md +5 -30
  20. package/pipeline/skills/shared/external/hig-components-status/SKILL.md +1 -1
  21. package/pipeline/skills/shared/external/hig-foundations/SKILL.md +23 -4
  22. package/pipeline/skills/shared/external/hig-foundations/evals/evals.json +47 -0
  23. package/pipeline/skills/shared/external/hig-foundations/evals/trigger-eval.json +21 -0
  24. package/pipeline/skills/shared/external/hig-foundations/references/foundations-detail.md +34 -26
  25. package/pipeline/skills/shared/external/hig-foundations/references/type-color-spacing.md +219 -0
  26. package/pipeline/skills/shared/external/hig-inputs/SKILL.md +5 -4
  27. package/pipeline/skills/shared/external/hig-patterns/SKILL.md +19 -3
  28. package/pipeline/skills/shared/external/hig-patterns/references/onboarding-and-states.md +188 -0
  29. package/pipeline/skills/shared/external/hig-patterns/references/patterns.md +5 -11
  30. package/pipeline/skills/shared/external/hig-platforms/SKILL.md +17 -5
  31. package/pipeline/skills/shared/external/hig-platforms/references/ipad-layout.md +218 -0
  32. package/pipeline/skills/shared/external/ios-accessibility/SKILL.md +4 -1
  33. package/pipeline/skills/shared/external/ios-accessibility/references/large-content-and-appearance.md +135 -0
  34. package/pipeline/skills/shared/external/ios-design-review/SKILL.md +146 -0
  35. package/pipeline/skills/shared/external/ios-design-review/evals/evals.json +48 -0
  36. package/pipeline/skills/shared/external/ios-design-review/evals/trigger-eval.json +22 -0
  37. package/pipeline/skills/shared/external/ios-design-review/references/appearance-matrix.md +135 -0
  38. package/pipeline/skills/shared/external/ios-design-review/references/checklist.md +193 -0
  39. package/pipeline/skills/shared/external/ios-haptics/SKILL.md +191 -0
  40. package/pipeline/skills/shared/external/ios-haptics/evals/evals.json +49 -0
  41. package/pipeline/skills/shared/external/ios-haptics/evals/trigger-eval.json +22 -0
  42. package/pipeline/skills/shared/external/ios-haptics/references/core-haptics.md +215 -0
  43. package/pipeline/skills/shared/external/ios-iphone-duo/NOTICE.md +53 -0
  44. package/pipeline/skills/shared/external/ios-iphone-duo/SKILL.md +170 -0
  45. package/pipeline/skills/shared/external/ios-iphone-duo/evals/evals.json +50 -0
  46. package/pipeline/skills/shared/external/ios-iphone-duo/evals/trigger-eval.json +22 -0
  47. package/pipeline/skills/shared/external/ios-iphone-duo/references/api-map.md +212 -0
  48. package/pipeline/skills/shared/external/ios-iphone-duo/references/fixed-assumptions.md +145 -0
  49. package/pipeline/skills/shared/external/ios-iphone-duo/references/poses-and-panes.md +122 -0
  50. package/pipeline/skills/shared/external/ios-sf-symbols/SKILL.md +177 -0
  51. package/pipeline/skills/shared/external/ios-sf-symbols/evals/evals.json +49 -0
  52. package/pipeline/skills/shared/external/ios-sf-symbols/evals/trigger-eval.json +22 -0
  53. package/pipeline/skills/shared/external/ios-sf-symbols/references/effects-and-custom-symbols.md +135 -0
  54. package/pipeline/skills/shared/external/swiftui-animation/SKILL.md +38 -50
  55. package/pipeline/skills/shared/external/swiftui-animation/evals/evals.json +47 -0
  56. package/pipeline/skills/shared/external/swiftui-animation/evals/trigger-eval.json +22 -0
  57. package/pipeline/skills/shared/external/swiftui-animation/references/animation-advanced.md +7 -73
  58. package/pipeline/skills/shared/external/swiftui-animation/references/scroll-effects.md +185 -0
  59. package/pipeline/skills/shared/external/swiftui-expert-skill/SKILL.md +6 -6
  60. package/pipeline/skills/shared/external/swiftui-expert-skill/references/animation-advanced.md +3 -253
  61. package/pipeline/skills/shared/external/swiftui-expert-skill/references/animation-basics.md +4 -47
  62. package/pipeline/skills/shared/external/swiftui-expert-skill/references/animation-transitions.md +3 -111
  63. package/pipeline/skills/shared/external/swiftui-expert-skill/references/liquid-glass.md +11 -381
  64. package/pipeline/skills/shared/external/swiftui-liquid-glass/SKILL.md +184 -224
  65. package/pipeline/skills/shared/external/swiftui-liquid-glass/evals/evals.json +47 -0
  66. package/pipeline/skills/shared/external/swiftui-liquid-glass/evals/trigger-eval.json +22 -0
  67. package/pipeline/skills/shared/external/swiftui-liquid-glass/references/liquid-glass.md +246 -33
  68. package/pipeline/skills/shared/external/swiftui-liquid-glass/references/uikit-glass.md +227 -0
  69. package/pipeline/skills/shared/external/swiftui-patterns/SKILL.md +15 -18
  70. package/pipeline/skills/shared/external/swiftui-ui-patterns/references/components-index.md +1 -1
  71. package/pipeline/skills/shared/external/swiftui-ui-patterns/references/haptics.md +6 -68
  72. package/pipeline/skills/skills-index.md +16 -11
  73. package/pipeline/skills/shared/external/swiftui-patterns/references/design-polish.md +0 -786
@@ -1,786 +0,0 @@
1
- # Design Polish
2
-
3
- Human Interface Guidelines applied in SwiftUI (layout, type, color, navigation
4
- chrome, feedback, accessibility, empty states), plus theming, haptics, matched
5
- transitions, loading placeholders and form focus.
6
-
7
- ## Contents
8
-
9
- 1. [HIG: Layout and Spacing](#1-hig-layout-and-spacing)
10
- 2. [HIG: Typography](#2-hig-typography)
11
- 3. [HIG: Color](#3-hig-color)
12
- 4. [HIG: Navigation Chrome](#4-hig-navigation-chrome)
13
- 5. [HIG: Feedback](#5-hig-feedback)
14
- 6. [HIG: Accessibility](#6-hig-accessibility)
15
- 7. [HIG: Error and Empty States](#7-hig-error-and-empty-states)
16
- 8. [Theming and Dynamic Type](#8-theming-and-dynamic-type)
17
- 9. [Haptics](#9-haptics)
18
- 10. [Core Haptics](#10-core-haptics)
19
- 11. [Matched Transitions](#11-matched-transitions)
20
- 12. [Loading and Placeholders](#12-loading-and-placeholders)
21
- 13. [Form Focus](#13-form-focus)
22
-
23
- ## 1. HIG: Layout and Spacing
24
-
25
- Start by leaving `spacing:` off stacks; the adaptive default is usually right.
26
- When you do choose a value on purpose, take it from a 4 point grid. That grid is
27
- a widespread design convention, not something Apple requires, and values
28
- between its stops should not be invented.
29
-
30
- | Points | Token | Typical use |
31
- |---|---|---|
32
- | 4 | `xxSmall` | Icon to label, inline badge offset |
33
- | 8 | `xSmall` | Related items in one group, tight stack gaps |
34
- | 12 | `small` | Row inner padding, label to secondary text |
35
- | 16 | `medium` | Standard margin, default gap between sections |
36
- | 20 | `mediumLarge` | Room between separate controls |
37
- | 24 | `large` | Section separators, card inner padding |
38
- | 32 | `xLarge` | Major groups, header to content |
39
- | 40 | `xxLarge` | Large section breaks |
40
- | 48 | `xxxLarge` | Hero, splash and onboarding layouts |
41
-
42
- ```swift
43
- enum Spacing {
44
- static let xxSmall: CGFloat = 4
45
- static let xSmall: CGFloat = 8
46
- static let small: CGFloat = 12
47
- static let medium: CGFloat = 16
48
- static let mediumLarge: CGFloat = 20
49
- static let large: CGFloat = 24
50
- static let xLarge: CGFloat = 32
51
- static let xxLarge: CGFloat = 40
52
- static let xxxLarge: CGFloat = 48
53
- }
54
- ```
55
-
56
- Margins shared across screens can be named insets:
57
-
58
- ```swift
59
- private enum Margin {
60
- static let regular: CGFloat = 16
61
- static let tight: CGFloat = 8
62
- static let wide: CGFloat = 24
63
- }
64
-
65
- extension EdgeInsets {
66
- static let regular = EdgeInsets(top: Margin.regular, leading: Margin.regular,
67
- bottom: Margin.regular, trailing: Margin.regular)
68
- static let rowContent = EdgeInsets(top: 12, leading: Margin.regular,
69
- bottom: 12, trailing: Margin.regular)
70
- }
71
- ```
72
-
73
- Respect the safe area and pin persistent actions with `safeAreaInset`:
74
-
75
- ```swift
76
- ScrollView {
77
- LazyVStack { ForEach(lines) { OrderLineRow(line: $0) } }
78
- .padding(.horizontal)
79
- }
80
- .safeAreaInset(edge: .bottom) {
81
- HStack {
82
- Button("Back", action: goBack).buttonStyle(.bordered)
83
- Button("Place order", action: placeOrder).buttonStyle(.borderedProminent)
84
- }
85
- .padding()
86
- .background(.regularMaterial)
87
- }
88
- ```
89
-
90
- Adapt compact and regular widths with the size class:
91
-
92
- ```swift
93
- @Environment(\.horizontalSizeClass) private var sizeClass
94
-
95
- private var gridColumns: [GridItem] {
96
- let count = sizeClass == .regular ? 3 : 1
97
- return Array(repeating: GridItem(.flexible()), count: count)
98
- }
99
- ```
100
-
101
- ## 2. HIG: Typography
102
-
103
- Text styles scale with Dynamic Type without extra work. Sizes below are the
104
- iOS defaults at the Large content size.
105
-
106
- | Style | Size and weight | Use |
107
- |---|---|---|
108
- | `.largeTitle` | 34 pt regular | Screen title |
109
- | `.title` | 28 pt regular | Section header |
110
- | `.title2` | 22 pt regular | Sub-section header |
111
- | `.title3` | 20 pt regular | Group header |
112
- | `.headline` | 17 pt semibold | Row title |
113
- | `.body` | 17 pt regular | Main content |
114
- | `.callout` | 16 pt regular | Secondary content |
115
- | `.subheadline` | 15 pt regular | Supporting text |
116
- | `.footnote` | 13 pt regular | Tertiary information |
117
- | `.caption` | 12 pt regular | Labels |
118
- | `.caption2` | 11 pt regular | Small labels |
119
-
120
- A custom typeface keeps Dynamic Type when it is tied to a text style:
121
-
122
- ```swift
123
- extension Font {
124
- static func brandBody(_ family: String) -> Font {
125
- .custom(family, size: 17, relativeTo: .body)
126
- }
127
- }
128
- ```
129
-
130
- ## 3. HIG: Color
131
-
132
- Semantic colors switch between light and dark appearance on their own.
133
-
134
- | Role | Colors |
135
- |---|---|
136
- | Labels | `Color.primary`, `Color.secondary`, `Color(uiColor: .tertiaryLabel)` |
137
- | Backgrounds | `Color(uiColor: .systemBackground)`, `.secondarySystemBackground`, `.systemGroupedBackground` |
138
- | Fill and divider | `.systemFill` and `.separator`, both wrapped in `Color(uiColor:)` |
139
-
140
- - Set the app tint once on the root view, for example `.tint(.indigo)`.
141
- - Interactive elements use the tint through `.tint(...)` or
142
- `.foregroundStyle(.tint)`.
143
- - Destructive actions use red (`Color.red`, or a `.destructive` button role).
144
-
145
- ## 4. HIG: Navigation Chrome
146
-
147
- Multi-column hierarchies on iPad and Mac use `NavigationSplitView`:
148
-
149
- ```swift
150
- NavigationSplitView {
151
- List(folders, selection: $selectedFolder) { folder in
152
- NavigationLink(folder.name, value: folder)
153
- }
154
- .navigationTitle("Folders")
155
- } detail: {
156
- if let selectedFolder {
157
- FolderDetail(folder: selectedFolder)
158
- } else {
159
- ContentUnavailableView("Pick a folder", systemImage: "sidebar.leading")
160
- }
161
- }
162
- ```
163
-
164
- Tab-based apps give each tab its own `NavigationStack` inside a `TabView`. The
165
- full tab patterns are in `swiftui-navigation`.
166
-
167
- Toolbars (`.topBarLeading` and `.topBarTrailing` come with the iOS 17 SDK and
168
- are back-deployed to iOS 14):
169
-
170
- ```swift
171
- .toolbar {
172
- ToolbarItem(placement: .topBarLeading) {
173
- EditButton()
174
- }
175
- ToolbarItemGroup(placement: .topBarTrailing) {
176
- Button("Mark all read", systemImage: "envelope.open", action: markAllRead)
177
- Button("New", systemImage: "plus", action: createItem)
178
- }
179
- ToolbarItemGroup(placement: .bottomBar) {
180
- Button("Archive", systemImage: "archivebox", action: archiveSelection)
181
- Spacer()
182
- Text("\(messages.count) messages")
183
- .font(.footnote)
184
- .foregroundStyle(.secondary)
185
- Spacer()
186
- Button("Compose", systemImage: "square.and.pencil", action: compose)
187
- }
188
- }
189
- ```
190
-
191
- Search with scopes (`.searchScopes` needs iOS 16.4):
192
-
193
- ```swift
194
- enum LibraryScope: String, CaseIterable {
195
- case titles, authors, tags
196
- var label: String { rawValue.localizedCapitalized }
197
- }
198
-
199
- .searchable(text: $query, placement: .navigationBarDrawer(displayMode: .always))
200
- .searchScopes($scope) {
201
- ForEach(LibraryScope.allCases, id: \.self) { scope in
202
- Text(scope.label).tag(scope)
203
- }
204
- }
205
- ```
206
-
207
- ## 5. HIG: Feedback
208
-
209
- For haptics that follow state, use `sensoryFeedback(_:trigger:)` (iOS 17+):
210
-
211
- ```swift
212
- Button("Bookmark") { isBookmarked.toggle() }
213
- .sensoryFeedback(.success, trigger: isBookmarked)
214
-
215
- Picker("Order", selection: $ordering) {
216
- ForEach(Ordering.allCases) { Text($0.title).tag($0) }
217
- }
218
- .sensoryFeedback(.selection, trigger: ordering)
219
- ```
220
-
221
- Reach for UIKit feedback generators only when the trigger is imperative and
222
- comes from UIKit or other code outside SwiftUI views.
223
-
224
- ## 6. HIG: Accessibility
225
-
226
- Combine a row into one VoiceOver element and describe it:
227
-
228
- ```swift
229
- VStack(alignment: .leading) {
230
- Text(course.title).font(.headline)
231
- Text("\(course.score, specifier: "%.1f") out of 5")
232
- }
233
- .accessibilityElement(children: .combine)
234
- .accessibilityLabel(course.title)
235
- .accessibilityValue("Rated \(course.score, specifier: "%.1f") out of 5")
236
- .accessibilityHint("Opens the course overview")
237
- .accessibilityAddTraits(.isButton)
238
- ```
239
-
240
- Switch layout for the accessibility text sizes:
241
-
242
- ```swift
243
- @Environment(\.dynamicTypeSize) private var typeSize
244
-
245
- var body: some View {
246
- if typeSize.isAccessibilitySize {
247
- VStack(alignment: .leading) { avatar; details }
248
- } else {
249
- HStack { avatar; details; Spacer() }
250
- }
251
- }
252
- ```
253
-
254
- ## 7. HIG: Error and Empty States
255
-
256
- `ContentUnavailableView` covers both. The builder form adds actions:
257
-
258
- ```swift
259
- ContentUnavailableView {
260
- Label("Sync failed", systemImage: "icloud.slash")
261
- } description: {
262
- Text(error.localizedDescription)
263
- } actions: {
264
- Button("Retry") { Task { await resync() } }
265
- .buttonStyle(.borderedProminent)
266
- }
267
-
268
- ContentUnavailableView {
269
- Label("No receipts", systemImage: "doc.text.viewfinder")
270
- } description: {
271
- Text("Scan a paper receipt to add it here.")
272
- } actions: {
273
- Button("Scan") { isScannerPresented = true }
274
- .buttonStyle(.borderedProminent)
275
- }
276
- ```
277
-
278
- ## 8. Theming and Dynamic Type
279
-
280
- Goal: theming that grows with the app while view code stays semantic.
281
-
282
- - One `Theme` object is the only source of colors, fonts and spacing.
283
- - Install it at the app root; views read it with `@Environment(Theme.self)`.
284
- - Name values by role (`primaryBackground`, `secondaryBackground`, `label`,
285
- `tint`) rather than by hue.
286
- - Put user-facing theme choices on a dedicated settings screen.
287
- - Scaling with the user's text size comes from three places: built-in text
288
- styles, a custom font tied to a style via `Font.custom(_:size:relativeTo:)`,
289
- and `@ScaledMetric` for numeric layout values.
290
-
291
- ```swift
292
- @MainActor @Observable
293
- final class Theme {
294
- var accent: Color = .teal
295
- var primaryBackground: Color = .white
296
- var secondaryBackground: Color = .secondary.opacity(0.12)
297
- var label: Color = .primary
298
- var textScale: Double = 1.0
299
- }
300
-
301
- @main
302
- struct JournalApp: App {
303
- @State private var theme = Theme()
304
-
305
- var body: some Scene {
306
- WindowGroup {
307
- EntriesView().environment(theme)
308
- }
309
- }
310
- }
311
-
312
- struct EntryCard: View {
313
- @Environment(Theme.self) private var currentTheme
314
- let entry: Entry
315
-
316
- var body: some View {
317
- Text(entry.title)
318
- .foregroundStyle(currentTheme.label)
319
- .background(currentTheme.primaryBackground)
320
- }
321
- }
322
- ```
323
-
324
- Guidelines:
325
-
326
- - Keep the set of theme values small and semantic; do not re-create system
327
- colors.
328
- - Persist theme choices the user makes, if the app offers them.
329
- - Check contrast between every text color and its background.
330
-
331
- Pitfalls:
332
-
333
- - Raw `Color` literals spread through views.
334
- - A theme stored in one view's local state.
335
- - Treating `@Environment(\.colorScheme)` as the whole theming story. Light or
336
- dark mode is one input to the theme, not a substitute for it.
337
-
338
- ## 9. Haptics
339
-
340
- Use haptics for moments that matter (tab switch, refresh, success, failure) and
341
- respect the user's settings.
342
-
343
- - In SwiftUI views, prefer `sensoryFeedback(_:trigger:)`.
344
- - Centralise imperative feedback in a manager only for UIKit interop or code
345
- that is not a view.
346
- - Check both the user preference and hardware support.
347
- - Use different feedback types for different moments: selection, notification,
348
- refresh.
349
- - Core Haptics is the step up when a pattern needs shaping that the built-in
350
- feedback types do not offer.
351
-
352
- SwiftUI first:
353
-
354
- ```swift
355
- @State private var uploadCount = 0
356
-
357
- Button("Upload") {
358
- uploadCount += 1
359
- startUpload()
360
- }
361
- .sensoryFeedback(.success, trigger: uploadCount)
362
- ```
363
-
364
- UIKit interop:
365
-
366
- ```swift
367
- import UIKit
368
-
369
- @MainActor
370
- final class FeedbackPlayer {
371
- static let shared = FeedbackPlayer()
372
-
373
- enum Kind {
374
- case tap
375
- case tabChange
376
- case refresh(strength: CGFloat)
377
- case result(UINotificationFeedbackGenerator.FeedbackType)
378
- }
379
-
380
- private let selection = UISelectionFeedbackGenerator()
381
- private let impact = UIImpactFeedbackGenerator(style: .heavy)
382
- private let notification = UINotificationFeedbackGenerator()
383
-
384
- private init() { selection.prepare() }
385
-
386
- func play(_ kind: Kind, allowed: Bool) {
387
- guard allowed else { return }
388
- switch kind {
389
- case .tap: impact.impactOccurred()
390
- case .tabChange: selection.selectionChanged()
391
- case .refresh(let strength): impact.impactOccurred(intensity: strength)
392
- case .result(let type): notification.notificationOccurred(type)
393
- }
394
- }
395
- }
396
- ```
397
-
398
- ```swift
399
- @AppStorage("hapticsOn") private var hapticsOn = true
400
-
401
- Button("Pay") {
402
- FeedbackPlayer.shared.play(.result(.success), allowed: hapticsOn)
403
- }
404
-
405
- TabView(selection: $tab) { tabs }
406
- .onChange(of: tab) {
407
- FeedbackPlayer.shared.play(.tabChange, allowed: hapticsOn)
408
- }
409
- ```
410
-
411
- Guidelines:
412
-
413
- - Keep feedback subtle and skip it for minor interactions.
414
- - Offer a setting that turns haptics off.
415
- - Trigger feedback where the user acts, not from the data layer.
416
-
417
- Pitfalls:
418
-
419
- - Several haptics fired back to back.
420
- - Assuming the device has a haptic engine.
421
-
422
- ## 10. Core Haptics
423
-
424
- Core Haptics controls intensity, sharpness, timing and audio sync precisely,
425
- for patterns the simple generators cannot produce. Apple docs:
426
- [CHHapticEngine](https://developer.apple.com/documentation/corehaptics/chhapticengine),
427
- and the [Core Haptics](https://developer.apple.com/documentation/corehaptics)
428
- topic page, whose "Essentials" group holds the article on preparing an app to
429
- play haptics.
430
-
431
- ### Engine lifecycle
432
-
433
- Check the hardware before creating an engine.
434
-
435
- ```swift
436
- import CoreHaptics
437
- import OSLog
438
-
439
- extension Logger {
440
- static let haptics = Logger(subsystem: "app.haptics", category: "engine")
441
- }
442
-
443
- @MainActor
444
- final class PatternPlayer {
445
- private var engine: CHHapticEngine?
446
-
447
- var canPlayHaptics: Bool {
448
- CHHapticEngine.capabilitiesForHardware().supportsHaptics
449
- }
450
-
451
- var canPlayHapticAudio: Bool {
452
- CHHapticEngine.capabilitiesForHardware().supportsAudio
453
- }
454
-
455
- func startEngine() throws {
456
- guard canPlayHaptics else { return }
457
- let engine = try CHHapticEngine()
458
- engine.stoppedHandler = { @Sendable reason in
459
- Logger.haptics.info("Engine stopped, reason \(reason.rawValue)")
460
- }
461
- engine.resetHandler = { @Sendable [weak self] in
462
- Task { @MainActor in try? self?.engine?.start() }
463
- }
464
- try engine.start()
465
- self.engine = engine
466
- }
467
-
468
- func stopEngine() {
469
- engine?.stop()
470
- }
471
- }
472
- ```
473
-
474
- - `stoppedHandler` runs when something outside the app stops the engine, such
475
- as an audio session interruption or the app moving to the background.
476
- - Both handlers are called on a queue owned by the engine, so they are
477
- `@Sendable`; any state change is scheduled onto the main actor first.
478
- - `resetHandler` runs after the system resets the engine, for instance once an
479
- audio interruption is over. Start the engine again there.
480
- - Call `start()` before playing any pattern.
481
- - Once the screen no longer plays patterns, `stop()` the engine to save power.
482
-
483
- ### A single tap
484
-
485
- ```swift
486
- func playTap() throws {
487
- let crisp = CHHapticEventParameter(parameterID: .hapticSharpness, value: 0.75)
488
- let strong = CHHapticEventParameter(parameterID: .hapticIntensity, value: 0.95)
489
- let tap = CHHapticEvent(eventType: .hapticTransient,
490
- parameters: [crisp, strong],
491
- relativeTime: 0)
492
- let pattern = try CHHapticPattern(events: [tap], parameters: [])
493
- try engine?.makePlayer(with: pattern).start(atTime: CHHapticTimeImmediate)
494
- }
495
- ```
496
-
497
- ### Event types and parameters
498
-
499
- | Event type | Effect |
500
- |---|---|
501
- | `.hapticTransient` | Short impulse, like a tap |
502
- | `.hapticContinuous` | Sustained vibration for a `duration` |
503
- | `.audioContinuous` | Sustained audio tone |
504
- | `.audioCustom` | Plays an audio resource you register |
505
-
506
- Common parameters: `.hapticIntensity` (0 to 1), `.hapticSharpness` (0 to 1),
507
- `.attackTime`, `.decayTime`, `.releaseTime`.
508
-
509
- ```swift
510
- func playHum() throws {
511
- let hum = CHHapticEvent(
512
- eventType: .hapticContinuous,
513
- parameters: [
514
- CHHapticEventParameter(parameterID: .hapticIntensity, value: 0.55),
515
- CHHapticEventParameter(parameterID: .hapticSharpness, value: 0.35)
516
- ],
517
- relativeTime: 0,
518
- duration: 0.5
519
- )
520
- let humPattern = try CHHapticPattern(events: [hum], parameters: [])
521
- try engine?.makePlayer(with: humPattern).start(atTime: CHHapticTimeImmediate)
522
- }
523
- ```
524
-
525
- Looping, seeking and pausing need a `CHHapticAdvancedPatternPlayer` from
526
- `engine.makeAdvancedPlayer(with:)`.
527
-
528
- ### Parameter curves
529
-
530
- A `CHHapticParameterCurve` changes a parameter smoothly while the pattern
531
- plays:
532
-
533
- ```swift
534
- func playSwell() throws {
535
- let base = CHHapticEvent(
536
- eventType: .hapticContinuous,
537
- parameters: [
538
- CHHapticEventParameter(parameterID: .hapticIntensity, value: 0.25),
539
- CHHapticEventParameter(parameterID: .hapticSharpness, value: 0.15)
540
- ],
541
- relativeTime: 0,
542
- duration: 1.0
543
- )
544
- let swell = CHHapticParameterCurve(
545
- parameterID: .hapticIntensityControl,
546
- controlPoints: [
547
- CHHapticParameterCurve.ControlPoint(relativeTime: 0, value: 0.25),
548
- CHHapticParameterCurve.ControlPoint(relativeTime: 0.4, value: 0.6),
549
- CHHapticParameterCurve.ControlPoint(relativeTime: 0.9, value: 1)
550
- ],
551
- relativeTime: 0
552
- )
553
- let pattern = try CHHapticPattern(events: [base], parameterCurves: [swell])
554
- try engine?.makePlayer(with: pattern).start(atTime: CHHapticTimeImmediate)
555
- }
556
- ```
557
-
558
- ### AHAP files
559
-
560
- Patterns can also live outside code, in an AHAP file: a JSON document you
561
- tune without recompiling.
562
-
563
- ```swift
564
- func playFromFile() throws {
565
- guard let patternURL = Bundle.main.url(forResource: "checkout", withExtension: "ahap") else {
566
- return
567
- }
568
- try engine?.playPattern(from: patternURL)
569
- }
570
- ```
571
-
572
- Everything the code API expresses (events, parameters, curves) can be written
573
- in AHAP too, and Xcode can play the file back for you while you iterate. File
574
- format reference: the AHAP article listed on the
575
- [Core Haptics](https://developer.apple.com/documentation/corehaptics) topic page.
576
-
577
- ## 11. Matched Transitions
578
-
579
- Goal: carry the eye from a source view (thumbnail, avatar) to its destination
580
- (sheet, detail, full-screen viewer).
581
-
582
- - Share a `Namespace` between source and destination, and give the source a
583
- stable id.
584
- - Across screens, use `matchedTransitionSource(id:in:)` with
585
- `navigationTransition(.zoom(sourceID:in:))`. Both need iOS 18.
586
- - Inside one hierarchy, use `matchedGeometryEffect(id:in:)`.
587
- - Ids must stay the same across updates; never generate random UUIDs for them.
588
-
589
- ```swift
590
- struct GalleryGrid: View {
591
- @Namespace private var zoom
592
- @State private var opened: Photo?
593
- let photos: [Photo]
594
-
595
- var body: some View {
596
- LazyVGrid(columns: [GridItem(.adaptive(minimum: 96))]) {
597
- ForEach(photos) { photo in
598
- Button { opened = photo } label: {
599
- PhotoThumbnail(photo: photo)
600
- }
601
- .matchedTransitionSource(id: photo.id, in: zoom)
602
- }
603
- }
604
- .sheet(item: $opened) { photo in
605
- PhotoViewer(photo: photo)
606
- .navigationTransition(.zoom(sourceID: photo.id, in: zoom))
607
- }
608
- }
609
- }
610
- ```
611
-
612
- In-place change within one view:
613
-
614
- ```swift
615
- struct LockToggle: View {
616
- @Namespace private var space
617
- @State private var isLocked = false
618
-
619
- var body: some View {
620
- Button {
621
- withAnimation(.spring) { isLocked.toggle() }
622
- } label: {
623
- Image(systemName: isLocked ? "lock" : "lock.open")
624
- .matchedGeometryEffect(id: "lockGlyph", in: space)
625
- }
626
- }
627
- }
628
- ```
629
-
630
- Guidelines:
631
-
632
- - Prefer `matchedTransitionSource` for transitions between screens.
633
- - Keep source and destination sizes close so the scale change is not jarring.
634
- - Drive state-based transitions with `withAnimation`.
635
-
636
- Pitfalls:
637
-
638
- - Ids that change between updates break the transition.
639
- - Shapes that do not match (a square into a circle) look wrong unless the
640
- design asks for it.
641
-
642
- ## 12. Loading and Placeholders
643
-
644
- Use this for loading states (skeletons, redaction, empty state) that stay
645
- consistent and never block interaction.
646
-
647
- - For lists and detail content, prefer redacted placeholders; the layout stays
648
- in place while data arrives.
649
- - Once loading ends with nothing to show, or with an error, show
650
- `ContentUnavailableView`.
651
- - Keep `ProgressView` for short, global operations, and use it sparingly on
652
- content-heavy screens.
653
-
654
- Steps:
655
-
656
- 1. Render the real layout with placeholder data and apply
657
- `.redacted(reason: .placeholder)`.
658
- 2. A list gets a small, fixed set of skeleton rows, never a spinner that runs
659
- with no end in sight.
660
- 3. When loading finishes with no data, switch to `ContentUnavailableView`.
661
-
662
- ```swift
663
- List {
664
- if isFetching {
665
- ForEach(0..<4, id: \.self) { _ in
666
- InvoiceRow(invoice: .sample)
667
- .redacted(reason: .placeholder)
668
- }
669
- } else if invoices.isEmpty {
670
- ContentUnavailableView("No invoices", systemImage: "tray")
671
- } else {
672
- ForEach(invoices) { InvoiceRow(invoice: $0) }
673
- }
674
- }
675
- ```
676
-
677
- Pitfalls and limits:
678
-
679
- - Layout that shifts or animates while redacted. Keep frames stable.
680
- - Spinners nested inside spinners. One indicator per section.
681
- - More than three to six placeholder rows, which costs frames on older
682
- devices.
683
-
684
- ## 13. Form Focus
685
-
686
- This section covers keyboard focus in forms. Directional focus, focus sections
687
- and focus on tvOS, macOS and iPadOS keyboards are covered in `hig-inputs`;
688
- VoiceOver and other assistive focus is covered in `ios-accessibility`.
689
-
690
- `@FocusState` sets keyboard focus, moves it from field to field, and
691
- coordinates it across a complex form.
692
-
693
- - Describe the focusable fields with an enum.
694
- - Set the first focus in `onAppear`.
695
- - Move to the following field in `.onSubmit`.
696
- - For a variable number of fields, use an enum case with an associated value,
697
- such as `.answer(Int)`.
698
-
699
- One field:
700
-
701
- ```swift
702
- struct HostEntryForm: View {
703
- @State private var host = ""
704
- @FocusState private var hostFocused: Bool
705
-
706
- var body: some View {
707
- Form {
708
- TextField("Host", text: $host)
709
- .focused($hostFocused)
710
- }
711
- .onAppear { hostFocused = true }
712
- }
713
- }
714
- ```
715
-
716
- Chained fields:
717
-
718
- ```swift
719
- struct ContactForm: View {
720
- enum Field { case name, phone, note }
721
-
722
- @State private var name = ""
723
- @State private var phone = ""
724
- @State private var note = ""
725
- @FocusState private var focus: Field?
726
-
727
- var body: some View {
728
- Form {
729
- TextField("Name", text: $name)
730
- .focused($focus, equals: .name)
731
- .onSubmit { focus = .phone }
732
- TextField("Phone", text: $phone)
733
- .focused($focus, equals: .phone)
734
- .onSubmit { focus = .note }
735
- TextField("Note", text: $note)
736
- .focused($focus, equals: .note)
737
- }
738
- .onAppear { focus = .name }
739
- }
740
- }
741
- ```
742
-
743
- A growing list of fields:
744
-
745
- ```swift
746
- struct SurveyAnswers: View {
747
- enum Field: Hashable { case answer(Int) }
748
-
749
- @State private var answers = [""]
750
- @FocusState private var focus: Field?
751
-
752
- var body: some View {
753
- Form {
754
- ForEach(answers.indices, id: \.self) { index in
755
- TextField("Answer \(index + 1)", text: $answers[index])
756
- .focused($focus, equals: .answer(index))
757
- .onSubmit { addAnswer(after: index) }
758
- }
759
- }
760
- }
761
-
762
- private func addAnswer(after index: Int) {
763
- answers.append("")
764
- let next = answers.count - 1
765
- Task { @MainActor in
766
- try? await Task.sleep(for: Duration.milliseconds(15))
767
- focus = .answer(next)
768
- }
769
- }
770
- }
771
- ```
772
-
773
- The short sleep lets SwiftUI insert the new field before focus moves to it.
774
-
775
- Guidelines:
776
-
777
- - The view holding the text fields also holds their `@FocusState`.
778
- - React to focus moving: show a validation message or a hint for the field
779
- that just gained or lost focus.
780
- - Inside a `ScrollView` or `Form`, pair focus with `.scrollDismissesKeyboard(_:)`.
781
-
782
- Pitfalls:
783
-
784
- - Focus state stored in shared objects; it is local to the view.
785
- - Focus changed aggressively in the middle of an animation. Delay it when
786
- needed.