@mmerterden/multi-agent-pipeline 20.7.0 → 20.8.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +9 -0
- package/LICENSE +0 -10
- package/docs/facts.json +1 -1
- package/manifest.json +266 -267
- package/package.json +2 -2
- package/pipeline/scripts/_notices.mjs +1 -1
- package/pipeline/skills/.skill-manifest.json +68 -68
- package/pipeline/skills/shared/README.md +70 -70
- package/pipeline/skills/shared/external/alarmkit/SKILL.md +373 -381
- package/pipeline/skills/shared/external/alarmkit/evals/evals.json +23 -18
- package/pipeline/skills/shared/external/alarmkit/references/alarmkit-patterns.md +328 -378
- package/pipeline/skills/shared/external/app-clips/SKILL.md +260 -160
- package/pipeline/skills/shared/external/app-clips/evals/evals.json +27 -27
- package/pipeline/skills/shared/external/app-clips/references/data-handoff-notifications-location.md +150 -83
- package/pipeline/skills/shared/external/app-clips/references/routing-and-experiences.md +135 -83
- package/pipeline/skills/shared/external/app-clips/references/size-capabilities-and-promotion.md +143 -85
- package/pipeline/skills/shared/external/app-intents/SKILL.md +302 -304
- package/pipeline/skills/shared/external/app-intents/evals/evals.json +21 -21
- package/pipeline/skills/shared/external/app-intents/references/appintents-advanced.md +594 -894
- package/pipeline/skills/shared/external/app-store-optimization/SKILL.md +339 -277
- package/pipeline/skills/shared/external/app-store-optimization/evals/evals.json +27 -23
- package/pipeline/skills/shared/external/app-store-optimization/references/keyword-research-methodology.md +105 -122
- package/pipeline/skills/shared/external/app-store-optimization/references/product-page-variants.md +143 -166
- package/pipeline/skills/shared/external/app-store-review/SKILL.md +307 -326
- package/pipeline/skills/shared/external/app-store-review/evals/evals.json +21 -21
- package/pipeline/skills/shared/external/app-store-review/references/privacy-manifest.md +105 -67
- package/pipeline/skills/shared/external/app-store-review/references/review-checklists.md +114 -101
- package/pipeline/skills/shared/external/apple-on-device-ai/SKILL.md +333 -360
- package/pipeline/skills/shared/external/apple-on-device-ai/evals/evals.json +24 -27
- package/pipeline/skills/shared/external/apple-on-device-ai/references/coreml-conversion.md +215 -322
- package/pipeline/skills/shared/external/apple-on-device-ai/references/coreml-optimization.md +161 -256
- package/pipeline/skills/shared/external/apple-on-device-ai/references/foundation-models.md +277 -387
- package/pipeline/skills/shared/external/apple-on-device-ai/references/mlx-swift.md +196 -210
- package/pipeline/skills/shared/external/authentication/SKILL.md +265 -381
- package/pipeline/skills/shared/external/authentication/evals/evals.json +25 -25
- package/pipeline/skills/shared/external/authentication/references/keychain-biometric.md +133 -178
- package/pipeline/skills/shared/external/authentication/references/passkeys.md +111 -147
- package/pipeline/skills/shared/external/avkit/SKILL.md +267 -364
- package/pipeline/skills/shared/external/avkit/evals/evals.json +26 -26
- package/pipeline/skills/shared/external/avkit/references/avkit-patterns.md +375 -493
- package/pipeline/skills/shared/external/background-processing/SKILL.md +270 -382
- package/pipeline/skills/shared/external/background-processing/evals/evals.json +22 -22
- package/pipeline/skills/shared/external/background-processing/references/background-task-patterns.md +169 -317
- package/pipeline/skills/shared/external/callkit-voip/SKILL.md +290 -371
- package/pipeline/skills/shared/external/callkit-voip/evals/evals.json +24 -24
- package/pipeline/skills/shared/external/callkit-voip/references/callkit-patterns.md +175 -343
- package/pipeline/skills/shared/external/cloudkit-sync/SKILL.md +292 -381
- package/pipeline/skills/shared/external/cloudkit-sync/evals/evals.json +33 -30
- package/pipeline/skills/shared/external/cloudkit-sync/references/cloudkit-patterns.md +227 -355
- package/pipeline/skills/shared/external/contacts-framework/SKILL.md +197 -346
- package/pipeline/skills/shared/external/contacts-framework/evals/evals.json +19 -21
- package/pipeline/skills/shared/external/contacts-framework/references/contacts-patterns.md +169 -308
- package/pipeline/skills/shared/external/core-bluetooth/SKILL.md +226 -376
- package/pipeline/skills/shared/external/core-bluetooth/evals/evals.json +25 -22
- package/pipeline/skills/shared/external/core-bluetooth/references/ble-patterns.md +257 -337
- package/pipeline/skills/shared/external/core-data/SKILL.md +292 -368
- package/pipeline/skills/shared/external/core-data/evals/evals.json +30 -27
- package/pipeline/skills/shared/external/core-motion/SKILL.md +235 -324
- package/pipeline/skills/shared/external/core-motion/evals/evals.json +31 -27
- package/pipeline/skills/shared/external/core-motion/references/motion-patterns.md +210 -310
- package/pipeline/skills/shared/external/core-nfc/SKILL.md +292 -366
- package/pipeline/skills/shared/external/core-nfc/evals/evals.json +22 -24
- package/pipeline/skills/shared/external/core-nfc/references/nfc-patterns.md +233 -329
- package/pipeline/skills/shared/external/coreml/SKILL.md +323 -367
- package/pipeline/skills/shared/external/coreml/evals/evals.json +24 -21
- package/pipeline/skills/shared/external/coreml/references/coreml-swift-integration.md +562 -565
- package/pipeline/skills/shared/external/cryptokit/SKILL.md +253 -394
- package/pipeline/skills/shared/external/cryptokit/evals/evals.json +20 -18
- package/pipeline/skills/shared/external/cryptokit/references/cryptokit-patterns.md +299 -488
- package/pipeline/skills/shared/external/debugging-instruments/SKILL.md +270 -323
- package/pipeline/skills/shared/external/debugging-instruments/evals/evals.json +27 -30
- package/pipeline/skills/shared/external/debugging-instruments/references/instruments-guide.md +167 -315
- package/pipeline/skills/shared/external/debugging-instruments/references/lldb-patterns.md +140 -193
- package/pipeline/skills/shared/external/device-integrity/SKILL.md +230 -353
- package/pipeline/skills/shared/external/device-integrity/evals/evals.json +25 -21
- package/pipeline/skills/shared/external/device-integrity/references/device-integrity-patterns.md +159 -197
- package/pipeline/skills/shared/external/energykit/SKILL.md +225 -392
- package/pipeline/skills/shared/external/energykit/evals/evals.json +29 -28
- package/pipeline/skills/shared/external/energykit/references/energykit-patterns.md +174 -470
- package/pipeline/skills/shared/external/eventkit-calendar/SKILL.md +261 -383
- package/pipeline/skills/shared/external/eventkit-calendar/evals/evals.json +25 -22
- package/pipeline/skills/shared/external/eventkit-calendar/references/eventkit-patterns.md +165 -268
- package/pipeline/skills/shared/external/healthkit/SKILL.md +252 -303
- package/pipeline/skills/shared/external/healthkit/evals/evals.json +24 -23
- package/pipeline/skills/shared/external/healthkit/references/healthkit-patterns.md +369 -523
- package/pipeline/skills/shared/external/homekit-matter/SKILL.md +233 -348
- package/pipeline/skills/shared/external/homekit-matter/evals/evals.json +27 -22
- package/pipeline/skills/shared/external/homekit-matter/references/matter-commissioning.md +199 -305
- package/pipeline/skills/shared/external/ios-accessibility/SKILL.md +368 -340
- package/pipeline/skills/shared/external/ios-accessibility/evals/evals.json +28 -27
- package/pipeline/skills/shared/external/ios-accessibility/references/a11y-patterns.md +314 -260
- package/pipeline/skills/shared/external/ios-accessibility/references/media-accessibility.md +97 -67
- package/pipeline/skills/shared/external/ios-accessibility/references/nutrition-labels.md +165 -101
- package/pipeline/skills/shared/external/ios-localization/SKILL.md +258 -371
- package/pipeline/skills/shared/external/ios-localization/evals/evals.json +23 -23
- package/pipeline/skills/shared/external/ios-localization/references/formatstyle-locale.md +283 -491
- package/pipeline/skills/shared/external/ios-localization/references/string-catalogs.md +313 -440
- package/pipeline/skills/shared/external/ios-networking/SKILL.md +265 -341
- package/pipeline/skills/shared/external/ios-networking/evals/evals.json +24 -24
- package/pipeline/skills/shared/external/ios-networking/references/background-websocket.md +425 -652
- package/pipeline/skills/shared/external/ios-networking/references/file-storage-patterns.md +143 -285
- package/pipeline/skills/shared/external/ios-networking/references/lightweight-clients.md +93 -53
- package/pipeline/skills/shared/external/ios-networking/references/network-framework.md +231 -456
- package/pipeline/skills/shared/external/ios-networking/references/urlsession-patterns.md +517 -784
- package/pipeline/skills/shared/external/ios-simulator/SKILL.md +265 -393
- package/pipeline/skills/shared/external/ios-simulator/evals/evals.json +21 -21
- package/pipeline/skills/shared/external/ios-simulator/references/simctl-commands.md +177 -270
- package/pipeline/skills/shared/external/live-activities/SKILL.md +318 -360
- package/pipeline/skills/shared/external/live-activities/evals/evals.json +21 -21
- package/pipeline/skills/shared/external/live-activities/references/activitykit-patterns.md +478 -710
- package/pipeline/skills/shared/external/mapkit-location/SKILL.md +295 -267
- package/pipeline/skills/shared/external/mapkit-location/evals/evals.json +28 -24
- package/pipeline/skills/shared/external/mapkit-location/references/mapkit-corelocation-patterns.md +378 -532
- package/pipeline/skills/shared/external/mapkit-location/references/mapkit-patterns.md +397 -499
- package/pipeline/skills/shared/external/metrickit-diagnostics/SKILL.md +165 -348
- package/pipeline/skills/shared/external/metrickit-diagnostics/evals/evals.json +26 -23
- package/pipeline/skills/shared/external/metrickit-diagnostics/references/metrickit-patterns.md +123 -130
- package/pipeline/skills/shared/external/musickit-audio/SKILL.md +189 -315
- package/pipeline/skills/shared/external/musickit-audio/evals/evals.json +22 -21
- package/pipeline/skills/shared/external/musickit-audio/references/musickit-patterns.md +181 -270
- package/pipeline/skills/shared/external/natural-language/SKILL.md +188 -340
- package/pipeline/skills/shared/external/natural-language/evals/evals.json +21 -21
- package/pipeline/skills/shared/external/natural-language/references/translation-patterns.md +171 -225
- package/pipeline/skills/shared/external/passkit-wallet/SKILL.md +258 -392
- package/pipeline/skills/shared/external/passkit-wallet/evals/evals.json +30 -29
- package/pipeline/skills/shared/external/passkit-wallet/references/wallet-passes.md +164 -231
- package/pipeline/skills/shared/external/pdfkit/SKILL.md +312 -344
- package/pipeline/skills/shared/external/pdfkit/evals/evals.json +19 -19
- package/pipeline/skills/shared/external/pdfkit/references/pdfkit-patterns.md +413 -624
- package/pipeline/skills/shared/external/pencilkit-drawing/SKILL.md +242 -358
- package/pipeline/skills/shared/external/pencilkit-drawing/evals/evals.json +25 -21
- package/pipeline/skills/shared/external/pencilkit-drawing/references/pencilkit-patterns.md +161 -226
- package/pipeline/skills/shared/external/permissionkit/SKILL.md +282 -400
- package/pipeline/skills/shared/external/permissionkit/evals/evals.json +27 -30
- package/pipeline/skills/shared/external/permissionkit/references/permissionkit-patterns.md +237 -350
- package/pipeline/skills/shared/external/photos-camera-media/SKILL.md +276 -325
- package/pipeline/skills/shared/external/photos-camera-media/references/av-playback.md +299 -545
- package/pipeline/skills/shared/external/photos-camera-media/references/camera-capture.md +344 -588
- package/pipeline/skills/shared/external/photos-camera-media/references/image-loading-caching.md +316 -660
- package/pipeline/skills/shared/external/photos-camera-media/references/photokit-patterns.md +270 -416
- package/pipeline/skills/shared/external/push-notifications/SKILL.md +312 -340
- package/pipeline/skills/shared/external/push-notifications/evals/evals.json +27 -26
- package/pipeline/skills/shared/external/push-notifications/references/notification-patterns.md +328 -485
- package/pipeline/skills/shared/external/push-notifications/references/rich-notifications.md +327 -560
- package/pipeline/skills/shared/external/realitykit-ar/SKILL.md +218 -410
- package/pipeline/skills/shared/external/realitykit-ar/evals/evals.json +24 -27
- package/pipeline/skills/shared/external/realitykit-ar/references/realitykit-patterns.md +221 -348
- package/pipeline/skills/shared/external/shareplay-activities/SKILL.md +222 -393
- package/pipeline/skills/shared/external/shareplay-activities/evals/evals.json +23 -24
- package/pipeline/skills/shared/external/shareplay-activities/references/shareplay-patterns.md +280 -420
- package/pipeline/skills/shared/external/speech-recognition/SKILL.md +217 -421
- package/pipeline/skills/shared/external/speech-recognition/evals/evals.json +23 -26
- package/pipeline/skills/shared/external/speech-recognition/references/speechanalyzer-patterns.md +133 -125
- package/pipeline/skills/shared/external/storekit/SKILL.md +228 -204
- package/pipeline/skills/shared/external/storekit/evals/evals.json +27 -24
- package/pipeline/skills/shared/external/storekit/references/app-review-guidelines.md +98 -109
- package/pipeline/skills/shared/external/storekit/references/core-patterns.md +298 -242
- package/pipeline/skills/shared/external/storekit/references/storekit-advanced.md +356 -649
- package/pipeline/skills/shared/external/swift-api-design-guidelines/SKILL.md +274 -399
- package/pipeline/skills/shared/external/swift-api-design-guidelines/evals/evals.json +22 -24
- package/pipeline/skills/shared/external/swift-api-design-guidelines/references/argument-labels-and-parameters.md +107 -108
- package/pipeline/skills/shared/external/swift-api-design-guidelines/references/conventions-and-special-rules.md +93 -165
- package/pipeline/skills/shared/external/swift-api-design-guidelines/references/naming-and-clarity.md +99 -137
- package/pipeline/skills/shared/external/swift-api-design-guidelines/references/side-effects-and-mutating-pairs.md +77 -120
- package/pipeline/skills/shared/external/swift-architecture/SKILL.md +334 -350
- package/pipeline/skills/shared/external/swift-architecture/evals/evals.json +22 -22
- package/pipeline/skills/shared/external/swift-charts/SKILL.md +208 -394
- package/pipeline/skills/shared/external/swift-charts/evals/evals.json +27 -30
- package/pipeline/skills/shared/external/swift-charts/references/charts-patterns.md +351 -762
- package/pipeline/skills/shared/external/swift-codable/SKILL.md +339 -343
- package/pipeline/skills/shared/external/swift-codable/evals/evals.json +20 -20
- package/pipeline/skills/shared/external/swift-concurrency/SKILL.md +303 -351
- package/pipeline/skills/shared/external/swift-concurrency/evals/evals.json +27 -24
- package/pipeline/skills/shared/external/swift-concurrency/references/approachable-concurrency.md +65 -80
- package/pipeline/skills/shared/external/swift-concurrency/references/async-algorithms.md +48 -84
- package/pipeline/skills/shared/external/swift-concurrency/references/bridging-interop.md +134 -79
- package/pipeline/skills/shared/external/swift-concurrency/references/concurrency-patterns.md +145 -167
- package/pipeline/skills/shared/external/swift-concurrency/references/diagnostics.md +62 -50
- package/pipeline/skills/shared/external/swift-concurrency/references/swiftui-concurrency.md +92 -121
- package/pipeline/skills/shared/external/swift-concurrency/references/synchronization-primitives.md +177 -241
- package/pipeline/skills/shared/external/swift-formatstyle/SKILL.md +258 -234
- package/pipeline/skills/shared/external/swift-language/SKILL.md +342 -382
- package/pipeline/skills/shared/external/swift-language/evals/evals.json +24 -27
- package/pipeline/skills/shared/external/swift-language/references/swift-attributes-interop.md +79 -56
- package/pipeline/skills/shared/external/swift-language/references/swift-patterns-extended.md +297 -340
- package/pipeline/skills/shared/external/swift-security/SKILL.md +180 -161
- package/pipeline/skills/shared/external/swift-security/evals/evals.json +25 -25
- package/pipeline/skills/shared/external/swift-security/references/biometric-authentication.md +314 -469
- package/pipeline/skills/shared/external/swift-security/references/certificate-trust.md +408 -476
- package/pipeline/skills/shared/external/swift-security/references/common-anti-patterns.md +260 -530
- package/pipeline/skills/shared/external/swift-security/references/compliance-owasp-mapping.md +270 -477
- package/pipeline/skills/shared/external/swift-security/references/credential-storage-patterns.md +573 -571
- package/pipeline/skills/shared/external/swift-security/references/cryptokit-public-key.md +370 -441
- package/pipeline/skills/shared/external/swift-security/references/cryptokit-symmetric.md +332 -433
- package/pipeline/skills/shared/external/swift-security/references/keychain-access-control.md +346 -468
- package/pipeline/skills/shared/external/swift-security/references/keychain-fundamentals.md +352 -472
- package/pipeline/skills/shared/external/swift-security/references/keychain-item-classes.md +431 -432
- package/pipeline/skills/shared/external/swift-security/references/keychain-sharing.md +328 -425
- package/pipeline/skills/shared/external/swift-security/references/migration-legacy-stores.md +341 -579
- package/pipeline/skills/shared/external/swift-security/references/secure-enclave.md +396 -457
- package/pipeline/skills/shared/external/swift-security/references/testing-security-code.md +354 -614
- package/pipeline/skills/shared/external/swift-testing/SKILL.md +188 -175
- package/pipeline/skills/shared/external/swift-testing/evals/evals.json +26 -24
- package/pipeline/skills/shared/external/swift-testing/references/testing-advanced.md +80 -84
- package/pipeline/skills/shared/external/swift-testing/references/testing-patterns.md +317 -433
- package/pipeline/skills/shared/external/swiftdata/SKILL.md +392 -256
- package/pipeline/skills/shared/external/swiftdata/evals/evals.json +24 -24
- package/pipeline/skills/shared/external/swiftdata/references/core-data-coexistence.md +206 -402
- package/pipeline/skills/shared/external/swiftdata/references/indexing.md +59 -52
- package/pipeline/skills/shared/external/swiftdata/references/predicate-pitfalls.md +57 -33
- package/pipeline/skills/shared/external/swiftdata/references/swiftdata-advanced.md +354 -747
- package/pipeline/skills/shared/external/swiftdata/references/swiftdata-queries.md +300 -508
- package/pipeline/skills/shared/external/swiftlint/SKILL.md +175 -226
- package/pipeline/skills/shared/external/swiftlint/references/adoption-and-configuration.md +141 -208
- package/pipeline/skills/shared/external/swiftlint/references/custom-rules-and-analyze.md +100 -109
- package/pipeline/skills/shared/external/swiftlint/references/plugins-run-scripts-and-integrations.md +159 -179
- package/pipeline/skills/shared/external/swiftlint/references/rule-reference.md +383 -18
- package/pipeline/skills/shared/external/swiftlint/references/rules-suppressions-and-baselines.md +143 -229
- package/pipeline/skills/shared/external/swiftui-animation/SKILL.md +283 -366
- package/pipeline/skills/shared/external/swiftui-animation/references/animation-advanced.md +396 -608
- package/pipeline/skills/shared/external/swiftui-animation/references/core-animation-bridge.md +336 -385
- package/pipeline/skills/shared/external/swiftui-gestures/SKILL.md +239 -349
- package/pipeline/skills/shared/external/swiftui-gestures/references/gesture-patterns.md +228 -310
- package/pipeline/skills/shared/external/swiftui-layout-components/SKILL.md +260 -249
- package/pipeline/skills/shared/external/swiftui-layout-components/references/form.md +92 -74
- package/pipeline/skills/shared/external/swiftui-layout-components/references/grids.md +112 -177
- package/pipeline/skills/shared/external/swiftui-layout-components/references/list.md +61 -64
- package/pipeline/skills/shared/external/swiftui-layout-components/references/scrollview.md +94 -134
- package/pipeline/skills/shared/external/swiftui-liquid-glass/SKILL.md +193 -225
- package/pipeline/skills/shared/external/swiftui-liquid-glass/references/liquid-glass.md +173 -327
- package/pipeline/skills/shared/external/swiftui-navigation/SKILL.md +193 -168
- package/pipeline/skills/shared/external/swiftui-navigation/references/deeplinks.md +127 -150
- package/pipeline/skills/shared/external/swiftui-navigation/references/navigationstack.md +132 -133
- package/pipeline/skills/shared/external/swiftui-navigation/references/sheets.md +152 -117
- package/pipeline/skills/shared/external/swiftui-navigation/references/tabview.md +106 -140
- package/pipeline/skills/shared/external/swiftui-patterns/SKILL.md +316 -252
- package/pipeline/skills/shared/external/swiftui-patterns/references/architecture-patterns.md +341 -332
- package/pipeline/skills/shared/external/swiftui-patterns/references/deprecated-migration.md +547 -854
- package/pipeline/skills/shared/external/swiftui-patterns/references/design-polish.md +485 -537
- package/pipeline/skills/shared/external/swiftui-patterns/references/platform-and-sharing.md +417 -499
- package/pipeline/skills/shared/external/swiftui-performance/SKILL.md +213 -376
- package/pipeline/skills/shared/external/swiftui-performance/references/demystify-swiftui-performance-wwdc23.md +86 -175
- package/pipeline/skills/shared/external/swiftui-performance/references/optimizing-swiftui-performance-instruments.md +89 -195
- package/pipeline/skills/shared/external/swiftui-performance/references/understanding-hangs-in-your-app.md +95 -182
- package/pipeline/skills/shared/external/swiftui-performance/references/understanding-improving-swiftui-performance.md +71 -149
- package/pipeline/skills/shared/external/swiftui-performance/references/wwdc-session-sources.md +21 -27
- package/pipeline/skills/shared/external/swiftui-uikit-interop/SKILL.md +303 -295
- package/pipeline/skills/shared/external/swiftui-uikit-interop/references/hosting-migration.md +204 -387
- package/pipeline/skills/shared/external/swiftui-uikit-interop/references/representable-recipes.md +469 -683
- package/pipeline/skills/shared/external/swiftui-webkit/SKILL.md +140 -186
- package/pipeline/skills/shared/external/swiftui-webkit/references/loading-and-observation.md +75 -86
- package/pipeline/skills/shared/external/swiftui-webkit/references/local-content-and-custom-schemes.md +63 -60
- package/pipeline/skills/shared/external/swiftui-webkit/references/migration-and-fallbacks.md +69 -137
- package/pipeline/skills/shared/external/swiftui-webkit/references/navigation-and-javascript.md +95 -67
- package/pipeline/skills/shared/external/tipkit/SKILL.md +220 -335
- package/pipeline/skills/shared/external/tipkit/references/tipkit-patterns.md +356 -494
- package/pipeline/skills/shared/external/vision-framework/SKILL.md +260 -375
- package/pipeline/skills/shared/external/vision-framework/references/vision-requests.md +393 -515
- package/pipeline/skills/shared/external/vision-framework/references/visionkit-scanner.md +363 -539
- package/pipeline/skills/shared/external/weatherkit/SKILL.md +152 -310
- package/pipeline/skills/shared/external/weatherkit/references/weatherkit-patterns.md +288 -407
- package/pipeline/skills/shared/external/widgetkit/SKILL.md +216 -288
- package/pipeline/skills/shared/external/widgetkit/references/widgetkit-advanced.md +414 -719
- package/pipeline/skills/shared/external/NOTICE-swift-ios-skills.md +0 -39
package/pipeline/skills/shared/external/swiftui-patterns/references/architecture-patterns.md
CHANGED
|
@@ -1,145 +1,144 @@
|
|
|
1
1
|
# Architecture Patterns
|
|
2
2
|
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
- [App Wiring and Dependency Graph](#app-wiring-and-dependency-graph)
|
|
6
|
-
- [Lightweight Clients](#lightweight-clients)
|
|
7
|
-
|
|
8
|
-
## MV Patterns
|
|
9
|
-
|
|
10
|
-
Default to Model-View (MV) in SwiftUI. Views are lightweight state expressions; models and services own business logic. Do not introduce view models unless the existing code already requires them.
|
|
11
|
-
|
|
12
|
-
### Contents
|
|
13
|
-
|
|
14
|
-
- [Core Principles](#core-principles)
|
|
15
|
-
- [Why Not MVVM](#why-not-mvvm)
|
|
16
|
-
- [MV Pattern in Practice](#mv-pattern-in-practice)
|
|
17
|
-
- [When a ViewModel Already Exists](#when-a-viewmodel-already-exists)
|
|
18
|
-
- [When a New ViewModel Is Justified](#when-a-new-viewmodel-is-justified)
|
|
19
|
-
- [Environment vs. Initializer Injection](#environment-vs-initializer-injection)
|
|
20
|
-
- [Testing Strategy](#testing-strategy)
|
|
21
|
-
- [Source](#source)
|
|
22
|
-
|
|
23
|
-
### Core Principles
|
|
24
|
-
|
|
25
|
-
- Views orchestrate UI flow using `@State`, `@Environment`, `@Query`, `.task`, and `.onChange`
|
|
26
|
-
- Services and shared models live in the environment, are testable in isolation, and encapsulate complexity
|
|
27
|
-
- Split large views into smaller subviews rather than introducing a view model
|
|
28
|
-
- Test models, services, and business logic; views should stay simple and declarative
|
|
29
|
-
|
|
30
|
-
### Why Not MVVM
|
|
3
|
+
The reasoning behind the Model-View default, the cases where a view model still
|
|
4
|
+
earns its place, and a reference wiring for the app shell.
|
|
31
5
|
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
Every ViewModel adds:
|
|
35
|
-
- More complexity and objects to synchronize
|
|
36
|
-
- More indirection and cognitive overhead
|
|
37
|
-
- Manual data fetching that duplicates SwiftUI/SwiftData mechanisms
|
|
38
|
-
|
|
39
|
-
### MV Pattern in Practice
|
|
6
|
+
## Contents
|
|
40
7
|
|
|
41
|
-
|
|
8
|
+
1. [MV Principles](#1-mv-principles)
|
|
9
|
+
2. [Why Not MVVM](#2-why-not-mvvm)
|
|
10
|
+
3. [MV in Practice](#3-mv-in-practice)
|
|
11
|
+
4. [Working With an Existing View Model](#4-working-with-an-existing-view-model)
|
|
12
|
+
5. [When a New View Model Is Justified](#5-when-a-new-view-model-is-justified)
|
|
13
|
+
6. [Environment or Initializer Injection](#6-environment-or-initializer-injection)
|
|
14
|
+
7. [Testing Strategy](#7-testing-strategy)
|
|
15
|
+
8. [App Shell and Dependency Graph](#8-app-shell-and-dependency-graph)
|
|
16
|
+
9. [Lightweight Clients](#9-lightweight-clients)
|
|
17
|
+
10. [Further Reading](#10-further-reading)
|
|
18
|
+
|
|
19
|
+
## 1. MV Principles
|
|
20
|
+
|
|
21
|
+
- Views run the UI flow with `@State`, `@Environment`, `@Query`, `.task` and
|
|
22
|
+
`.onChange`.
|
|
23
|
+
- Services and shared models live in the environment. Each one can be tested
|
|
24
|
+
on its own and keeps its complexity behind a small surface.
|
|
25
|
+
- When a view gets big, extract subviews first. Add a view model only when the
|
|
26
|
+
existing code already depends on one.
|
|
27
|
+
|
|
28
|
+
## 2. Why Not MVVM
|
|
29
|
+
|
|
30
|
+
- A SwiftUI view is a struct that is cheap to build and rebuilt often. A view
|
|
31
|
+
model that mirrors it pushes against that design.
|
|
32
|
+
- Apple's data-flow sessions barely mention view models: *Data Flow Through
|
|
33
|
+
SwiftUI* (WWDC19), *Data Essentials in SwiftUI* (WWDC20), *Discover
|
|
34
|
+
Observation in SwiftUI* (WWDC23).
|
|
35
|
+
- Every view model adds cost: another object to keep in step with the view,
|
|
36
|
+
another hop to read through, and hand-written fetching that repeats what
|
|
37
|
+
SwiftUI and SwiftData already do.
|
|
38
|
+
|
|
39
|
+
## 3. MV in Practice
|
|
40
|
+
|
|
41
|
+
### A view backed by an environment service
|
|
42
42
|
|
|
43
43
|
```swift
|
|
44
|
-
struct
|
|
45
|
-
@Environment(
|
|
46
|
-
@Environment(
|
|
47
|
-
|
|
48
|
-
enum
|
|
49
|
-
case
|
|
44
|
+
struct ArticlesView: View {
|
|
45
|
+
@Environment(NewsService.self) private var news
|
|
46
|
+
@Environment(Branding.self) private var branding
|
|
47
|
+
|
|
48
|
+
enum Phase {
|
|
49
|
+
case fetching
|
|
50
|
+
case failed(String)
|
|
51
|
+
case ready([Article])
|
|
50
52
|
}
|
|
51
53
|
|
|
52
|
-
@State private var
|
|
53
|
-
@State private var isRefreshing = false
|
|
54
|
+
@State private var phase: Phase = .fetching
|
|
54
55
|
|
|
55
56
|
var body: some View {
|
|
56
57
|
NavigationStack {
|
|
57
58
|
List {
|
|
58
|
-
switch
|
|
59
|
-
case .
|
|
60
|
-
ProgressView("
|
|
59
|
+
switch phase {
|
|
60
|
+
case .fetching:
|
|
61
|
+
ProgressView("Fetching articles")
|
|
61
62
|
.frame(maxWidth: .infinity)
|
|
62
63
|
.listRowSeparator(.hidden)
|
|
63
|
-
case .
|
|
64
|
-
ContentUnavailableView("
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
}
|
|
64
|
+
case .failed(let reason):
|
|
65
|
+
ContentUnavailableView("No articles",
|
|
66
|
+
systemImage: "wifi.exclamationmark",
|
|
67
|
+
description: Text(reason))
|
|
68
|
+
.listRowSeparator(.hidden)
|
|
69
|
+
case .ready(let articles):
|
|
70
|
+
ForEach(articles) { ArticleRow(article: $0) }
|
|
71
71
|
}
|
|
72
72
|
}
|
|
73
73
|
.listStyle(.plain)
|
|
74
|
-
.refreshable { await
|
|
75
|
-
.task { await
|
|
74
|
+
.refreshable { await fetch() }
|
|
75
|
+
.task { await fetch() }
|
|
76
76
|
}
|
|
77
77
|
}
|
|
78
78
|
|
|
79
|
-
private func
|
|
80
|
-
do {
|
|
81
|
-
|
|
82
|
-
viewState = .loaded(posts)
|
|
83
|
-
} catch {
|
|
84
|
-
viewState = .error(error.localizedDescription)
|
|
85
|
-
}
|
|
79
|
+
private func fetch() async {
|
|
80
|
+
do { phase = .ready(try await news.latest()) }
|
|
81
|
+
catch { phase = .failed(error.localizedDescription) }
|
|
86
82
|
}
|
|
87
83
|
}
|
|
88
84
|
```
|
|
89
85
|
|
|
90
|
-
|
|
86
|
+
### Modifiers as small reducers
|
|
91
87
|
|
|
92
|
-
|
|
88
|
+
Treat `.task(id:)` and `.onChange` as tiny reducers: an input changes, a bit of
|
|
89
|
+
state follows.
|
|
93
90
|
|
|
94
91
|
```swift
|
|
95
|
-
.task(id:
|
|
96
|
-
guard !
|
|
97
|
-
await
|
|
92
|
+
.task(id: filterText) {
|
|
93
|
+
guard !filterText.isEmpty else { return }
|
|
94
|
+
await runFilter(filterText)
|
|
98
95
|
}
|
|
99
|
-
.onChange(of:
|
|
100
|
-
|
|
101
|
-
|
|
96
|
+
.onChange(of: isFiltering, initial: false) { _, active in
|
|
97
|
+
if !active {
|
|
98
|
+
Task { await loadDefaultArticles() }
|
|
99
|
+
}
|
|
102
100
|
}
|
|
103
101
|
```
|
|
104
102
|
|
|
105
|
-
|
|
103
|
+
### Dependencies installed once at the app level
|
|
106
104
|
|
|
107
105
|
```swift
|
|
108
106
|
@main
|
|
109
|
-
struct
|
|
110
|
-
@State var
|
|
111
|
-
@State var
|
|
112
|
-
@State var router =
|
|
107
|
+
struct NewsReaderApp: App {
|
|
108
|
+
@State private var news = NewsService()
|
|
109
|
+
@State private var session = SessionManager()
|
|
110
|
+
@State private var router = TabRouter(startTab: .today)
|
|
113
111
|
|
|
114
112
|
var body: some Scene {
|
|
115
113
|
WindowGroup {
|
|
116
|
-
|
|
117
|
-
.environment(
|
|
118
|
-
.environment(
|
|
114
|
+
RootView()
|
|
115
|
+
.environment(news)
|
|
116
|
+
.environment(session)
|
|
119
117
|
.environment(router)
|
|
120
118
|
}
|
|
121
119
|
}
|
|
122
120
|
}
|
|
123
121
|
```
|
|
124
122
|
|
|
125
|
-
|
|
123
|
+
Each dependency is created one time and every view below can reach it.
|
|
126
124
|
|
|
127
|
-
|
|
125
|
+
### SwiftData directly in views
|
|
128
126
|
|
|
129
|
-
SwiftData
|
|
127
|
+
SwiftData is designed to be queried from views. Wrapping it in a view model
|
|
128
|
+
means fetching and refreshing by hand, plus boilerplate.
|
|
130
129
|
|
|
131
130
|
```swift
|
|
132
|
-
struct
|
|
133
|
-
@Query private var
|
|
134
|
-
@Environment(\.modelContext) private var
|
|
131
|
+
struct ShelfView: View {
|
|
132
|
+
@Query(sort: \Novel.title) private var novels: [Novel]
|
|
133
|
+
@Environment(\.modelContext) private var context
|
|
135
134
|
|
|
136
135
|
var body: some View {
|
|
137
136
|
List {
|
|
138
|
-
ForEach(
|
|
139
|
-
|
|
137
|
+
ForEach(novels) { novel in
|
|
138
|
+
NovelRow(novel: novel)
|
|
140
139
|
.swipeActions {
|
|
141
140
|
Button("Delete", role: .destructive) {
|
|
142
|
-
|
|
141
|
+
context.delete(novel)
|
|
143
142
|
}
|
|
144
143
|
}
|
|
145
144
|
}
|
|
@@ -148,150 +147,133 @@ struct BookListView: View {
|
|
|
148
147
|
}
|
|
149
148
|
```
|
|
150
149
|
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
### When a ViewModel Already Exists
|
|
154
|
-
|
|
155
|
-
If a ViewModel exists in the codebase:
|
|
156
|
-
- Make it non-optional when possible
|
|
157
|
-
- Pass dependencies via `init`, then forward them into the ViewModel in the view's `init`
|
|
158
|
-
- Store as `@State` in the root view that owns it
|
|
159
|
-
- Avoid `bootstrapIfNeeded` patterns
|
|
150
|
+
## 4. Working With an Existing View Model
|
|
160
151
|
|
|
161
|
-
|
|
162
|
-
@State private var viewModel: SomeViewModel
|
|
163
|
-
|
|
164
|
-
init(dependency: Dependency) {
|
|
165
|
-
_viewModel = State(initialValue: SomeViewModel(dependency: dependency))
|
|
166
|
-
}
|
|
167
|
-
```
|
|
152
|
+
When the project already has view models, keep them tidy:
|
|
168
153
|
|
|
169
|
-
|
|
154
|
+
- Make the view model non-optional where you can.
|
|
155
|
+
- Take dependencies in the view's `init` and hand them to the view model
|
|
156
|
+
there.
|
|
157
|
+
- Hold the view model in `@State` in the view that owns it.
|
|
158
|
+
- Skip lazy `bootstrapIfNeeded`-style setup.
|
|
170
159
|
|
|
171
160
|
```swift
|
|
172
|
-
@MainActor @Observable
|
|
173
|
-
|
|
174
|
-
var
|
|
175
|
-
|
|
176
|
-
private let
|
|
177
|
-
|
|
178
|
-
init(
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
defer { isSaving = false }
|
|
185
|
-
try await client.update(name: name)
|
|
161
|
+
@MainActor @Observable
|
|
162
|
+
final class AccountEditorModel {
|
|
163
|
+
var displayName = ""
|
|
164
|
+
var isSubmitting = false
|
|
165
|
+
private let api: AccountAPI
|
|
166
|
+
|
|
167
|
+
init(api: AccountAPI) { self.api = api }
|
|
168
|
+
|
|
169
|
+
func submit() async throws {
|
|
170
|
+
isSubmitting = true
|
|
171
|
+
defer { isSubmitting = false }
|
|
172
|
+
try await api.rename(to: displayName)
|
|
186
173
|
}
|
|
187
174
|
}
|
|
188
175
|
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
@State private var viewModel: ProfileViewModel
|
|
176
|
+
struct AccountEditorScreen: View {
|
|
177
|
+
@State private var model: AccountEditorModel
|
|
192
178
|
|
|
193
|
-
init(
|
|
194
|
-
|
|
179
|
+
init(api: AccountAPI) {
|
|
180
|
+
_model = State(initialValue: AccountEditorModel(api: api))
|
|
195
181
|
}
|
|
196
182
|
|
|
197
|
-
var body: some View {
|
|
198
|
-
ProfileForm(viewModel: viewModel)
|
|
199
|
-
}
|
|
183
|
+
var body: some View { AccountEditorForm(model: model) }
|
|
200
184
|
}
|
|
201
185
|
|
|
202
|
-
|
|
203
|
-
|
|
204
|
-
@Bindable var viewModel: ProfileViewModel
|
|
186
|
+
struct AccountEditorForm: View {
|
|
187
|
+
@Bindable var model: AccountEditorModel
|
|
205
188
|
|
|
206
189
|
var body: some View {
|
|
207
|
-
|
|
208
|
-
|
|
209
|
-
|
|
190
|
+
Form {
|
|
191
|
+
TextField("Display name", text: $model.displayName)
|
|
192
|
+
Button("Submit") {
|
|
193
|
+
Task { try? await model.submit() }
|
|
194
|
+
}
|
|
195
|
+
.disabled(model.isSubmitting)
|
|
196
|
+
}
|
|
210
197
|
}
|
|
211
198
|
}
|
|
212
199
|
```
|
|
213
200
|
|
|
214
|
-
|
|
215
|
-
|
|
216
|
-
The MV pattern is the default. Introduce a ViewModel only when the view would be hard to read or test without one:
|
|
217
|
-
|
|
218
|
-
- **Multi-step workflows** - onboarding, checkout, or wizard flows where each step mutates shared draft state
|
|
219
|
-
- **Non-trivial business logic** - validation chains, derived state from multiple sources, or transformation pipelines that don't belong in a lightweight client
|
|
220
|
-
- **Coordinated async streams** - the view orchestrates multiple publishers or `AsyncSequence` values with interdependent state transitions
|
|
221
|
-
- **Existing test surface** - the codebase already tests against a ViewModel interface and rewriting to MV would be high cost, low reward
|
|
222
|
-
|
|
223
|
-
The bar is "this view would be hard to read and test without a ViewModel," not "I'm used to MVVM."
|
|
201
|
+
## 5. When a New View Model Is Justified
|
|
224
202
|
|
|
225
|
-
|
|
203
|
+
Add one only when the view would otherwise be hard to read or hard to test.
|
|
204
|
+
Cases that qualify:
|
|
226
205
|
|
|
227
|
-
|
|
228
|
-
|
|
229
|
-
-
|
|
230
|
-
|
|
206
|
+
- Multi-step flows such as onboarding, checkout or a wizard, where every step
|
|
207
|
+
edits the same draft.
|
|
208
|
+
- Real business logic: validation chains, state derived from several sources,
|
|
209
|
+
transformation pipelines that do not fit a lightweight client.
|
|
210
|
+
- Coordinating several publishers or `AsyncSequence` streams whose transitions
|
|
211
|
+
depend on each other.
|
|
212
|
+
- A code base that already tests against a view model interface, where moving
|
|
213
|
+
to MV would cost more than it returns.
|
|
231
214
|
|
|
232
|
-
|
|
233
|
-
|
|
234
|
-
- Parent-to-child data that only one view needs
|
|
235
|
-
- Values known at call site that don't change
|
|
215
|
+
The test is readability and testability. Familiarity with MVVM from other
|
|
216
|
+
platforms is not a reason.
|
|
236
217
|
|
|
237
|
-
|
|
218
|
+
## 6. Environment or Initializer Injection
|
|
238
219
|
|
|
239
|
-
|
|
220
|
+
Use `@Environment` for dependencies many views need at different depths:
|
|
240
221
|
|
|
241
|
-
-
|
|
242
|
-
-
|
|
243
|
-
-
|
|
244
|
-
- Use UI automation for end-to-end tests
|
|
245
|
-
- Views should be simple enough that they do not need dedicated unit tests
|
|
222
|
+
- app-wide services: authentication, the network client, theme, router
|
|
223
|
+
- the SwiftData `ModelContext`
|
|
224
|
+
- a feature store installed at a navigation root
|
|
246
225
|
|
|
247
|
-
|
|
226
|
+
Use initializer parameters for data that belongs to one instance:
|
|
248
227
|
|
|
249
|
-
|
|
228
|
+
- the selected item, a filter mode, a configuration value
|
|
229
|
+
- data one child needs from its parent
|
|
230
|
+
- values fixed at the call site
|
|
250
231
|
|
|
251
|
-
|
|
232
|
+
Init parameters make requirements visible and keep previews simple. If three or
|
|
233
|
+
more views in between would only forward a parameter, move it to the
|
|
234
|
+
environment.
|
|
252
235
|
|
|
253
|
-
|
|
236
|
+
## 7. Testing Strategy
|
|
254
237
|
|
|
255
|
-
-
|
|
256
|
-
-
|
|
257
|
-
-
|
|
258
|
-
-
|
|
259
|
-
-
|
|
260
|
-
- [Sheet Routing (Enum-Driven)](#sheet-routing-enum-driven)
|
|
261
|
-
- [App Entry Point](#app-entry-point)
|
|
262
|
-
- [Deep Linking](#deep-linking)
|
|
263
|
-
- [When to Use](#when-to-use)
|
|
264
|
-
- [Caveats](#caveats)
|
|
238
|
+
- Unit test services and business rules.
|
|
239
|
+
- Test models and data transformations.
|
|
240
|
+
- Use SwiftUI previews to catch visual regressions.
|
|
241
|
+
- Cover end-to-end flows with UI automation.
|
|
242
|
+
- Keep views simple enough that they need no unit tests of their own.
|
|
265
243
|
|
|
266
|
-
|
|
244
|
+
## 8. App Shell and Dependency Graph
|
|
267
245
|
|
|
268
|
-
|
|
246
|
+
### Goal
|
|
269
247
|
|
|
270
|
-
|
|
248
|
+
Wire the shell (tab view, navigation stacks, sheets) and install global
|
|
249
|
+
dependencies (environment objects, services, streaming clients, the SwiftData
|
|
250
|
+
`ModelContainer`) in one place:
|
|
271
251
|
|
|
272
|
-
1.
|
|
273
|
-
2.
|
|
274
|
-
|
|
252
|
+
1. The root view builds the tabs, one router per tab, and the sheets.
|
|
253
|
+
2. One view modifier installs global dependencies and lifecycle work: auth
|
|
254
|
+
state, streaming watchers, push tokens, data containers.
|
|
255
|
+
3. Feature views read only what they need from the environment. Feature state
|
|
256
|
+
stays inside the feature.
|
|
275
257
|
|
|
276
|
-
### Root
|
|
258
|
+
### Root shell
|
|
277
259
|
|
|
278
260
|
```swift
|
|
279
261
|
@MainActor
|
|
280
|
-
struct
|
|
281
|
-
@State private var
|
|
282
|
-
@State private var
|
|
262
|
+
struct RootView: View {
|
|
263
|
+
@State private var currentTab: MainTab = .today
|
|
264
|
+
@State private var routers = TabRouter()
|
|
283
265
|
|
|
284
266
|
var body: some View {
|
|
285
|
-
TabView(selection: $
|
|
286
|
-
ForEach(
|
|
287
|
-
let router =
|
|
267
|
+
TabView(selection: $currentTab) {
|
|
268
|
+
ForEach(MainTab.allCases) { tab in
|
|
269
|
+
let router = routers.router(for: tab)
|
|
288
270
|
Tab(value: tab) {
|
|
289
|
-
NavigationStack(path:
|
|
290
|
-
tab.
|
|
271
|
+
NavigationStack(path: routers.pathBinding(for: tab)) {
|
|
272
|
+
tab.rootView()
|
|
291
273
|
}
|
|
292
|
-
.
|
|
293
|
-
get: { router.
|
|
294
|
-
set: { router.
|
|
274
|
+
.presentsSheets(Binding(
|
|
275
|
+
get: { router.activeSheet },
|
|
276
|
+
set: { router.activeSheet = $0 }
|
|
295
277
|
))
|
|
296
278
|
.environment(router)
|
|
297
279
|
} label: {
|
|
@@ -300,259 +282,286 @@ struct AppView: View {
|
|
|
300
282
|
}
|
|
301
283
|
}
|
|
302
284
|
.tabBarMinimizeBehavior(.onScrollDown)
|
|
303
|
-
.
|
|
285
|
+
.installAppServices()
|
|
304
286
|
}
|
|
305
287
|
}
|
|
306
288
|
```
|
|
307
289
|
|
|
308
|
-
|
|
290
|
+
`Tab(value:)` needs iOS 18; `.tabBarMinimizeBehavior(_:)` needs iOS 26.
|
|
309
291
|
|
|
310
292
|
```swift
|
|
311
293
|
@MainActor
|
|
312
|
-
enum
|
|
313
|
-
case
|
|
314
|
-
|
|
294
|
+
enum MainTab: Identifiable, Hashable, CaseIterable {
|
|
295
|
+
case today, alerts, preferences
|
|
296
|
+
|
|
297
|
+
nonisolated var id: String { String(describing: self) }
|
|
315
298
|
|
|
316
299
|
@ViewBuilder
|
|
317
|
-
func
|
|
300
|
+
func rootView() -> some View {
|
|
318
301
|
switch self {
|
|
319
|
-
case .
|
|
320
|
-
case .
|
|
321
|
-
case .
|
|
302
|
+
case .today: TodayView()
|
|
303
|
+
case .alerts: AlertsView()
|
|
304
|
+
case .preferences: PreferencesView()
|
|
322
305
|
}
|
|
323
306
|
}
|
|
324
307
|
|
|
325
308
|
@ViewBuilder
|
|
326
309
|
var label: some View {
|
|
327
310
|
switch self {
|
|
328
|
-
case .
|
|
329
|
-
case .
|
|
330
|
-
case .
|
|
311
|
+
case .today: Label("Today", systemImage: "house")
|
|
312
|
+
case .alerts: Label("Alerts", systemImage: "bell")
|
|
313
|
+
case .preferences: Label("Preferences", systemImage: "gear")
|
|
331
314
|
}
|
|
332
315
|
}
|
|
333
316
|
}
|
|
334
|
-
```
|
|
335
317
|
|
|
336
|
-
|
|
337
|
-
|
|
338
|
-
|
|
339
|
-
|
|
340
|
-
@Observable
|
|
341
|
-
final class RouterPath {
|
|
342
|
-
var path: [Route] = []
|
|
343
|
-
var presentedSheet: SheetDestination?
|
|
318
|
+
@MainActor @Observable
|
|
319
|
+
final class NavigationRouter {
|
|
320
|
+
var path: [Destination] = []
|
|
321
|
+
var activeSheet: SheetRoute?
|
|
344
322
|
}
|
|
345
323
|
|
|
346
|
-
enum
|
|
347
|
-
case
|
|
324
|
+
enum Destination: Hashable {
|
|
325
|
+
case article(id: String)
|
|
348
326
|
}
|
|
349
327
|
```
|
|
350
328
|
|
|
351
|
-
|
|
329
|
+
`TabRouter` is a small `@Observable` holder that keeps one `NavigationRouter`
|
|
330
|
+
per tab and vends a `Binding` to each router's `path`.
|
|
331
|
+
|
|
332
|
+
`MainTab` is `@MainActor` because it builds views, so its `id` is marked
|
|
333
|
+
`nonisolated`; otherwise the `Identifiable` conformance would cross into
|
|
334
|
+
main-actor code and Swift 6 rejects it.
|
|
352
335
|
|
|
353
|
-
|
|
336
|
+
### Dependency graph modifier
|
|
337
|
+
|
|
338
|
+
Install every environment object and lifecycle hook through one modifier. Call
|
|
339
|
+
sites stay consistent and none of them can forget a dependency.
|
|
354
340
|
|
|
355
341
|
```swift
|
|
356
342
|
extension View {
|
|
357
|
-
func
|
|
358
|
-
|
|
359
|
-
|
|
360
|
-
|
|
361
|
-
|
|
343
|
+
func installAppServices(
|
|
344
|
+
api: APIClient = .shared,
|
|
345
|
+
session: SessionManager = .shared,
|
|
346
|
+
branding: Branding = .shared,
|
|
347
|
+
banners: BannerCenter = .shared
|
|
362
348
|
) -> some View {
|
|
363
|
-
environment(
|
|
364
|
-
.environment(
|
|
365
|
-
.environment(
|
|
366
|
-
.environment(
|
|
367
|
-
.task(id:
|
|
368
|
-
|
|
369
|
-
await client.configure(for: auth.currentAccount)
|
|
349
|
+
environment(api)
|
|
350
|
+
.environment(session)
|
|
351
|
+
.environment(branding)
|
|
352
|
+
.environment(banners)
|
|
353
|
+
.task(id: session.activeUser?.id) {
|
|
354
|
+
await api.prepare(for: session.activeUser)
|
|
370
355
|
}
|
|
371
356
|
}
|
|
372
357
|
}
|
|
373
358
|
```
|
|
374
359
|
|
|
375
|
-
|
|
376
|
-
|
|
377
|
-
|
|
378
|
-
|
|
360
|
+
The `.task(id:)` hook runs again when the signed-in account changes, so
|
|
361
|
+
services and watchers are re-seeded for the new user. Keep this modifier to
|
|
362
|
+
global wiring; feature state belongs to features. Rename the types to match the
|
|
363
|
+
project.
|
|
379
364
|
|
|
380
|
-
###
|
|
365
|
+
### ModelContainer
|
|
381
366
|
|
|
382
|
-
Install `ModelContainer` at the root so
|
|
367
|
+
Install the `ModelContainer` at the root so every feature shares one store:
|
|
383
368
|
|
|
384
369
|
```swift
|
|
385
370
|
extension View {
|
|
386
|
-
func
|
|
387
|
-
modelContainer(for: [
|
|
371
|
+
func installDataStore() -> some View {
|
|
372
|
+
modelContainer(for: [Note.self, Folder.self, Attachment.self])
|
|
388
373
|
}
|
|
389
374
|
}
|
|
390
375
|
```
|
|
391
376
|
|
|
392
|
-
A single container avoids
|
|
377
|
+
A single container avoids a separate store per sheet or tab and keeps data
|
|
378
|
+
consistent.
|
|
393
379
|
|
|
394
|
-
###
|
|
380
|
+
### Enum-driven sheets
|
|
395
381
|
|
|
396
|
-
|
|
382
|
+
Keep every sheet in one small `Identifiable` enum plus a helper modifier:
|
|
397
383
|
|
|
398
384
|
```swift
|
|
399
|
-
enum
|
|
400
|
-
case
|
|
401
|
-
case
|
|
402
|
-
|
|
385
|
+
enum SheetRoute: Identifiable {
|
|
386
|
+
case newPost
|
|
387
|
+
case accountSettings
|
|
388
|
+
|
|
389
|
+
var id: Self { self }
|
|
403
390
|
}
|
|
404
391
|
|
|
405
392
|
extension View {
|
|
406
|
-
func
|
|
407
|
-
sheet(item:
|
|
408
|
-
switch
|
|
409
|
-
case .
|
|
410
|
-
|
|
411
|
-
case .settings:
|
|
412
|
-
SettingsView().withEnvironments()
|
|
393
|
+
func presentsSheets(_ route: Binding<SheetRoute?>) -> some View {
|
|
394
|
+
sheet(item: route) { route in
|
|
395
|
+
switch route {
|
|
396
|
+
case .newPost: NewPostView().installAppServices()
|
|
397
|
+
case .accountSettings: AccountSettingsView().installAppServices()
|
|
413
398
|
}
|
|
414
399
|
}
|
|
415
400
|
}
|
|
416
401
|
}
|
|
417
402
|
```
|
|
418
403
|
|
|
419
|
-
|
|
404
|
+
A new sheet costs one case and one branch. Presentation stays in one place and
|
|
405
|
+
is easy to test. Sheets start a new presentation context, so each one applies
|
|
406
|
+
the dependency modifier again.
|
|
420
407
|
|
|
421
|
-
### App
|
|
408
|
+
### App entry point
|
|
422
409
|
|
|
423
410
|
```swift
|
|
424
411
|
@main
|
|
425
|
-
struct
|
|
426
|
-
@State var
|
|
427
|
-
@State var
|
|
428
|
-
@State var router = AppRouter(initialTab: .home)
|
|
412
|
+
struct FieldNotesApp: App {
|
|
413
|
+
@State private var api = APIClient.shared
|
|
414
|
+
@State private var session = SessionManager.shared
|
|
429
415
|
|
|
430
416
|
var body: some Scene {
|
|
431
417
|
WindowGroup {
|
|
432
|
-
|
|
433
|
-
.environment(
|
|
434
|
-
.environment(
|
|
435
|
-
.
|
|
418
|
+
RootView()
|
|
419
|
+
.environment(api)
|
|
420
|
+
.environment(session)
|
|
421
|
+
.installDataStore()
|
|
436
422
|
}
|
|
437
423
|
}
|
|
438
424
|
}
|
|
439
425
|
```
|
|
440
426
|
|
|
441
|
-
### Deep
|
|
427
|
+
### Deep links
|
|
442
428
|
|
|
443
|
-
|
|
429
|
+
- For state restoration, save the navigation path. `NavigationPath.codable`
|
|
430
|
+
gives a `Codable` representation when every element is `Codable`.
|
|
431
|
+
- Handle incoming URLs with `.onOpenURL`:
|
|
444
432
|
|
|
445
433
|
```swift
|
|
446
434
|
.onOpenURL { url in
|
|
447
|
-
guard let
|
|
448
|
-
router.
|
|
435
|
+
guard let destination = Destination(url: url) else { return }
|
|
436
|
+
router.path.append(destination)
|
|
449
437
|
}
|
|
450
438
|
```
|
|
451
439
|
|
|
452
|
-
|
|
440
|
+
Complete URL routing is covered in `swiftui-navigation`.
|
|
453
441
|
|
|
454
|
-
### When
|
|
442
|
+
### When this structure fits
|
|
455
443
|
|
|
456
|
-
-
|
|
457
|
-
-
|
|
458
|
-
|
|
444
|
+
- Several packages or modules share the same environment objects and services.
|
|
445
|
+
- The app has to react to account or client changes and rewire streaming or
|
|
446
|
+
push safely.
|
|
447
|
+
- You want tabs, navigation stacks and sheets wired the same way everywhere
|
|
448
|
+
without repeating environment setup.
|
|
459
449
|
|
|
460
450
|
### Caveats
|
|
461
451
|
|
|
462
|
-
- Keep the dependency modifier slim
|
|
463
|
-
-
|
|
464
|
-
|
|
465
|
-
|
|
466
|
-
|
|
452
|
+
- Keep the dependency modifier slim: no feature state, no heavy logic.
|
|
453
|
+
- Work in `.task(id:)` must be short or cancel cleanly. Long-running work
|
|
454
|
+
belongs in services.
|
|
455
|
+
- When a client can exist without a signed-in user, gate streaming and watch
|
|
456
|
+
calls so the app does not fall into a reconnect loop.
|
|
467
457
|
|
|
468
|
-
|
|
458
|
+
## 9. Lightweight Clients
|
|
469
459
|
|
|
470
|
-
|
|
471
|
-
|
|
472
|
-
|
|
473
|
-
- Enable easy stubbing in previews/tests.
|
|
460
|
+
A client can be a plain struct of async closures. There is no view model and
|
|
461
|
+
no DI framework, the view never sees networking, and previews or tests swap in
|
|
462
|
+
a stub. Business logic sits in a store or feature layer.
|
|
474
463
|
|
|
475
|
-
### Minimal shape
|
|
476
464
|
```swift
|
|
477
|
-
struct
|
|
478
|
-
var
|
|
479
|
-
var
|
|
480
|
-
|
|
481
|
-
|
|
482
|
-
|
|
483
|
-
|
|
484
|
-
|
|
485
|
-
|
|
486
|
-
|
|
487
|
-
|
|
465
|
+
struct CatalogClient {
|
|
466
|
+
var products: (_ page: Int) async throws -> [Product]
|
|
467
|
+
var lookup: (_ term: String, _ page: Int) async throws -> [Product]
|
|
468
|
+
|
|
469
|
+
static func live(host: URL) -> CatalogClient {
|
|
470
|
+
let session = URLSession(configuration: .catalog)
|
|
471
|
+
return CatalogClient(
|
|
472
|
+
products: { page in
|
|
473
|
+
let url = host.appending(path: "products")
|
|
474
|
+
.appending(queryItems: [URLQueryItem(name: "page", value: String(page))])
|
|
475
|
+
let (data, _) = try await session.data(from: url)
|
|
476
|
+
return try JSONDecoder().decode([Product].self, from: data)
|
|
488
477
|
},
|
|
489
|
-
|
|
490
|
-
|
|
478
|
+
lookup: { term, page in
|
|
479
|
+
let url = host.appending(path: "search")
|
|
480
|
+
.appending(queryItems: [
|
|
481
|
+
URLQueryItem(name: "q", value: term),
|
|
482
|
+
URLQueryItem(name: "page", value: String(page))
|
|
483
|
+
])
|
|
484
|
+
let (data, _) = try await session.data(from: url)
|
|
485
|
+
return try JSONDecoder().decode([Product].self, from: data)
|
|
491
486
|
}
|
|
492
487
|
)
|
|
493
488
|
}
|
|
494
489
|
}
|
|
490
|
+
|
|
491
|
+
extension URLSessionConfiguration {
|
|
492
|
+
static var catalog: URLSessionConfiguration {
|
|
493
|
+
let config = URLSessionConfiguration.default
|
|
494
|
+
config.timeoutIntervalForRequest = 30
|
|
495
|
+
config.timeoutIntervalForResource = 300
|
|
496
|
+
config.waitsForConnectivity = true
|
|
497
|
+
config.urlCache = URLCache(memoryCapacity: 10_000_000, diskCapacity: 50_000_000)
|
|
498
|
+
return config
|
|
499
|
+
}
|
|
500
|
+
}
|
|
495
501
|
```
|
|
496
502
|
|
|
497
|
-
|
|
503
|
+
`URLSession.shared` is fine for a prototype. Production code builds its own
|
|
504
|
+
session as above: 30 second request timeout, 300 second resource timeout,
|
|
505
|
+
`waitsForConnectivity` on, and a `URLCache`.
|
|
506
|
+
|
|
498
507
|
```swift
|
|
499
|
-
@MainActor
|
|
500
|
-
|
|
501
|
-
enum
|
|
508
|
+
@MainActor @Observable
|
|
509
|
+
final class CatalogStore {
|
|
510
|
+
enum Status { case idle, loading, loaded, failed(String) }
|
|
502
511
|
|
|
503
|
-
var
|
|
504
|
-
var
|
|
505
|
-
private let client:
|
|
512
|
+
var status: Status = .idle
|
|
513
|
+
var products: [Product] = []
|
|
514
|
+
private let client: CatalogClient
|
|
506
515
|
|
|
507
|
-
init(client:
|
|
508
|
-
self.client = client
|
|
509
|
-
}
|
|
516
|
+
init(client: CatalogClient) { self.client = client }
|
|
510
517
|
|
|
511
|
-
func load(
|
|
512
|
-
|
|
518
|
+
func load(page: Int = 1) async {
|
|
519
|
+
status = .loading
|
|
513
520
|
do {
|
|
514
|
-
|
|
515
|
-
|
|
521
|
+
products = try await client.products(page)
|
|
522
|
+
status = .loaded
|
|
516
523
|
} catch {
|
|
517
|
-
|
|
524
|
+
status = .failed(error.localizedDescription)
|
|
518
525
|
}
|
|
519
526
|
}
|
|
520
527
|
}
|
|
521
|
-
```
|
|
522
528
|
|
|
523
|
-
|
|
524
|
-
|
|
525
|
-
@Environment(ItemsStore.self) private var store
|
|
529
|
+
struct CatalogView: View {
|
|
530
|
+
@Environment(CatalogStore.self) private var store
|
|
526
531
|
|
|
527
532
|
var body: some View {
|
|
528
|
-
List(store.
|
|
529
|
-
|
|
530
|
-
}
|
|
531
|
-
.task { await store.load() }
|
|
533
|
+
List(store.products) { ProductRow(product: $0) }
|
|
534
|
+
.task { await store.load() }
|
|
532
535
|
}
|
|
533
536
|
}
|
|
534
|
-
```
|
|
535
537
|
|
|
536
|
-
```swift
|
|
537
538
|
@main
|
|
538
|
-
struct
|
|
539
|
-
@State private var store =
|
|
539
|
+
struct ShopApp: App {
|
|
540
|
+
@State private var store = CatalogStore(client: .live(host: AppConfig.apiHost))
|
|
540
541
|
|
|
541
542
|
var body: some Scene {
|
|
542
543
|
WindowGroup {
|
|
543
|
-
|
|
544
|
-
.environment(store)
|
|
544
|
+
CatalogView().environment(store)
|
|
545
545
|
}
|
|
546
546
|
}
|
|
547
547
|
}
|
|
548
548
|
```
|
|
549
549
|
|
|
550
|
-
|
|
551
|
-
|
|
552
|
-
-
|
|
553
|
-
|
|
554
|
-
-
|
|
550
|
+
Guidelines:
|
|
551
|
+
|
|
552
|
+
- URL building and decoding live in the client. State changes live in the
|
|
553
|
+
store.
|
|
554
|
+
- The store receives the client in `init` and keeps it `private`.
|
|
555
|
+
- No global singletons for stores; install them with `.environment`.
|
|
556
|
+
- Add `static func mock(...)` factories for stubbed variants.
|
|
557
|
+
|
|
558
|
+
Pitfalls:
|
|
559
|
+
|
|
560
|
+
- UI state kept in the client. It belongs in the store.
|
|
561
|
+
- Client closures that capture `self` or view state.
|
|
562
|
+
|
|
563
|
+
## 10. Further Reading
|
|
555
564
|
|
|
556
|
-
|
|
557
|
-
|
|
558
|
-
-
|
|
565
|
+
- "SwiftUI in 2025: Forget MVVM" by Thomas Ricouard, the article this MV
|
|
566
|
+
guidance follows.
|
|
567
|
+
- The WWDC data-flow sessions listed in [Why Not MVVM](#2-why-not-mvvm).
|