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,93 @@
|
|
|
1
|
+
# Lightweight Clients (Closure-Based)
|
|
2
|
+
|
|
3
|
+
Use this pattern to keep networking or service dependencies simple and testable without introducing a full view model or heavy DI framework. It works well for SwiftUI apps where you want a small, composable API surface that can be swapped in previews/tests.
|
|
4
|
+
|
|
5
|
+
## Intent
|
|
6
|
+
- Provide a tiny "client" type made of async closures.
|
|
7
|
+
- Keep business logic in a store or feature layer, not the view.
|
|
8
|
+
- Enable easy stubbing in previews/tests.
|
|
9
|
+
|
|
10
|
+
## Minimal shape
|
|
11
|
+
```swift
|
|
12
|
+
struct SomeClient {
|
|
13
|
+
var fetchItems: (_ limit: Int) async throws -> [Item]
|
|
14
|
+
var search: (_ query: String, _ limit: Int) async throws -> [Item]
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
extension SomeClient {
|
|
18
|
+
static func live(baseURL: URL = URL(string: "https://example.com")!) -> SomeClient {
|
|
19
|
+
let session = URLSession.shared
|
|
20
|
+
return SomeClient(
|
|
21
|
+
fetchItems: { limit in
|
|
22
|
+
// build URL, call session, decode
|
|
23
|
+
},
|
|
24
|
+
search: { query, limit in
|
|
25
|
+
// build URL, call session, decode
|
|
26
|
+
}
|
|
27
|
+
)
|
|
28
|
+
}
|
|
29
|
+
}
|
|
30
|
+
```
|
|
31
|
+
|
|
32
|
+
## Usage pattern
|
|
33
|
+
```swift
|
|
34
|
+
@MainActor
|
|
35
|
+
@Observable final class ItemsStore {
|
|
36
|
+
enum LoadState { case idle, loading, loaded, failed(String) }
|
|
37
|
+
|
|
38
|
+
var items: [Item] = []
|
|
39
|
+
var state: LoadState = .idle
|
|
40
|
+
private let client: SomeClient
|
|
41
|
+
|
|
42
|
+
init(client: SomeClient) {
|
|
43
|
+
self.client = client
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
func load(limit: Int = 20) async {
|
|
47
|
+
state = .loading
|
|
48
|
+
do {
|
|
49
|
+
items = try await client.fetchItems(limit)
|
|
50
|
+
state = .loaded
|
|
51
|
+
} catch {
|
|
52
|
+
state = .failed(error.localizedDescription)
|
|
53
|
+
}
|
|
54
|
+
}
|
|
55
|
+
}
|
|
56
|
+
```
|
|
57
|
+
|
|
58
|
+
```swift
|
|
59
|
+
struct ContentView: View {
|
|
60
|
+
@Environment(ItemsStore.self) private var store
|
|
61
|
+
|
|
62
|
+
var body: some View {
|
|
63
|
+
List(store.items) { item in
|
|
64
|
+
Text(item.title)
|
|
65
|
+
}
|
|
66
|
+
.task { await store.load() }
|
|
67
|
+
}
|
|
68
|
+
}
|
|
69
|
+
```
|
|
70
|
+
|
|
71
|
+
```swift
|
|
72
|
+
@main
|
|
73
|
+
struct MyApp: App {
|
|
74
|
+
@State private var store = ItemsStore(client: .live())
|
|
75
|
+
|
|
76
|
+
var body: some Scene {
|
|
77
|
+
WindowGroup {
|
|
78
|
+
ContentView()
|
|
79
|
+
.environment(store)
|
|
80
|
+
}
|
|
81
|
+
}
|
|
82
|
+
}
|
|
83
|
+
```
|
|
84
|
+
|
|
85
|
+
## Guidance
|
|
86
|
+
- Keep decoding and URL-building in the client; keep state changes in the store.
|
|
87
|
+
- Make the store accept the client in `init` and keep it private.
|
|
88
|
+
- Avoid global singletons; use `.environment` for store injection.
|
|
89
|
+
- If you need multiple variants (mock/stub), add `static func mock(...)`.
|
|
90
|
+
|
|
91
|
+
## Pitfalls
|
|
92
|
+
- Don’t put UI state in the client; keep state in the store.
|
|
93
|
+
- Don’t capture `self` or view state in the client closures.
|
|
@@ -0,0 +1,86 @@
|
|
|
1
|
+
# List and Section
|
|
2
|
+
|
|
3
|
+
## Intent
|
|
4
|
+
|
|
5
|
+
Use `List` for feed-style content and settings-style rows where built-in row reuse, selection, and accessibility matter.
|
|
6
|
+
|
|
7
|
+
## Core patterns
|
|
8
|
+
|
|
9
|
+
- Prefer `List` for long, vertically scrolling content with repeated rows.
|
|
10
|
+
- Use `Section` headers to group related rows.
|
|
11
|
+
- Pair with `ScrollViewReader` when you need scroll-to-top or jump-to-id.
|
|
12
|
+
- Use `.listStyle(.plain)` for modern feed layouts.
|
|
13
|
+
- Use `.listStyle(.grouped)` for multi-section discovery/search pages where section grouping helps.
|
|
14
|
+
- Apply `.scrollContentBackground(.hidden)` + a custom background when you need a themed surface.
|
|
15
|
+
- Use `.listRowInsets(...)` and `.listRowSeparator(.hidden)` to tune row spacing and separators.
|
|
16
|
+
- Use `.environment(\\.defaultMinListRowHeight, ...)` to control dense list layouts.
|
|
17
|
+
|
|
18
|
+
## Example: feed list with scroll-to-top
|
|
19
|
+
|
|
20
|
+
```swift
|
|
21
|
+
@MainActor
|
|
22
|
+
struct TimelineListView: View {
|
|
23
|
+
@Environment(\.selectedTabScrollToTop) private var selectedTabScrollToTop
|
|
24
|
+
@State private var scrollToId: String?
|
|
25
|
+
|
|
26
|
+
var body: some View {
|
|
27
|
+
ScrollViewReader { proxy in
|
|
28
|
+
List {
|
|
29
|
+
ForEach(items) { item in
|
|
30
|
+
TimelineRow(item: item)
|
|
31
|
+
.id(item.id)
|
|
32
|
+
.listRowInsets(.init(top: 12, leading: 16, bottom: 6, trailing: 16))
|
|
33
|
+
.listRowSeparator(.hidden)
|
|
34
|
+
}
|
|
35
|
+
}
|
|
36
|
+
.listStyle(.plain)
|
|
37
|
+
.environment(\\.defaultMinListRowHeight, 1)
|
|
38
|
+
.onChange(of: scrollToId) { _, newValue in
|
|
39
|
+
if let newValue {
|
|
40
|
+
proxy.scrollTo(newValue, anchor: .top)
|
|
41
|
+
scrollToId = nil
|
|
42
|
+
}
|
|
43
|
+
}
|
|
44
|
+
.onChange(of: selectedTabScrollToTop) { _, newValue in
|
|
45
|
+
if newValue == 0 {
|
|
46
|
+
withAnimation {
|
|
47
|
+
proxy.scrollTo(ScrollToView.Constants.scrollToTop, anchor: .top)
|
|
48
|
+
}
|
|
49
|
+
}
|
|
50
|
+
}
|
|
51
|
+
}
|
|
52
|
+
}
|
|
53
|
+
}
|
|
54
|
+
```
|
|
55
|
+
|
|
56
|
+
## Example: settings-style list
|
|
57
|
+
|
|
58
|
+
```swift
|
|
59
|
+
@MainActor
|
|
60
|
+
struct SettingsView: View {
|
|
61
|
+
var body: some View {
|
|
62
|
+
List {
|
|
63
|
+
Section("General") {
|
|
64
|
+
NavigationLink("Display") { DisplaySettingsView() }
|
|
65
|
+
NavigationLink("Haptics") { HapticsSettingsView() }
|
|
66
|
+
}
|
|
67
|
+
Section("Account") {
|
|
68
|
+
Button("Sign Out", role: .destructive) {}
|
|
69
|
+
}
|
|
70
|
+
}
|
|
71
|
+
.listStyle(.insetGrouped)
|
|
72
|
+
}
|
|
73
|
+
}
|
|
74
|
+
```
|
|
75
|
+
|
|
76
|
+
## Design choices to keep
|
|
77
|
+
|
|
78
|
+
- Use `List` for dynamic feeds, settings, and any UI where row semantics help.
|
|
79
|
+
- Use stable IDs for rows to keep animations and scroll positioning reliable.
|
|
80
|
+
- Prefer `.contentShape(Rectangle())` on rows that should be tappable end-to-end.
|
|
81
|
+
- Use `.refreshable` for pull-to-refresh feeds when the data source supports it.
|
|
82
|
+
|
|
83
|
+
## Pitfalls
|
|
84
|
+
|
|
85
|
+
- Avoid heavy custom layouts inside a `List` row; use `ScrollView` + `LazyVStack` instead.
|
|
86
|
+
- Be careful mixing `List` and nested `ScrollView`; it can cause gesture conflicts.
|
|
@@ -0,0 +1,38 @@
|
|
|
1
|
+
# Loading & Placeholders
|
|
2
|
+
|
|
3
|
+
Use this when a view needs a consistent loading state (skeletons, redaction, empty state) without blocking interaction.
|
|
4
|
+
|
|
5
|
+
## Patterns to prefer
|
|
6
|
+
|
|
7
|
+
- **Redacted placeholders** for list/detail content to preserve layout while loading.
|
|
8
|
+
- **ContentUnavailableView** for empty or error states after loading completes.
|
|
9
|
+
- **ProgressView** only for short, global operations (use sparingly in content-heavy screens).
|
|
10
|
+
|
|
11
|
+
## Recommended approach
|
|
12
|
+
|
|
13
|
+
1. Keep the real layout, render placeholder data, then apply `.redacted(reason: .placeholder)`.
|
|
14
|
+
2. For lists, show a fixed number of placeholder rows (avoid infinite spinners).
|
|
15
|
+
3. Switch to `ContentUnavailableView` when load finishes but data is empty.
|
|
16
|
+
|
|
17
|
+
## Pitfalls
|
|
18
|
+
|
|
19
|
+
- Don’t animate layout shifts during redaction; keep frames stable.
|
|
20
|
+
- Avoid nesting multiple spinners; use one loading indicator per section.
|
|
21
|
+
- Keep placeholder count small (3–6) to reduce jank on low-end devices.
|
|
22
|
+
|
|
23
|
+
## Minimal usage
|
|
24
|
+
|
|
25
|
+
```swift
|
|
26
|
+
VStack {
|
|
27
|
+
if isLoading {
|
|
28
|
+
ForEach(0..<3, id: \.self) { _ in
|
|
29
|
+
RowView(model: .placeholder())
|
|
30
|
+
}
|
|
31
|
+
.redacted(reason: .placeholder)
|
|
32
|
+
} else if items.isEmpty {
|
|
33
|
+
ContentUnavailableView("No items", systemImage: "tray")
|
|
34
|
+
} else {
|
|
35
|
+
ForEach(items) { item in RowView(model: item) }
|
|
36
|
+
}
|
|
37
|
+
}
|
|
38
|
+
```
|
|
@@ -0,0 +1,71 @@
|
|
|
1
|
+
# macOS Settings
|
|
2
|
+
|
|
3
|
+
## Intent
|
|
4
|
+
|
|
5
|
+
Use this when building a macOS Settings window backed by SwiftUI's `Settings` scene.
|
|
6
|
+
|
|
7
|
+
## Core patterns
|
|
8
|
+
|
|
9
|
+
- Declare the Settings scene in the `App` and compile it only for macOS.
|
|
10
|
+
- Keep settings content in a dedicated root view (`SettingsView`) and drive values with `@AppStorage`.
|
|
11
|
+
- Use `TabView` to group settings sections when you have more than one category.
|
|
12
|
+
- Use `Form` inside each tab to keep controls aligned and accessible.
|
|
13
|
+
- Use `OpenSettingsAction` or `SettingsLink` for in-app entry points to the Settings window.
|
|
14
|
+
|
|
15
|
+
## Example: settings scene
|
|
16
|
+
|
|
17
|
+
```swift
|
|
18
|
+
@main
|
|
19
|
+
struct MyApp: App {
|
|
20
|
+
var body: some Scene {
|
|
21
|
+
WindowGroup {
|
|
22
|
+
ContentView()
|
|
23
|
+
}
|
|
24
|
+
#if os(macOS)
|
|
25
|
+
Settings {
|
|
26
|
+
SettingsView()
|
|
27
|
+
}
|
|
28
|
+
#endif
|
|
29
|
+
}
|
|
30
|
+
}
|
|
31
|
+
```
|
|
32
|
+
|
|
33
|
+
## Example: tabbed settings view
|
|
34
|
+
|
|
35
|
+
```swift
|
|
36
|
+
@MainActor
|
|
37
|
+
struct SettingsView: View {
|
|
38
|
+
@AppStorage("showPreviews") private var showPreviews = true
|
|
39
|
+
@AppStorage("fontSize") private var fontSize = 12.0
|
|
40
|
+
|
|
41
|
+
var body: some View {
|
|
42
|
+
TabView {
|
|
43
|
+
Form {
|
|
44
|
+
Toggle("Show Previews", isOn: $showPreviews)
|
|
45
|
+
Slider(value: $fontSize, in: 9...96) {
|
|
46
|
+
Text("Font Size (\(fontSize, specifier: "%.0f") pts)")
|
|
47
|
+
}
|
|
48
|
+
}
|
|
49
|
+
.tabItem { Label("General", systemImage: "gear") }
|
|
50
|
+
|
|
51
|
+
Form {
|
|
52
|
+
Toggle("Enable Advanced Mode", isOn: .constant(false))
|
|
53
|
+
}
|
|
54
|
+
.tabItem { Label("Advanced", systemImage: "star") }
|
|
55
|
+
}
|
|
56
|
+
.scenePadding()
|
|
57
|
+
.frame(maxWidth: 420, minHeight: 240)
|
|
58
|
+
}
|
|
59
|
+
}
|
|
60
|
+
```
|
|
61
|
+
|
|
62
|
+
## Skip navigation
|
|
63
|
+
|
|
64
|
+
- Avoid wrapping `SettingsView` in a `NavigationStack` unless you truly need deep push navigation.
|
|
65
|
+
- Prefer tabs or sections; Settings is already presented as a separate window and should feel flat.
|
|
66
|
+
- If you must show hierarchical settings, use a single `NavigationSplitView` with a sidebar list of categories.
|
|
67
|
+
|
|
68
|
+
## Pitfalls
|
|
69
|
+
|
|
70
|
+
- Don’t reuse iOS-only settings layouts (full-screen stacks, toolbar-heavy flows).
|
|
71
|
+
- Avoid large custom view hierarchies inside `Form`; keep rows focused and accessible.
|
|
@@ -0,0 +1,59 @@
|
|
|
1
|
+
# Matched transitions
|
|
2
|
+
|
|
3
|
+
## Intent
|
|
4
|
+
|
|
5
|
+
Use matched transitions to create smooth continuity between a source view (thumbnail, avatar) and a destination view (sheet, detail, viewer).
|
|
6
|
+
|
|
7
|
+
## Core patterns
|
|
8
|
+
|
|
9
|
+
- Use a shared `Namespace` and a stable ID for the source.
|
|
10
|
+
- Use `matchedTransitionSource` + `navigationTransition(.zoom(...))` on iOS 26+.
|
|
11
|
+
- Use `matchedGeometryEffect` for in-place transitions within a view hierarchy.
|
|
12
|
+
- Keep IDs stable across view updates (avoid random UUIDs).
|
|
13
|
+
|
|
14
|
+
## Example: media preview to full-screen viewer (iOS 26+)
|
|
15
|
+
|
|
16
|
+
```swift
|
|
17
|
+
struct MediaPreview: View {
|
|
18
|
+
@Namespace private var namespace
|
|
19
|
+
@State private var selected: MediaAttachment?
|
|
20
|
+
|
|
21
|
+
var body: some View {
|
|
22
|
+
ThumbnailView()
|
|
23
|
+
.matchedTransitionSource(id: selected?.id ?? "", in: namespace)
|
|
24
|
+
.sheet(item: $selected) { item in
|
|
25
|
+
MediaViewer(item: item)
|
|
26
|
+
.navigationTransition(.zoom(sourceID: item.id, in: namespace))
|
|
27
|
+
}
|
|
28
|
+
}
|
|
29
|
+
}
|
|
30
|
+
```
|
|
31
|
+
|
|
32
|
+
## Example: matched geometry within a view
|
|
33
|
+
|
|
34
|
+
```swift
|
|
35
|
+
struct ToggleBadge: View {
|
|
36
|
+
@Namespace private var space
|
|
37
|
+
@State private var isOn = false
|
|
38
|
+
|
|
39
|
+
var body: some View {
|
|
40
|
+
Button {
|
|
41
|
+
withAnimation(.spring) { isOn.toggle() }
|
|
42
|
+
} label: {
|
|
43
|
+
Image(systemName: isOn ? "eye" : "eye.slash")
|
|
44
|
+
.matchedGeometryEffect(id: "icon", in: space)
|
|
45
|
+
}
|
|
46
|
+
}
|
|
47
|
+
}
|
|
48
|
+
```
|
|
49
|
+
|
|
50
|
+
## Design choices to keep
|
|
51
|
+
|
|
52
|
+
- Prefer `matchedTransitionSource` for cross-screen transitions.
|
|
53
|
+
- Keep source and destination sizes reasonable to avoid jarring scale changes.
|
|
54
|
+
- Use `withAnimation` for state-driven transitions.
|
|
55
|
+
|
|
56
|
+
## Pitfalls
|
|
57
|
+
|
|
58
|
+
- Don’t use unstable IDs; it breaks the transition.
|
|
59
|
+
- Avoid mismatched shapes (e.g., square to circle) unless the design expects it.
|
|
@@ -0,0 +1,73 @@
|
|
|
1
|
+
# Media (images, video, viewer)
|
|
2
|
+
|
|
3
|
+
## Intent
|
|
4
|
+
|
|
5
|
+
Use consistent patterns for loading images, previewing media, and presenting a full-screen viewer.
|
|
6
|
+
|
|
7
|
+
## Core patterns
|
|
8
|
+
|
|
9
|
+
- Use `LazyImage` (or `AsyncImage`) for remote images with loading states.
|
|
10
|
+
- Prefer a lightweight preview component for inline media.
|
|
11
|
+
- Use a shared viewer state (e.g., `QuickLook`) to present a full-screen media viewer.
|
|
12
|
+
- Use `openWindow` for desktop/visionOS and a sheet for iOS.
|
|
13
|
+
|
|
14
|
+
## Example: inline media preview
|
|
15
|
+
|
|
16
|
+
```swift
|
|
17
|
+
struct MediaPreviewRow: View {
|
|
18
|
+
@Environment(QuickLook.self) private var quickLook
|
|
19
|
+
|
|
20
|
+
let attachments: [MediaAttachment]
|
|
21
|
+
|
|
22
|
+
var body: some View {
|
|
23
|
+
ScrollView(.horizontal, showsIndicators: false) {
|
|
24
|
+
HStack {
|
|
25
|
+
ForEach(attachments) { attachment in
|
|
26
|
+
LazyImage(url: attachment.previewURL) { state in
|
|
27
|
+
if let image = state.image {
|
|
28
|
+
image.resizable().aspectRatio(contentMode: .fill)
|
|
29
|
+
} else {
|
|
30
|
+
ProgressView()
|
|
31
|
+
}
|
|
32
|
+
}
|
|
33
|
+
.frame(width: 120, height: 120)
|
|
34
|
+
.clipped()
|
|
35
|
+
.onTapGesture {
|
|
36
|
+
quickLook.prepareFor(
|
|
37
|
+
selectedMediaAttachment: attachment,
|
|
38
|
+
mediaAttachments: attachments
|
|
39
|
+
)
|
|
40
|
+
}
|
|
41
|
+
}
|
|
42
|
+
}
|
|
43
|
+
}
|
|
44
|
+
}
|
|
45
|
+
}
|
|
46
|
+
```
|
|
47
|
+
|
|
48
|
+
## Example: global media viewer sheet
|
|
49
|
+
|
|
50
|
+
```swift
|
|
51
|
+
struct AppRoot: View {
|
|
52
|
+
@State private var quickLook = QuickLook.shared
|
|
53
|
+
|
|
54
|
+
var body: some View {
|
|
55
|
+
content
|
|
56
|
+
.environment(quickLook)
|
|
57
|
+
.sheet(item: $quickLook.selectedMediaAttachment) { selected in
|
|
58
|
+
MediaUIView(selectedAttachment: selected, attachments: quickLook.mediaAttachments)
|
|
59
|
+
}
|
|
60
|
+
}
|
|
61
|
+
}
|
|
62
|
+
```
|
|
63
|
+
|
|
64
|
+
## Design choices to keep
|
|
65
|
+
|
|
66
|
+
- Keep previews lightweight; load full media in the viewer.
|
|
67
|
+
- Use shared viewer state so any view can open media without prop-drilling.
|
|
68
|
+
- Use a single entry point for the viewer (sheet/window) to avoid duplicates.
|
|
69
|
+
|
|
70
|
+
## Pitfalls
|
|
71
|
+
|
|
72
|
+
- Avoid loading full-size images in list rows; use resized previews.
|
|
73
|
+
- Don’t present multiple viewer sheets at once; keep a single source of truth.
|
|
@@ -0,0 +1,101 @@
|
|
|
1
|
+
# Menu Bar
|
|
2
|
+
|
|
3
|
+
## Intent
|
|
4
|
+
|
|
5
|
+
Use this when adding or customizing the macOS/iPadOS menu bar with SwiftUI commands.
|
|
6
|
+
|
|
7
|
+
## Core patterns
|
|
8
|
+
|
|
9
|
+
- Add commands at the `Scene` level with `.commands { ... }`.
|
|
10
|
+
- Use `SidebarCommands()` when your UI includes a navigation sidebar.
|
|
11
|
+
- Use `CommandMenu` for app-specific menus and group related actions.
|
|
12
|
+
- Use `CommandGroup` to insert items before/after system groups or replace them.
|
|
13
|
+
- Use `FocusedValue` for context-sensitive menu items that depend on the active scene.
|
|
14
|
+
|
|
15
|
+
## Example: basic command menu
|
|
16
|
+
|
|
17
|
+
```swift
|
|
18
|
+
@main
|
|
19
|
+
struct MyApp: App {
|
|
20
|
+
var body: some Scene {
|
|
21
|
+
WindowGroup {
|
|
22
|
+
ContentView()
|
|
23
|
+
}
|
|
24
|
+
.commands {
|
|
25
|
+
CommandMenu("Actions") {
|
|
26
|
+
Button("Run", action: run)
|
|
27
|
+
.keyboardShortcut("R")
|
|
28
|
+
Button("Stop", action: stop)
|
|
29
|
+
.keyboardShortcut(".")
|
|
30
|
+
}
|
|
31
|
+
}
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
private func run() {}
|
|
35
|
+
private func stop() {}
|
|
36
|
+
}
|
|
37
|
+
```
|
|
38
|
+
|
|
39
|
+
## Example: insert and replace groups
|
|
40
|
+
|
|
41
|
+
```swift
|
|
42
|
+
WindowGroup {
|
|
43
|
+
ContentView()
|
|
44
|
+
}
|
|
45
|
+
.commands {
|
|
46
|
+
CommandGroup(before: .systemServices) {
|
|
47
|
+
Button("Check for Updates") { /* open updater */ }
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
CommandGroup(after: .newItem) {
|
|
51
|
+
Button("New from Clipboard") { /* create item */ }
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
CommandGroup(replacing: .help) {
|
|
55
|
+
Button("User Manual") { /* open docs */ }
|
|
56
|
+
}
|
|
57
|
+
}
|
|
58
|
+
```
|
|
59
|
+
|
|
60
|
+
## Example: focused menu state
|
|
61
|
+
|
|
62
|
+
```swift
|
|
63
|
+
@Observable
|
|
64
|
+
final class DataModel {
|
|
65
|
+
var items: [String] = []
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
struct ContentView: View {
|
|
69
|
+
@State private var model = DataModel()
|
|
70
|
+
|
|
71
|
+
var body: some View {
|
|
72
|
+
List(model.items, id: \.self) { item in
|
|
73
|
+
Text(item)
|
|
74
|
+
}
|
|
75
|
+
.focusedSceneValue(model)
|
|
76
|
+
}
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
struct ItemCommands: Commands {
|
|
80
|
+
@FocusedValue(DataModel.self) private var model: DataModel?
|
|
81
|
+
|
|
82
|
+
var body: some Commands {
|
|
83
|
+
CommandGroup(after: .newItem) {
|
|
84
|
+
Button("New Item") {
|
|
85
|
+
model?.items.append("Untitled")
|
|
86
|
+
}
|
|
87
|
+
.disabled(model == nil)
|
|
88
|
+
}
|
|
89
|
+
}
|
|
90
|
+
}
|
|
91
|
+
```
|
|
92
|
+
|
|
93
|
+
## Menu bar and Settings
|
|
94
|
+
|
|
95
|
+
- Defining a `Settings` scene adds the Settings menu item on macOS automatically.
|
|
96
|
+
- If you need a custom entry point inside the app, use `OpenSettingsAction` or `SettingsLink`.
|
|
97
|
+
|
|
98
|
+
## Pitfalls
|
|
99
|
+
|
|
100
|
+
- Avoid registering the same keyboard shortcut in multiple command groups.
|
|
101
|
+
- Don’t use menu items as the only discoverable entry point for critical features.
|
|
@@ -0,0 +1,159 @@
|
|
|
1
|
+
# NavigationStack
|
|
2
|
+
|
|
3
|
+
## Intent
|
|
4
|
+
|
|
5
|
+
Use this pattern for programmatic navigation and deep links, especially when each tab needs an independent navigation history. The key idea is one `NavigationStack` per tab, each with its own path binding and router object.
|
|
6
|
+
|
|
7
|
+
## Core architecture
|
|
8
|
+
|
|
9
|
+
- Define a route enum that is `Hashable` and represents all destinations.
|
|
10
|
+
- Create a lightweight router (or use a library such as `https://github.com/Dimillian/AppRouter`) that owns the `path` and any sheet state.
|
|
11
|
+
- Each tab owns its own router instance and binds `NavigationStack(path:)` to it.
|
|
12
|
+
- Inject the router into the environment so child views can navigate programmatically.
|
|
13
|
+
- Centralize destination mapping with a single `navigationDestination(for:)` block (or a `withAppRouter()` modifier).
|
|
14
|
+
|
|
15
|
+
## Example: custom router with per-tab stack
|
|
16
|
+
|
|
17
|
+
```swift
|
|
18
|
+
@MainActor
|
|
19
|
+
@Observable
|
|
20
|
+
final class RouterPath {
|
|
21
|
+
var path: [Route] = []
|
|
22
|
+
var presentedSheet: SheetDestination?
|
|
23
|
+
|
|
24
|
+
func navigate(to route: Route) {
|
|
25
|
+
path.append(route)
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
func reset() {
|
|
29
|
+
path = []
|
|
30
|
+
}
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
enum Route: Hashable {
|
|
34
|
+
case account(id: String)
|
|
35
|
+
case status(id: String)
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
@MainActor
|
|
39
|
+
struct TimelineTab: View {
|
|
40
|
+
@State private var routerPath = RouterPath()
|
|
41
|
+
|
|
42
|
+
var body: some View {
|
|
43
|
+
NavigationStack(path: $routerPath.path) {
|
|
44
|
+
TimelineView()
|
|
45
|
+
.navigationDestination(for: Route.self) { route in
|
|
46
|
+
switch route {
|
|
47
|
+
case .account(let id): AccountView(id: id)
|
|
48
|
+
case .status(let id): StatusView(id: id)
|
|
49
|
+
}
|
|
50
|
+
}
|
|
51
|
+
}
|
|
52
|
+
.environment(routerPath)
|
|
53
|
+
}
|
|
54
|
+
}
|
|
55
|
+
```
|
|
56
|
+
|
|
57
|
+
## Example: centralized destination mapping
|
|
58
|
+
|
|
59
|
+
Use a shared view modifier to avoid duplicating route switches across screens.
|
|
60
|
+
|
|
61
|
+
```swift
|
|
62
|
+
extension View {
|
|
63
|
+
func withAppRouter() -> some View {
|
|
64
|
+
navigationDestination(for: Route.self) { route in
|
|
65
|
+
switch route {
|
|
66
|
+
case .account(let id):
|
|
67
|
+
AccountView(id: id)
|
|
68
|
+
case .status(let id):
|
|
69
|
+
StatusView(id: id)
|
|
70
|
+
}
|
|
71
|
+
}
|
|
72
|
+
}
|
|
73
|
+
}
|
|
74
|
+
```
|
|
75
|
+
|
|
76
|
+
Then apply it once per stack:
|
|
77
|
+
|
|
78
|
+
```swift
|
|
79
|
+
NavigationStack(path: $routerPath.path) {
|
|
80
|
+
TimelineView()
|
|
81
|
+
.withAppRouter()
|
|
82
|
+
}
|
|
83
|
+
```
|
|
84
|
+
|
|
85
|
+
## Example: binding per tab (tabs with independent history)
|
|
86
|
+
|
|
87
|
+
```swift
|
|
88
|
+
@MainActor
|
|
89
|
+
struct TabsView: View {
|
|
90
|
+
@State private var timelineRouter = RouterPath()
|
|
91
|
+
@State private var notificationsRouter = RouterPath()
|
|
92
|
+
|
|
93
|
+
var body: some View {
|
|
94
|
+
TabView {
|
|
95
|
+
TimelineTab(router: timelineRouter)
|
|
96
|
+
NotificationsTab(router: notificationsRouter)
|
|
97
|
+
}
|
|
98
|
+
}
|
|
99
|
+
}
|
|
100
|
+
```
|
|
101
|
+
|
|
102
|
+
## Example: generic tabs with per-tab NavigationStack
|
|
103
|
+
|
|
104
|
+
Use this when tabs are built from data and each needs its own path without hard-coded names.
|
|
105
|
+
|
|
106
|
+
```swift
|
|
107
|
+
@MainActor
|
|
108
|
+
struct TabsView: View {
|
|
109
|
+
@State private var selectedTab: AppTab = .timeline
|
|
110
|
+
@State private var tabRouter = TabRouter()
|
|
111
|
+
|
|
112
|
+
var body: some View {
|
|
113
|
+
TabView(selection: $selectedTab) {
|
|
114
|
+
ForEach(AppTab.allCases) { tab in
|
|
115
|
+
NavigationStack(path: tabRouter.binding(for: tab)) {
|
|
116
|
+
tab.makeContentView()
|
|
117
|
+
}
|
|
118
|
+
.environment(tabRouter.router(for: tab))
|
|
119
|
+
.tabItem { tab.label }
|
|
120
|
+
.tag(tab)
|
|
121
|
+
}
|
|
122
|
+
}
|
|
123
|
+
}
|
|
124
|
+
}
|
|
125
|
+
```
|
|
126
|
+
|
|
127
|
+
@MainActor
|
|
128
|
+
@Observable
|
|
129
|
+
final class TabRouter {
|
|
130
|
+
private var routers: [AppTab: RouterPath] = [:]
|
|
131
|
+
|
|
132
|
+
func router(for tab: AppTab) -> RouterPath {
|
|
133
|
+
if let router = routers[tab] { return router }
|
|
134
|
+
let router = RouterPath()
|
|
135
|
+
routers[tab] = router
|
|
136
|
+
return router
|
|
137
|
+
}
|
|
138
|
+
|
|
139
|
+
func binding(for tab: AppTab) -> Binding<[Route]> {
|
|
140
|
+
let router = router(for: tab)
|
|
141
|
+
return Binding(get: { router.path }, set: { router.path = $0 })
|
|
142
|
+
}
|
|
143
|
+
}
|
|
144
|
+
|
|
145
|
+
## Design choices to keep
|
|
146
|
+
|
|
147
|
+
- One `NavigationStack` per tab to preserve independent history.
|
|
148
|
+
- A single source of truth for navigation state (`RouterPath` or library router).
|
|
149
|
+
- Use `navigationDestination(for:)` to map routes to views.
|
|
150
|
+
- Reset the path when app context changes (account switch, logout, etc.).
|
|
151
|
+
- Inject the router into the environment so child views can navigate and present sheets without prop-drilling.
|
|
152
|
+
- Keep sheet presentation state on the router if you want a single place to manage modals.
|
|
153
|
+
|
|
154
|
+
## Pitfalls
|
|
155
|
+
|
|
156
|
+
- Do not share one path across all tabs unless you want global history.
|
|
157
|
+
- Ensure route identifiers are stable and `Hashable`.
|
|
158
|
+
- Avoid storing view instances in the path; store lightweight route data instead.
|
|
159
|
+
- If using a router object, keep it outside other `@Observable` objects to avoid nested observation.
|