claudecode-omc 5.6.6 → 5.6.8
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/.local/skills/THIRD_PARTY_LICENSES/AvdLee-SwiftUI-Agent-Skill.LICENSE +21 -0
- package/.local/skills/THIRD_PARTY_LICENSES/Dimillian-Skills.LICENSE +21 -0
- package/.local/skills/THIRD_PARTY_LICENSES/README.md +36 -0
- package/.local/skills/THIRD_PARTY_LICENSES/twostraws-swiftui-agent-skill.LICENSE +21 -0
- package/.local/skills/h5-to-swiftui/SKILL.md +201 -0
- package/.local/skills/h5-to-swiftui/assets/calibration/README.md +176 -0
- package/.local/skills/h5-to-swiftui/assets/calibration/h5-twin/index.html +52 -0
- package/.local/skills/h5-to-swiftui/assets/calibration/h5-twin/style.css +133 -0
- package/.local/skills/h5-to-swiftui/assets/calibration/swiftui-twin/Package.swift +26 -0
- package/.local/skills/h5-to-swiftui/assets/calibration/swiftui-twin/Sources/CalibrationScreen/CalibrationScreen.swift +142 -0
- package/.local/skills/h5-to-swiftui/assets/calibration/swiftui-twin-divergent/Package.swift +32 -0
- package/.local/skills/h5-to-swiftui/assets/calibration/swiftui-twin-divergent/Sources/CalibrationScreenDivergent/CalibrationScreenDivergent.swift +122 -0
- package/.local/skills/h5-to-swiftui/assets/calibration/tokens.json +42 -0
- package/.local/skills/h5-to-swiftui/assets/sample-h5-react/index.html +14 -0
- package/.local/skills/h5-to-swiftui/assets/sample-h5-react/package.json +20 -0
- package/.local/skills/h5-to-swiftui/assets/sample-h5-react/public/api/articles/001.json +96 -0
- package/.local/skills/h5-to-swiftui/assets/sample-h5-react/public/api/articles/index.json +89 -0
- package/.local/skills/h5-to-swiftui/assets/sample-h5-react/src/App.jsx +22 -0
- package/.local/skills/h5-to-swiftui/assets/sample-h5-react/src/App.module.css +11 -0
- package/.local/skills/h5-to-swiftui/assets/sample-h5-react/src/components/ArticleCard.jsx +53 -0
- package/.local/skills/h5-to-swiftui/assets/sample-h5-react/src/components/ArticleCard.module.css +139 -0
- package/.local/skills/h5-to-swiftui/assets/sample-h5-react/src/components/NavBar.jsx +37 -0
- package/.local/skills/h5-to-swiftui/assets/sample-h5-react/src/components/NavBar.module.css +72 -0
- package/.local/skills/h5-to-swiftui/assets/sample-h5-react/src/components/TagCloud.jsx +30 -0
- package/.local/skills/h5-to-swiftui/assets/sample-h5-react/src/components/TagCloud.module.css +50 -0
- package/.local/skills/h5-to-swiftui/assets/sample-h5-react/src/components/TrendChart.jsx +159 -0
- package/.local/skills/h5-to-swiftui/assets/sample-h5-react/src/components/TrendChart.module.css +21 -0
- package/.local/skills/h5-to-swiftui/assets/sample-h5-react/src/main.jsx +12 -0
- package/.local/skills/h5-to-swiftui/assets/sample-h5-react/src/screens/ArticleScreen.jsx +182 -0
- package/.local/skills/h5-to-swiftui/assets/sample-h5-react/src/screens/ArticleScreen.module.css +294 -0
- package/.local/skills/h5-to-swiftui/assets/sample-h5-react/src/screens/FeedScreen.jsx +147 -0
- package/.local/skills/h5-to-swiftui/assets/sample-h5-react/src/screens/FeedScreen.module.css +161 -0
- package/.local/skills/h5-to-swiftui/assets/sample-h5-react/src/styles/global.css +50 -0
- package/.local/skills/h5-to-swiftui/assets/sample-h5-react/src/styles/tokens.css +103 -0
- package/.local/skills/h5-to-swiftui/assets/sample-h5-react/vite.config.js +6 -0
- package/.local/skills/h5-to-swiftui/assets/sample-h5-vanilla/data/tasks.js +67 -0
- package/.local/skills/h5-to-swiftui/assets/sample-h5-vanilla/index.html +26 -0
- package/.local/skills/h5-to-swiftui/assets/sample-h5-vanilla/router.js +73 -0
- package/.local/skills/h5-to-swiftui/assets/sample-h5-vanilla/screens/detail.js +164 -0
- package/.local/skills/h5-to-swiftui/assets/sample-h5-vanilla/screens/home.js +53 -0
- package/.local/skills/h5-to-swiftui/assets/sample-h5-vanilla/screens/list.js +87 -0
- package/.local/skills/h5-to-swiftui/assets/sample-h5-vanilla/styles/app.css +342 -0
- package/.local/skills/h5-to-swiftui/assets/sample-h5-vanilla/styles/tokens.css +68 -0
- package/.local/skills/h5-to-swiftui/references/css-to-swiftui-map.md +205 -0
- package/.local/skills/h5-to-swiftui/references/design-token-extraction.md +209 -0
- package/.local/skills/h5-to-swiftui/references/high-risk-triage.md +209 -0
- package/.local/skills/h5-to-swiftui/references/render-equivalence-calibration.md +193 -0
- package/.local/skills/h5-to-swiftui/references/stack-detection.md +160 -0
- package/.local/skills/h5-to-swiftui/references/visual-diff-loop-protocol.md +365 -0
- package/.local/skills/h5-to-swiftui/scripts/_calib-consts.mjs +150 -0
- package/.local/skills/h5-to-swiftui/scripts/_imglib.mjs +547 -0
- package/.local/skills/h5-to-swiftui/scripts/_provenance.mjs +123 -0
- package/.local/skills/h5-to-swiftui/scripts/calibrate-render.mjs +625 -0
- package/.local/skills/h5-to-swiftui/scripts/capture-reference.mjs +386 -0
- package/.local/skills/h5-to-swiftui/scripts/detect-stack.mjs +305 -0
- package/.local/skills/h5-to-swiftui/scripts/evaluate-convergence.mjs +1093 -0
- package/.local/skills/h5-to-swiftui/scripts/extract-tokens.mjs +600 -0
- package/.local/skills/h5-to-swiftui/scripts/mark-overlay.mjs +379 -0
- package/.local/skills/h5-to-swiftui/scripts/pixel-diff.mjs +530 -0
- package/.local/skills/h5-to-swiftui/scripts/sim-screenshot.sh +544 -0
- package/.local/skills/ios-debugger-agent/SKILL.md +51 -0
- package/.local/skills/ios-debugger-agent/agents/openai.yaml +4 -0
- package/.local/skills/swift-concurrency-expert/SKILL.md +105 -0
- package/.local/skills/swift-concurrency-expert/agents/openai.yaml +4 -0
- package/.local/skills/swift-concurrency-expert/references/approachable-concurrency.md +63 -0
- package/.local/skills/swift-concurrency-expert/references/swift-6-2-concurrency.md +272 -0
- package/.local/skills/swift-concurrency-expert/references/swiftui-concurrency-tour-wwdc.md +33 -0
- package/.local/skills/swiftui-expert-skill/SKILL.md +162 -0
- package/.local/skills/swiftui-expert-skill/references/accessibility-patterns.md +215 -0
- package/.local/skills/swiftui-expert-skill/references/animation-advanced.md +403 -0
- package/.local/skills/swiftui-expert-skill/references/animation-basics.md +284 -0
- package/.local/skills/swiftui-expert-skill/references/animation-transitions.md +326 -0
- package/.local/skills/swiftui-expert-skill/references/charts-accessibility.md +135 -0
- package/.local/skills/swiftui-expert-skill/references/charts.md +602 -0
- package/.local/skills/swiftui-expert-skill/references/focus-patterns.md +299 -0
- package/.local/skills/swiftui-expert-skill/references/image-optimization.md +203 -0
- package/.local/skills/swiftui-expert-skill/references/latest-apis.md +488 -0
- package/.local/skills/swiftui-expert-skill/references/layout-best-practices.md +266 -0
- package/.local/skills/swiftui-expert-skill/references/liquid-glass.md +423 -0
- package/.local/skills/swiftui-expert-skill/references/list-patterns.md +446 -0
- package/.local/skills/swiftui-expert-skill/references/macos-scenes.md +318 -0
- package/.local/skills/swiftui-expert-skill/references/macos-views.md +357 -0
- package/.local/skills/swiftui-expert-skill/references/macos-window-styling.md +303 -0
- package/.local/skills/swiftui-expert-skill/references/performance-patterns.md +403 -0
- package/.local/skills/swiftui-expert-skill/references/scroll-patterns.md +293 -0
- package/.local/skills/swiftui-expert-skill/references/sheet-navigation-patterns.md +363 -0
- package/.local/skills/swiftui-expert-skill/references/state-management.md +388 -0
- package/.local/skills/swiftui-expert-skill/references/text-patterns.md +32 -0
- package/.local/skills/swiftui-expert-skill/references/trace-analysis.md +295 -0
- package/.local/skills/swiftui-expert-skill/references/trace-recording.md +134 -0
- package/.local/skills/swiftui-expert-skill/references/view-structure.md +780 -0
- package/.local/skills/swiftui-expert-skill/scripts/__pycache__/analyze_trace.cpython-313.pyc +0 -0
- package/.local/skills/swiftui-expert-skill/scripts/__pycache__/record_trace.cpython-313.pyc +0 -0
- package/.local/skills/swiftui-expert-skill/scripts/analyze_trace.py +301 -0
- package/.local/skills/swiftui-expert-skill/scripts/instruments_parser/__init__.py +1 -0
- package/.local/skills/swiftui-expert-skill/scripts/instruments_parser/__pycache__/__init__.cpython-313.pyc +0 -0
- package/.local/skills/swiftui-expert-skill/scripts/instruments_parser/__pycache__/causes.cpython-313.pyc +0 -0
- package/.local/skills/swiftui-expert-skill/scripts/instruments_parser/__pycache__/correlate.cpython-313.pyc +0 -0
- package/.local/skills/swiftui-expert-skill/scripts/instruments_parser/__pycache__/events.cpython-313.pyc +0 -0
- package/.local/skills/swiftui-expert-skill/scripts/instruments_parser/__pycache__/hangs.cpython-313.pyc +0 -0
- package/.local/skills/swiftui-expert-skill/scripts/instruments_parser/__pycache__/hitches.cpython-313.pyc +0 -0
- package/.local/skills/swiftui-expert-skill/scripts/instruments_parser/__pycache__/summary.cpython-313.pyc +0 -0
- package/.local/skills/swiftui-expert-skill/scripts/instruments_parser/__pycache__/swiftui.cpython-313.pyc +0 -0
- package/.local/skills/swiftui-expert-skill/scripts/instruments_parser/__pycache__/time_profiler.cpython-313.pyc +0 -0
- package/.local/skills/swiftui-expert-skill/scripts/instruments_parser/__pycache__/xctrace.cpython-313.pyc +0 -0
- package/.local/skills/swiftui-expert-skill/scripts/instruments_parser/__pycache__/xml_utils.cpython-313.pyc +0 -0
- package/.local/skills/swiftui-expert-skill/scripts/instruments_parser/causes.py +187 -0
- package/.local/skills/swiftui-expert-skill/scripts/instruments_parser/correlate.py +179 -0
- package/.local/skills/swiftui-expert-skill/scripts/instruments_parser/events.py +291 -0
- package/.local/skills/swiftui-expert-skill/scripts/instruments_parser/hangs.py +108 -0
- package/.local/skills/swiftui-expert-skill/scripts/instruments_parser/hitches.py +145 -0
- package/.local/skills/swiftui-expert-skill/scripts/instruments_parser/summary.py +243 -0
- package/.local/skills/swiftui-expert-skill/scripts/instruments_parser/swiftui.py +195 -0
- package/.local/skills/swiftui-expert-skill/scripts/instruments_parser/time_profiler.py +135 -0
- package/.local/skills/swiftui-expert-skill/scripts/instruments_parser/xctrace.py +117 -0
- package/.local/skills/swiftui-expert-skill/scripts/instruments_parser/xml_utils.py +224 -0
- package/.local/skills/swiftui-expert-skill/scripts/record_trace.py +252 -0
- package/.local/skills/swiftui-liquid-glass/SKILL.md +90 -0
- package/.local/skills/swiftui-liquid-glass/agents/openai.yaml +4 -0
- package/.local/skills/swiftui-liquid-glass/references/liquid-glass.md +280 -0
- package/.local/skills/swiftui-performance-audit/SKILL.md +106 -0
- package/.local/skills/swiftui-performance-audit/agents/openai.yaml +4 -0
- package/.local/skills/swiftui-performance-audit/references/code-smells.md +150 -0
- package/.local/skills/swiftui-performance-audit/references/demystify-swiftui-performance-wwdc23.md +46 -0
- package/.local/skills/swiftui-performance-audit/references/optimizing-swiftui-performance-instruments.md +29 -0
- package/.local/skills/swiftui-performance-audit/references/profiling-intake.md +44 -0
- package/.local/skills/swiftui-performance-audit/references/report-template.md +47 -0
- package/.local/skills/swiftui-performance-audit/references/understanding-hangs-in-your-app.md +33 -0
- package/.local/skills/swiftui-performance-audit/references/understanding-improving-swiftui-performance.md +52 -0
- package/.local/skills/swiftui-pro/SKILL.md +108 -0
- package/.local/skills/swiftui-pro/agents/openai.yaml +10 -0
- package/.local/skills/swiftui-pro/assets/swiftui-pro-icon.png +0 -0
- package/.local/skills/swiftui-pro/assets/swiftui-pro-icon.svg +29 -0
- package/.local/skills/swiftui-pro/references/accessibility.md +13 -0
- package/.local/skills/swiftui-pro/references/api.md +39 -0
- package/.local/skills/swiftui-pro/references/data.md +43 -0
- package/.local/skills/swiftui-pro/references/design.md +32 -0
- package/.local/skills/swiftui-pro/references/hygiene.md +9 -0
- package/.local/skills/swiftui-pro/references/navigation.md +14 -0
- package/.local/skills/swiftui-pro/references/performance.md +46 -0
- package/.local/skills/swiftui-pro/references/swift.md +56 -0
- package/.local/skills/swiftui-pro/references/views.md +36 -0
- package/.local/skills/swiftui-ui-patterns/SKILL.md +95 -0
- package/.local/skills/swiftui-ui-patterns/agents/openai.yaml +4 -0
- package/.local/skills/swiftui-ui-patterns/references/app-wiring.md +201 -0
- package/.local/skills/swiftui-ui-patterns/references/async-state.md +96 -0
- package/.local/skills/swiftui-ui-patterns/references/components-index.md +50 -0
- package/.local/skills/swiftui-ui-patterns/references/controls.md +57 -0
- package/.local/skills/swiftui-ui-patterns/references/deeplinks.md +66 -0
- package/.local/skills/swiftui-ui-patterns/references/focus.md +90 -0
- package/.local/skills/swiftui-ui-patterns/references/form.md +97 -0
- package/.local/skills/swiftui-ui-patterns/references/grids.md +71 -0
- package/.local/skills/swiftui-ui-patterns/references/haptics.md +71 -0
- package/.local/skills/swiftui-ui-patterns/references/input-toolbar.md +51 -0
- package/.local/skills/swiftui-ui-patterns/references/lightweight-clients.md +93 -0
- package/.local/skills/swiftui-ui-patterns/references/list.md +86 -0
- package/.local/skills/swiftui-ui-patterns/references/loading-placeholders.md +38 -0
- package/.local/skills/swiftui-ui-patterns/references/macos-settings.md +71 -0
- package/.local/skills/swiftui-ui-patterns/references/matched-transitions.md +59 -0
- package/.local/skills/swiftui-ui-patterns/references/media.md +73 -0
- package/.local/skills/swiftui-ui-patterns/references/menu-bar.md +101 -0
- package/.local/skills/swiftui-ui-patterns/references/navigationstack.md +159 -0
- package/.local/skills/swiftui-ui-patterns/references/overlay.md +45 -0
- package/.local/skills/swiftui-ui-patterns/references/performance.md +62 -0
- package/.local/skills/swiftui-ui-patterns/references/previews.md +48 -0
- package/.local/skills/swiftui-ui-patterns/references/scroll-reveal.md +133 -0
- package/.local/skills/swiftui-ui-patterns/references/scrollview.md +87 -0
- package/.local/skills/swiftui-ui-patterns/references/searchable.md +71 -0
- package/.local/skills/swiftui-ui-patterns/references/sheets.md +155 -0
- package/.local/skills/swiftui-ui-patterns/references/split-views.md +72 -0
- package/.local/skills/swiftui-ui-patterns/references/tabview.md +114 -0
- package/.local/skills/swiftui-ui-patterns/references/theming.md +71 -0
- package/.local/skills/swiftui-ui-patterns/references/title-menus.md +93 -0
- package/.local/skills/swiftui-ui-patterns/references/top-bar.md +49 -0
- package/.local/skills/swiftui-view-refactor/SKILL.md +202 -0
- package/.local/skills/swiftui-view-refactor/agents/openai.yaml +4 -0
- package/.local/skills/swiftui-view-refactor/references/mv-patterns.md +161 -0
- package/bundled/manifest.json +1 -1
- package/package.json +1 -1
|
@@ -0,0 +1,96 @@
|
|
|
1
|
+
# Async state and task lifecycle
|
|
2
|
+
|
|
3
|
+
## Intent
|
|
4
|
+
|
|
5
|
+
Use this pattern when a view loads data, reacts to changing input, or coordinates async work that should follow the SwiftUI view lifecycle.
|
|
6
|
+
|
|
7
|
+
## Core rules
|
|
8
|
+
|
|
9
|
+
- Use `.task` for load-on-appear work that belongs to the view lifecycle.
|
|
10
|
+
- Use `.task(id:)` when async work should restart for a changing input such as a query, selection, or identifier.
|
|
11
|
+
- Treat cancellation as a normal path for view-driven tasks. Check `Task.isCancelled` in longer flows and avoid surfacing cancellation as a user-facing error.
|
|
12
|
+
- Debounce or coalesce user-driven async work such as search before it fans out into repeated requests.
|
|
13
|
+
- Keep UI-facing models and mutations main-actor-safe; do background work in services, then publish the result back to UI state.
|
|
14
|
+
|
|
15
|
+
## Example: load on appear
|
|
16
|
+
|
|
17
|
+
```swift
|
|
18
|
+
struct DetailView: View {
|
|
19
|
+
let id: String
|
|
20
|
+
@State private var state: LoadState<Item> = .idle
|
|
21
|
+
@Environment(ItemClient.self) private var client
|
|
22
|
+
|
|
23
|
+
var body: some View {
|
|
24
|
+
content
|
|
25
|
+
.task {
|
|
26
|
+
await load()
|
|
27
|
+
}
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
@ViewBuilder
|
|
31
|
+
private var content: some View {
|
|
32
|
+
switch state {
|
|
33
|
+
case .idle, .loading:
|
|
34
|
+
ProgressView()
|
|
35
|
+
case .loaded(let item):
|
|
36
|
+
ItemContent(item: item)
|
|
37
|
+
case .failed(let error):
|
|
38
|
+
ErrorView(error: error)
|
|
39
|
+
}
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
private func load() async {
|
|
43
|
+
state = .loading
|
|
44
|
+
do {
|
|
45
|
+
state = .loaded(try await client.fetch(id: id))
|
|
46
|
+
} catch is CancellationError {
|
|
47
|
+
return
|
|
48
|
+
} catch {
|
|
49
|
+
state = .failed(error)
|
|
50
|
+
}
|
|
51
|
+
}
|
|
52
|
+
}
|
|
53
|
+
```
|
|
54
|
+
|
|
55
|
+
## Example: restart on input change
|
|
56
|
+
|
|
57
|
+
```swift
|
|
58
|
+
struct SearchView: View {
|
|
59
|
+
@State private var query = ""
|
|
60
|
+
@State private var results: [ResultItem] = []
|
|
61
|
+
@Environment(SearchClient.self) private var client
|
|
62
|
+
|
|
63
|
+
var body: some View {
|
|
64
|
+
List(results) { item in
|
|
65
|
+
Text(item.title)
|
|
66
|
+
}
|
|
67
|
+
.searchable(text: $query)
|
|
68
|
+
.task(id: query) {
|
|
69
|
+
try? await Task.sleep(for: .milliseconds(250))
|
|
70
|
+
guard !Task.isCancelled, !query.isEmpty else {
|
|
71
|
+
results = []
|
|
72
|
+
return
|
|
73
|
+
}
|
|
74
|
+
do {
|
|
75
|
+
results = try await client.search(query)
|
|
76
|
+
} catch is CancellationError {
|
|
77
|
+
return
|
|
78
|
+
} catch {
|
|
79
|
+
results = []
|
|
80
|
+
}
|
|
81
|
+
}
|
|
82
|
+
}
|
|
83
|
+
}
|
|
84
|
+
```
|
|
85
|
+
|
|
86
|
+
## When to move work out of the view
|
|
87
|
+
|
|
88
|
+
- If the async flow spans multiple screens or must survive view dismissal, move it into a service or model.
|
|
89
|
+
- If the view is mostly coordinating app-level lifecycle or account changes, wire it at the app shell in `app-wiring.md`.
|
|
90
|
+
- If retry, caching, or offline policy becomes complex, keep the policy in the client/service and leave the view with simple state transitions.
|
|
91
|
+
|
|
92
|
+
## Pitfalls
|
|
93
|
+
|
|
94
|
+
- Do not start network work directly from `body`.
|
|
95
|
+
- Do not ignore cancellation for searches, typeahead, or rapidly changing selections.
|
|
96
|
+
- Avoid storing derived async state in multiple places when one source of truth is enough.
|
|
@@ -0,0 +1,50 @@
|
|
|
1
|
+
# Components Index
|
|
2
|
+
|
|
3
|
+
Use this file to find component and cross-cutting guidance. Each entry lists when to use it.
|
|
4
|
+
|
|
5
|
+
## Available components
|
|
6
|
+
|
|
7
|
+
- TabView: `references/tabview.md` — Use when building a tab-based app or any tabbed feature set.
|
|
8
|
+
- NavigationStack: `references/navigationstack.md` — Use when you need push navigation and programmatic routing, especially per-tab history.
|
|
9
|
+
- Sheets and presentation: `references/sheets.md` — Use for local item-driven sheets, centralized modal routing, and sheet-specific action patterns.
|
|
10
|
+
- Form and Settings: `references/form.md` — Use for settings, grouped inputs, and structured data entry.
|
|
11
|
+
- macOS Settings: `references/macos-settings.md` — Use when building a macOS Settings window with SwiftUI's Settings scene.
|
|
12
|
+
- Split views and columns: `references/split-views.md` — Use for iPad/macOS multi-column layouts or custom secondary columns.
|
|
13
|
+
- List and Section: `references/list.md` — Use for feed-style content and settings rows.
|
|
14
|
+
- ScrollView and Lazy stacks: `references/scrollview.md` — Use for custom layouts, horizontal scrollers, or grids.
|
|
15
|
+
- Scroll-reveal detail surfaces: `references/scroll-reveal.md` — Use when a detail screen reveals secondary content or actions as the user scrolls or swipes between full-screen sections.
|
|
16
|
+
- Grids: `references/grids.md` — Use for icon pickers, media galleries, and tiled layouts.
|
|
17
|
+
- Theming and dynamic type: `references/theming.md` — Use for app-wide theme tokens, colors, and type scaling.
|
|
18
|
+
- Controls (toggles, pickers, sliders): `references/controls.md` — Use for settings controls and input selection.
|
|
19
|
+
- Input toolbar (bottom anchored): `references/input-toolbar.md` — Use for chat/composer screens with a sticky input bar.
|
|
20
|
+
- Top bar overlays (iOS 26+ and fallback): `references/top-bar.md` — Use for pinned selectors or pills above scroll content.
|
|
21
|
+
- Overlay and toasts: `references/overlay.md` — Use for transient UI like banners or toasts.
|
|
22
|
+
- Focus handling: `references/focus.md` — Use for chaining fields and keyboard focus management.
|
|
23
|
+
- Searchable: `references/searchable.md` — Use for native search UI with scopes and async results.
|
|
24
|
+
- Async images and media: `references/media.md` — Use for remote media, previews, and media viewers.
|
|
25
|
+
- Haptics: `references/haptics.md` — Use for tactile feedback tied to key actions.
|
|
26
|
+
- Matched transitions: `references/matched-transitions.md` — Use for smooth source-to-destination animations.
|
|
27
|
+
- Deep links and URL routing: `references/deeplinks.md` — Use for in-app navigation from URLs.
|
|
28
|
+
- Title menus: `references/title-menus.md` — Use for filter or context menus in the navigation title.
|
|
29
|
+
- Menu bar commands: `references/menu-bar.md` — Use when adding or customizing macOS/iPadOS menu bar commands.
|
|
30
|
+
- Loading & placeholders: `references/loading-placeholders.md` — Use for redacted skeletons, empty states, and loading UX.
|
|
31
|
+
- Lightweight clients: `references/lightweight-clients.md` — Use for small, closure-based API clients injected into stores.
|
|
32
|
+
|
|
33
|
+
## Cross-cutting references
|
|
34
|
+
|
|
35
|
+
- App wiring and dependency graph: `references/app-wiring.md` — Use to wire the app shell, install shared dependencies, and decide what belongs in the environment.
|
|
36
|
+
- Async state and task lifecycle: `references/async-state.md` — Use when a view loads data, reacts to changing input, or needs cancellation/debouncing guidance.
|
|
37
|
+
- Previews: `references/previews.md` — Use when adding `#Preview`, fixtures, mock environments, or isolated preview setup.
|
|
38
|
+
- Performance guardrails: `references/performance.md` — Use when a screen is large, scroll-heavy, frequently updated, or showing signs of avoidable re-renders.
|
|
39
|
+
|
|
40
|
+
## Planned components (create files as needed)
|
|
41
|
+
|
|
42
|
+
- Web content: create `references/webview.md` — Use for embedded web content or in-app browsing.
|
|
43
|
+
- Status composer patterns: create `references/composer.md` — Use for composition or editor workflows.
|
|
44
|
+
- Text input and validation: create `references/text-input.md` — Use for forms, validation, and text-heavy input.
|
|
45
|
+
- Design system usage: create `references/design-system.md` — Use when applying shared styling rules.
|
|
46
|
+
|
|
47
|
+
## Adding entries
|
|
48
|
+
|
|
49
|
+
- Add the component file and link it here with a short “when to use” description.
|
|
50
|
+
- Keep each component reference short and actionable.
|
|
@@ -0,0 +1,57 @@
|
|
|
1
|
+
# Controls (Toggle, Slider, Picker)
|
|
2
|
+
|
|
3
|
+
## Intent
|
|
4
|
+
|
|
5
|
+
Use native controls for settings and configuration screens, keeping labels accessible and state bindings clear.
|
|
6
|
+
|
|
7
|
+
## Core patterns
|
|
8
|
+
|
|
9
|
+
- Bind controls directly to `@State`, `@Binding`, or `@AppStorage`.
|
|
10
|
+
- Prefer `Toggle` for boolean preferences.
|
|
11
|
+
- Use `Slider` for numeric ranges and show the current value in a label.
|
|
12
|
+
- Use `Picker` for discrete choices; use `.pickerStyle(.segmented)` only for 2–4 options.
|
|
13
|
+
- Keep labels visible and descriptive; avoid embedding buttons inside controls.
|
|
14
|
+
|
|
15
|
+
## Example: toggles with sections
|
|
16
|
+
|
|
17
|
+
```swift
|
|
18
|
+
Form {
|
|
19
|
+
Section("Notifications") {
|
|
20
|
+
Toggle("Mentions", isOn: $preferences.notificationsMentionsEnabled)
|
|
21
|
+
Toggle("Follows", isOn: $preferences.notificationsFollowsEnabled)
|
|
22
|
+
Toggle("Boosts", isOn: $preferences.notificationsBoostsEnabled)
|
|
23
|
+
}
|
|
24
|
+
}
|
|
25
|
+
```
|
|
26
|
+
|
|
27
|
+
## Example: slider with value text
|
|
28
|
+
|
|
29
|
+
```swift
|
|
30
|
+
Section("Font Size") {
|
|
31
|
+
Slider(value: $fontSizeScale, in: 0.5...1.5, step: 0.1)
|
|
32
|
+
Text("Scale: \(String(format: \"%.1f\", fontSizeScale))")
|
|
33
|
+
.font(.scaledBody)
|
|
34
|
+
}
|
|
35
|
+
```
|
|
36
|
+
|
|
37
|
+
## Example: picker for enums
|
|
38
|
+
|
|
39
|
+
```swift
|
|
40
|
+
Picker("Default Visibility", selection: $visibility) {
|
|
41
|
+
ForEach(Visibility.allCases, id: \.self) { option in
|
|
42
|
+
Text(option.title).tag(option)
|
|
43
|
+
}
|
|
44
|
+
}
|
|
45
|
+
```
|
|
46
|
+
|
|
47
|
+
## Design choices to keep
|
|
48
|
+
|
|
49
|
+
- Group related controls in a `Form` section.
|
|
50
|
+
- Use `.disabled(...)` to reflect locked or inherited settings.
|
|
51
|
+
- Use `Label` inside toggles to combine icon + text when it adds clarity.
|
|
52
|
+
|
|
53
|
+
## Pitfalls
|
|
54
|
+
|
|
55
|
+
- Avoid `.pickerStyle(.segmented)` for large sets; use menu or inline styles instead.
|
|
56
|
+
- Don’t hide labels for sliders; always show context.
|
|
57
|
+
- Avoid hard-coding colors for controls; use theme tint sparingly.
|
|
@@ -0,0 +1,66 @@
|
|
|
1
|
+
# Deep links and navigation
|
|
2
|
+
|
|
3
|
+
## Intent
|
|
4
|
+
|
|
5
|
+
Route external URLs into in-app destinations while falling back to system handling when needed.
|
|
6
|
+
|
|
7
|
+
## Core patterns
|
|
8
|
+
|
|
9
|
+
- Centralize URL handling in the router (`handle(url:)`, `handleDeepLink(url:)`).
|
|
10
|
+
- Inject an `OpenURLAction` handler that delegates to the router.
|
|
11
|
+
- Use `.onOpenURL` for app scheme links and convert them to web URLs if needed.
|
|
12
|
+
- Let the router decide whether to navigate or open externally.
|
|
13
|
+
|
|
14
|
+
## Example: router entry points
|
|
15
|
+
|
|
16
|
+
```swift
|
|
17
|
+
@MainActor
|
|
18
|
+
final class RouterPath {
|
|
19
|
+
var path: [Route] = []
|
|
20
|
+
var urlHandler: ((URL) -> OpenURLAction.Result)?
|
|
21
|
+
|
|
22
|
+
func handle(url: URL) -> OpenURLAction.Result {
|
|
23
|
+
if isInternal(url) {
|
|
24
|
+
navigate(to: .status(id: url.lastPathComponent))
|
|
25
|
+
return .handled
|
|
26
|
+
}
|
|
27
|
+
return urlHandler?(url) ?? .systemAction
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
func handleDeepLink(url: URL) -> OpenURLAction.Result {
|
|
31
|
+
// Resolve federated URLs, then navigate.
|
|
32
|
+
navigate(to: .status(id: url.lastPathComponent))
|
|
33
|
+
return .handled
|
|
34
|
+
}
|
|
35
|
+
}
|
|
36
|
+
```
|
|
37
|
+
|
|
38
|
+
## Example: attach to a root view
|
|
39
|
+
|
|
40
|
+
```swift
|
|
41
|
+
extension View {
|
|
42
|
+
func withLinkRouter(_ router: RouterPath) -> some View {
|
|
43
|
+
self
|
|
44
|
+
.environment(
|
|
45
|
+
\.openURL,
|
|
46
|
+
OpenURLAction { url in
|
|
47
|
+
router.handle(url: url)
|
|
48
|
+
}
|
|
49
|
+
)
|
|
50
|
+
.onOpenURL { url in
|
|
51
|
+
router.handleDeepLink(url: url)
|
|
52
|
+
}
|
|
53
|
+
}
|
|
54
|
+
}
|
|
55
|
+
```
|
|
56
|
+
|
|
57
|
+
## Design choices to keep
|
|
58
|
+
|
|
59
|
+
- Keep URL parsing and decision logic inside the router.
|
|
60
|
+
- Avoid handling deep links in multiple places; one entry point is enough.
|
|
61
|
+
- Always provide a fallback to `OpenURLAction` or `UIApplication.shared.open`.
|
|
62
|
+
|
|
63
|
+
## Pitfalls
|
|
64
|
+
|
|
65
|
+
- Don’t assume the URL is internal; validate first.
|
|
66
|
+
- Avoid blocking UI while resolving remote links; use `Task`.
|
|
@@ -0,0 +1,90 @@
|
|
|
1
|
+
# Focus handling and field chaining
|
|
2
|
+
|
|
3
|
+
## Intent
|
|
4
|
+
|
|
5
|
+
Use `@FocusState` to control keyboard focus, chain fields, and coordinate focus across complex forms.
|
|
6
|
+
|
|
7
|
+
## Core patterns
|
|
8
|
+
|
|
9
|
+
- Use an enum to represent focusable fields.
|
|
10
|
+
- Set initial focus in `onAppear`.
|
|
11
|
+
- Use `.onSubmit` to move focus to the next field.
|
|
12
|
+
- For dynamic lists of fields, use an enum with associated values (e.g., `.option(Int)`).
|
|
13
|
+
|
|
14
|
+
## Example: single field focus
|
|
15
|
+
|
|
16
|
+
```swift
|
|
17
|
+
struct AddServerView: View {
|
|
18
|
+
@State private var server = ""
|
|
19
|
+
@FocusState private var isServerFieldFocused: Bool
|
|
20
|
+
|
|
21
|
+
var body: some View {
|
|
22
|
+
Form {
|
|
23
|
+
TextField("Server", text: $server)
|
|
24
|
+
.focused($isServerFieldFocused)
|
|
25
|
+
}
|
|
26
|
+
.onAppear { isServerFieldFocused = true }
|
|
27
|
+
}
|
|
28
|
+
}
|
|
29
|
+
```
|
|
30
|
+
|
|
31
|
+
## Example: chained focus with enum
|
|
32
|
+
|
|
33
|
+
```swift
|
|
34
|
+
struct EditTagView: View {
|
|
35
|
+
enum FocusField { case title, symbol, newTag }
|
|
36
|
+
@FocusState private var focusedField: FocusField?
|
|
37
|
+
|
|
38
|
+
var body: some View {
|
|
39
|
+
Form {
|
|
40
|
+
TextField("Title", text: $title)
|
|
41
|
+
.focused($focusedField, equals: .title)
|
|
42
|
+
.onSubmit { focusedField = .symbol }
|
|
43
|
+
|
|
44
|
+
TextField("Symbol", text: $symbol)
|
|
45
|
+
.focused($focusedField, equals: .symbol)
|
|
46
|
+
.onSubmit { focusedField = .newTag }
|
|
47
|
+
}
|
|
48
|
+
.onAppear { focusedField = .title }
|
|
49
|
+
}
|
|
50
|
+
}
|
|
51
|
+
```
|
|
52
|
+
|
|
53
|
+
## Example: dynamic focus for variable fields
|
|
54
|
+
|
|
55
|
+
```swift
|
|
56
|
+
struct PollView: View {
|
|
57
|
+
enum FocusField: Hashable { case option(Int) }
|
|
58
|
+
@FocusState private var focused: FocusField?
|
|
59
|
+
@State private var options: [String] = ["", ""]
|
|
60
|
+
@State private var currentIndex = 0
|
|
61
|
+
|
|
62
|
+
var body: some View {
|
|
63
|
+
ForEach(options.indices, id: \.self) { index in
|
|
64
|
+
TextField("Option \(index + 1)", text: $options[index])
|
|
65
|
+
.focused($focused, equals: .option(index))
|
|
66
|
+
.onSubmit { addOption(at: index) }
|
|
67
|
+
}
|
|
68
|
+
.onAppear { focused = .option(0) }
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
private func addOption(at index: Int) {
|
|
72
|
+
options.append("")
|
|
73
|
+
currentIndex = index + 1
|
|
74
|
+
DispatchQueue.main.asyncAfter(deadline: .now() + 0.01) {
|
|
75
|
+
focused = .option(currentIndex)
|
|
76
|
+
}
|
|
77
|
+
}
|
|
78
|
+
}
|
|
79
|
+
```
|
|
80
|
+
|
|
81
|
+
## Design choices to keep
|
|
82
|
+
|
|
83
|
+
- Keep focus state local to the view that owns the fields.
|
|
84
|
+
- Use focus changes to drive UX (validation messages, helper UI).
|
|
85
|
+
- Pair with `.scrollDismissesKeyboard(...)` when using ScrollView/Form.
|
|
86
|
+
|
|
87
|
+
## Pitfalls
|
|
88
|
+
|
|
89
|
+
- Don’t store focus state in shared objects; it is view-local.
|
|
90
|
+
- Avoid aggressive focus changes during animation; delay if needed.
|
|
@@ -0,0 +1,97 @@
|
|
|
1
|
+
# Form
|
|
2
|
+
|
|
3
|
+
## Intent
|
|
4
|
+
|
|
5
|
+
Use `Form` for structured settings, grouped inputs, and action rows. This pattern keeps layout, spacing, and accessibility consistent for data entry screens.
|
|
6
|
+
|
|
7
|
+
## Core patterns
|
|
8
|
+
|
|
9
|
+
- Wrap the form in a `NavigationStack` only when it is presented in a sheet or standalone view without an existing navigation context.
|
|
10
|
+
- Group related controls into `Section` blocks.
|
|
11
|
+
- Use `.scrollContentBackground(.hidden)` plus a custom background color when you need design-system colors.
|
|
12
|
+
- Apply `.formStyle(.grouped)` for grouped styling when appropriate.
|
|
13
|
+
- Use `@FocusState` to manage keyboard focus in input-heavy forms.
|
|
14
|
+
|
|
15
|
+
## Example: settings-style form
|
|
16
|
+
|
|
17
|
+
```swift
|
|
18
|
+
@MainActor
|
|
19
|
+
struct SettingsView: View {
|
|
20
|
+
@Environment(Theme.self) private var theme
|
|
21
|
+
|
|
22
|
+
var body: some View {
|
|
23
|
+
NavigationStack {
|
|
24
|
+
Form {
|
|
25
|
+
Section("General") {
|
|
26
|
+
NavigationLink("Display") { DisplaySettingsView() }
|
|
27
|
+
NavigationLink("Haptics") { HapticsSettingsView() }
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
Section("Account") {
|
|
31
|
+
Button("Edit profile") { /* open sheet */ }
|
|
32
|
+
.buttonStyle(.plain)
|
|
33
|
+
}
|
|
34
|
+
.listRowBackground(theme.primaryBackgroundColor)
|
|
35
|
+
}
|
|
36
|
+
.navigationTitle("Settings")
|
|
37
|
+
.navigationBarTitleDisplayMode(.inline)
|
|
38
|
+
.scrollContentBackground(.hidden)
|
|
39
|
+
.background(theme.secondaryBackgroundColor)
|
|
40
|
+
}
|
|
41
|
+
}
|
|
42
|
+
}
|
|
43
|
+
```
|
|
44
|
+
|
|
45
|
+
## Example: modal form with validation
|
|
46
|
+
|
|
47
|
+
```swift
|
|
48
|
+
@MainActor
|
|
49
|
+
struct AddRemoteServerView: View {
|
|
50
|
+
@Environment(\.dismiss) private var dismiss
|
|
51
|
+
@Environment(Theme.self) private var theme
|
|
52
|
+
|
|
53
|
+
@State private var server: String = ""
|
|
54
|
+
@State private var isValid = false
|
|
55
|
+
@FocusState private var isServerFieldFocused: Bool
|
|
56
|
+
|
|
57
|
+
var body: some View {
|
|
58
|
+
NavigationStack {
|
|
59
|
+
Form {
|
|
60
|
+
TextField("Server URL", text: $server)
|
|
61
|
+
.keyboardType(.URL)
|
|
62
|
+
.textInputAutocapitalization(.never)
|
|
63
|
+
.autocorrectionDisabled()
|
|
64
|
+
.focused($isServerFieldFocused)
|
|
65
|
+
.listRowBackground(theme.primaryBackgroundColor)
|
|
66
|
+
|
|
67
|
+
Button("Add") {
|
|
68
|
+
guard isValid else { return }
|
|
69
|
+
dismiss()
|
|
70
|
+
}
|
|
71
|
+
.disabled(!isValid)
|
|
72
|
+
.listRowBackground(theme.primaryBackgroundColor)
|
|
73
|
+
}
|
|
74
|
+
.formStyle(.grouped)
|
|
75
|
+
.navigationTitle("Add Server")
|
|
76
|
+
.navigationBarTitleDisplayMode(.inline)
|
|
77
|
+
.scrollContentBackground(.hidden)
|
|
78
|
+
.background(theme.secondaryBackgroundColor)
|
|
79
|
+
.scrollDismissesKeyboard(.immediately)
|
|
80
|
+
.toolbar { CancelToolbarItem() }
|
|
81
|
+
.onAppear { isServerFieldFocused = true }
|
|
82
|
+
}
|
|
83
|
+
}
|
|
84
|
+
}
|
|
85
|
+
```
|
|
86
|
+
|
|
87
|
+
## Design choices to keep
|
|
88
|
+
|
|
89
|
+
- Prefer `Form` over custom stacks for settings and input screens.
|
|
90
|
+
- Keep rows tappable by using `.contentShape(Rectangle())` and `.buttonStyle(.plain)` on row buttons.
|
|
91
|
+
- Use list row backgrounds to keep section styling consistent with your theme.
|
|
92
|
+
|
|
93
|
+
## Pitfalls
|
|
94
|
+
|
|
95
|
+
- Avoid heavy custom layouts inside a `Form`; it can lead to spacing issues.
|
|
96
|
+
- If you need highly custom layouts, prefer `ScrollView` + `VStack`.
|
|
97
|
+
- Don’t mix multiple background strategies; pick either default Form styling or custom colors.
|
|
@@ -0,0 +1,71 @@
|
|
|
1
|
+
# Grids
|
|
2
|
+
|
|
3
|
+
## Intent
|
|
4
|
+
|
|
5
|
+
Use `LazyVGrid` for icon pickers, media galleries, and dense visual selections where items align in columns.
|
|
6
|
+
|
|
7
|
+
## Core patterns
|
|
8
|
+
|
|
9
|
+
- Use `.adaptive` columns for layouts that should scale across device sizes.
|
|
10
|
+
- Use multiple `.flexible` columns when you want a fixed column count.
|
|
11
|
+
- Keep spacing consistent and small to avoid uneven gutters.
|
|
12
|
+
- Use `GeometryReader` inside grid cells when you need square thumbnails.
|
|
13
|
+
|
|
14
|
+
## Example: adaptive icon grid
|
|
15
|
+
|
|
16
|
+
```swift
|
|
17
|
+
let columns = [GridItem(.adaptive(minimum: 120, maximum: 1024))]
|
|
18
|
+
|
|
19
|
+
LazyVGrid(columns: columns, spacing: 6) {
|
|
20
|
+
ForEach(icons) { icon in
|
|
21
|
+
Button {
|
|
22
|
+
select(icon)
|
|
23
|
+
} label: {
|
|
24
|
+
ZStack(alignment: .bottomTrailing) {
|
|
25
|
+
Image(icon.previewName)
|
|
26
|
+
.resizable()
|
|
27
|
+
.aspectRatio(contentMode: .fit)
|
|
28
|
+
.cornerRadius(6)
|
|
29
|
+
if icon.isSelected {
|
|
30
|
+
Image(systemName: "checkmark.seal.fill")
|
|
31
|
+
.padding(4)
|
|
32
|
+
.tint(.green)
|
|
33
|
+
}
|
|
34
|
+
}
|
|
35
|
+
}
|
|
36
|
+
.buttonStyle(.plain)
|
|
37
|
+
}
|
|
38
|
+
}
|
|
39
|
+
```
|
|
40
|
+
|
|
41
|
+
## Example: fixed 3-column media grid
|
|
42
|
+
|
|
43
|
+
```swift
|
|
44
|
+
LazyVGrid(
|
|
45
|
+
columns: [
|
|
46
|
+
.init(.flexible(minimum: 100), spacing: 4),
|
|
47
|
+
.init(.flexible(minimum: 100), spacing: 4),
|
|
48
|
+
.init(.flexible(minimum: 100), spacing: 4),
|
|
49
|
+
],
|
|
50
|
+
spacing: 4
|
|
51
|
+
) {
|
|
52
|
+
ForEach(items) { item in
|
|
53
|
+
GeometryReader { proxy in
|
|
54
|
+
ThumbnailView(item: item)
|
|
55
|
+
.frame(width: proxy.size.width, height: proxy.size.width)
|
|
56
|
+
}
|
|
57
|
+
.aspectRatio(1, contentMode: .fit)
|
|
58
|
+
}
|
|
59
|
+
}
|
|
60
|
+
```
|
|
61
|
+
|
|
62
|
+
## Design choices to keep
|
|
63
|
+
|
|
64
|
+
- Use `LazyVGrid` for large collections; avoid non-lazy grids for big sets.
|
|
65
|
+
- Keep tap targets full-bleed using `.contentShape(Rectangle())` when needed.
|
|
66
|
+
- Prefer adaptive grids for settings pickers and flexible layouts.
|
|
67
|
+
|
|
68
|
+
## Pitfalls
|
|
69
|
+
|
|
70
|
+
- Avoid heavy overlays in every grid cell; it can be expensive.
|
|
71
|
+
- Don’t nest grids inside other grids without a clear reason.
|
|
@@ -0,0 +1,71 @@
|
|
|
1
|
+
# Haptics
|
|
2
|
+
|
|
3
|
+
## Intent
|
|
4
|
+
|
|
5
|
+
Use haptics sparingly to reinforce user actions (tab selection, refresh, success/error) and respect user preferences.
|
|
6
|
+
|
|
7
|
+
## Core patterns
|
|
8
|
+
|
|
9
|
+
- Centralize haptic triggers in a `HapticManager` or similar utility.
|
|
10
|
+
- Gate haptics behind user preferences and hardware support.
|
|
11
|
+
- Use distinct types for different UX moments (selection vs. notification vs. refresh).
|
|
12
|
+
|
|
13
|
+
## Example: simple haptic manager
|
|
14
|
+
|
|
15
|
+
```swift
|
|
16
|
+
@MainActor
|
|
17
|
+
final class HapticManager {
|
|
18
|
+
static let shared = HapticManager()
|
|
19
|
+
|
|
20
|
+
enum HapticType {
|
|
21
|
+
case buttonPress
|
|
22
|
+
case tabSelection
|
|
23
|
+
case dataRefresh(intensity: CGFloat)
|
|
24
|
+
case notification(UINotificationFeedbackGenerator.FeedbackType)
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
private let selectionGenerator = UISelectionFeedbackGenerator()
|
|
28
|
+
private let impactGenerator = UIImpactFeedbackGenerator(style: .heavy)
|
|
29
|
+
private let notificationGenerator = UINotificationFeedbackGenerator()
|
|
30
|
+
|
|
31
|
+
private init() { selectionGenerator.prepare() }
|
|
32
|
+
|
|
33
|
+
func fire(_ type: HapticType, isEnabled: Bool) {
|
|
34
|
+
guard isEnabled else { return }
|
|
35
|
+
switch type {
|
|
36
|
+
case .buttonPress:
|
|
37
|
+
impactGenerator.impactOccurred()
|
|
38
|
+
case .tabSelection:
|
|
39
|
+
selectionGenerator.selectionChanged()
|
|
40
|
+
case let .dataRefresh(intensity):
|
|
41
|
+
impactGenerator.impactOccurred(intensity: intensity)
|
|
42
|
+
case let .notification(style):
|
|
43
|
+
notificationGenerator.notificationOccurred(style)
|
|
44
|
+
}
|
|
45
|
+
}
|
|
46
|
+
}
|
|
47
|
+
```
|
|
48
|
+
|
|
49
|
+
## Example: usage
|
|
50
|
+
|
|
51
|
+
```swift
|
|
52
|
+
Button("Save") {
|
|
53
|
+
HapticManager.shared.fire(.notification(.success), isEnabled: preferences.hapticsEnabled)
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
TabView(selection: $selectedTab) { /* tabs */ }
|
|
57
|
+
.onChange(of: selectedTab) { _, _ in
|
|
58
|
+
HapticManager.shared.fire(.tabSelection, isEnabled: preferences.hapticTabSelectionEnabled)
|
|
59
|
+
}
|
|
60
|
+
```
|
|
61
|
+
|
|
62
|
+
## Design choices to keep
|
|
63
|
+
|
|
64
|
+
- Haptics should be subtle and not fire on every tiny interaction.
|
|
65
|
+
- Respect user preferences (toggle to disable).
|
|
66
|
+
- Keep haptic triggers close to the user action, not deep in data layers.
|
|
67
|
+
|
|
68
|
+
## Pitfalls
|
|
69
|
+
|
|
70
|
+
- Avoid firing multiple haptics in quick succession.
|
|
71
|
+
- Do not assume haptics are available; check support.
|
|
@@ -0,0 +1,51 @@
|
|
|
1
|
+
# Input toolbar (bottom anchored)
|
|
2
|
+
|
|
3
|
+
## Intent
|
|
4
|
+
|
|
5
|
+
Use a bottom-anchored input bar for chat, composer, or quick actions without fighting the keyboard.
|
|
6
|
+
|
|
7
|
+
## Core patterns
|
|
8
|
+
|
|
9
|
+
- Use `.safeAreaInset(edge: .bottom)` to anchor the toolbar above the keyboard.
|
|
10
|
+
- Keep the main content in a `ScrollView` or `List`.
|
|
11
|
+
- Drive focus with `@FocusState` and set initial focus when needed.
|
|
12
|
+
- Avoid embedding the input bar inside the scroll content; keep it separate.
|
|
13
|
+
|
|
14
|
+
## Example: scroll view + bottom input
|
|
15
|
+
|
|
16
|
+
```swift
|
|
17
|
+
@MainActor
|
|
18
|
+
struct ConversationView: View {
|
|
19
|
+
@FocusState private var isInputFocused: Bool
|
|
20
|
+
|
|
21
|
+
var body: some View {
|
|
22
|
+
ScrollViewReader { _ in
|
|
23
|
+
ScrollView {
|
|
24
|
+
LazyVStack {
|
|
25
|
+
ForEach(messages) { message in
|
|
26
|
+
MessageRow(message: message)
|
|
27
|
+
}
|
|
28
|
+
}
|
|
29
|
+
.padding(.horizontal, .layoutPadding)
|
|
30
|
+
}
|
|
31
|
+
.safeAreaInset(edge: .bottom) {
|
|
32
|
+
InputBar(text: $draft)
|
|
33
|
+
.focused($isInputFocused)
|
|
34
|
+
}
|
|
35
|
+
.scrollDismissesKeyboard(.interactively)
|
|
36
|
+
.onAppear { isInputFocused = true }
|
|
37
|
+
}
|
|
38
|
+
}
|
|
39
|
+
}
|
|
40
|
+
```
|
|
41
|
+
|
|
42
|
+
## Design choices to keep
|
|
43
|
+
|
|
44
|
+
- Keep the input bar visually separated from the scrollable content.
|
|
45
|
+
- Use `.scrollDismissesKeyboard(.interactively)` for chat-like screens.
|
|
46
|
+
- Ensure send actions are reachable via keyboard return or a clear button.
|
|
47
|
+
|
|
48
|
+
## Pitfalls
|
|
49
|
+
|
|
50
|
+
- Avoid placing the input view inside the scroll stack; it will jump with content.
|
|
51
|
+
- Avoid nested scroll views that fight for drag gestures.
|