@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
|
@@ -1,500 +1,484 @@
|
|
|
1
1
|
---
|
|
2
2
|
name: swift-architecture
|
|
3
|
-
description: "
|
|
3
|
+
description: "App architecture on Apple platforms (Swift 6.3, SwiftUI, UIKit): MV with @Observable, MVVM, MVI, TCA, Clean Architecture, VIPER, Coordinator, module boundaries, dependency direction. Use when selecting, implementing or migrating between patterns, judging whether a feature's complexity justifies a heavier one, or reviewing whether an app's architecture fits. Not for SwiftUI state wrappers, navigation, concurrency diagnostics or test syntax."
|
|
4
4
|
metadata:
|
|
5
|
-
source:
|
|
5
|
+
source: multi-agent-pipeline
|
|
6
6
|
---
|
|
7
|
+
|
|
7
8
|
# Swift Architecture
|
|
8
9
|
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
- [Scope Boundary](#scope-boundary)
|
|
14
|
-
- [Architecture Selection](#architecture-selection)
|
|
15
|
-
- [MV Pattern (Model-View with `@Observable`)](#mv-pattern)
|
|
16
|
-
- [MVVM](#mvvm)
|
|
17
|
-
- [MVI (Model-View-Intent)](#mvi)
|
|
18
|
-
- [TCA (The Composable Architecture)](#tca)
|
|
19
|
-
- [Clean Architecture](#clean-architecture)
|
|
20
|
-
- [Coordinator Pattern](#coordinator-pattern)
|
|
21
|
-
- [VIPER](#viper)
|
|
22
|
-
- [Migration Between Patterns](#migration-between-patterns)
|
|
23
|
-
- [Common Mistakes](#common-mistakes)
|
|
24
|
-
- [Review Checklist](#review-checklist)
|
|
25
|
-
- [References](#references)
|
|
26
|
-
|
|
27
|
-
## Scope Boundary
|
|
28
|
-
|
|
29
|
-
This skill owns architecture-level decisions: pattern selection, module
|
|
30
|
-
boundaries, dependency direction, migration/escalation strategy, and structural
|
|
31
|
-
test strategy. It does not own SwiftUI state mechanics; route `@State`,
|
|
32
|
-
`@Bindable`, `@Environment`, edit-sheet/local state, bindings, view composition,
|
|
33
|
-
and `@Observable` MV implementation mechanics to `swiftui-patterns`. Use
|
|
34
|
-
`swiftui-navigation` for `NavigationStack`, `NavigationSplitView`,
|
|
35
|
-
`NavigationPath`, route models, sheets, tabs, and deep-link URL handling;
|
|
36
|
-
`swift-concurrency` for `@MainActor`, default MainActor isolation, `Sendable`,
|
|
37
|
-
strict-concurrency diagnostics, and data-race diagnostics; and `swift-testing`
|
|
38
|
-
for `@Test`, `#expect`, `#require`, fixtures, parameterized tests, mocks, stubs,
|
|
39
|
-
and suite organization.
|
|
40
|
-
|
|
41
|
-
## Architecture Selection
|
|
42
|
-
|
|
43
|
-
| Pattern | Best For | Complexity | Testability |
|
|
44
|
-
|---------|----------|-----------|-------------|
|
|
45
|
-
| **MV** | Small-to-medium SwiftUI apps, rapid iteration | Low | Moderate |
|
|
46
|
-
| **MVVM** | Medium apps, teams familiar with reactive patterns | Medium | High |
|
|
47
|
-
| **MVI** | Complex state machines, predictable state flow | Medium-High | High |
|
|
48
|
-
| **TCA** | Large apps needing composable features, strong testing | High | Very High |
|
|
49
|
-
| **Clean Architecture** | Enterprise apps, strict separation of concerns | High | Very High |
|
|
50
|
-
| **Coordinator** | Apps with complex navigation flows (UIKit or hybrid) | Medium | High |
|
|
51
|
-
| **VIPER** | Legacy UIKit modules already using VIPER boundaries | Very High | High |
|
|
52
|
-
|
|
53
|
-
**Default recommendation for new SwiftUI apps:** Start with MV (Model-View
|
|
54
|
-
with `@Observable`). Escalate to MVVM or TCA only when the feature's complexity
|
|
55
|
-
demands it.
|
|
56
|
-
|
|
57
|
-
Boundary-split answers should use one `swift-architecture` bucket for
|
|
58
|
-
pattern/module/dependency/migration/test-strategy decisions. Do not add a
|
|
59
|
-
separate architecture-owned "SwiftUI state ownership" bucket; property-wrapper,
|
|
60
|
-
local binding, navigation, concurrency-diagnostic, fixture, and parameterized
|
|
61
|
-
test mechanics are sibling-skill handoffs.
|
|
62
|
-
|
|
63
|
-
### Decision Framework
|
|
64
|
-
|
|
65
|
-
1. **Is the feature a simple CRUD screen?** → MV pattern
|
|
66
|
-
2. **Does the screen have complex business logic separate from the view?** → MVVM
|
|
67
|
-
3. **Do you need deterministic state transitions and side-effect management?** → MVI or TCA
|
|
68
|
-
4. **Is the app large with many independent feature modules?** → TCA or Clean Architecture
|
|
69
|
-
5. **Is navigation complex with deep linking and conditional flows?** → Add Coordinator pattern
|
|
70
|
-
|
|
71
|
-
## MV Pattern
|
|
72
|
-
|
|
73
|
-
The simplest SwiftUI architecture. The view observes `@Observable` models
|
|
74
|
-
directly. No intermediate view model layer.
|
|
10
|
+
Which structure a Swift app or feature should have, and how to move from one
|
|
11
|
+
structure to another without a rewrite. Examples are written for Swift 6.3; they use
|
|
12
|
+
SwiftUI with Observation (iOS 17 or later) unless a UIKit type appears.
|
|
75
13
|
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
14
|
+
## Scope boundary
|
|
15
|
+
|
|
16
|
+
This skill decides:
|
|
17
|
+
|
|
18
|
+
- which pattern a feature uses,
|
|
19
|
+
- where module boundaries fall and which way dependencies point,
|
|
20
|
+
- how to migrate or escalate between patterns,
|
|
21
|
+
- the test strategy at the structural level (what is unit-testable, and where).
|
|
22
|
+
|
|
23
|
+
It hands off the mechanics:
|
|
24
|
+
|
|
25
|
+
| Topic | Skill |
|
|
26
|
+
| --- | --- |
|
|
27
|
+
| `@State`, `@Bindable`, `@Environment`, bindings, local and edit-sheet state, view composition, the mechanics of MV with `@Observable` | `swiftui-patterns` |
|
|
28
|
+
| `NavigationStack`, `NavigationSplitView`, paths, route models, sheets, tabs, deep links | `swiftui-navigation` |
|
|
29
|
+
| `@MainActor`, default main-actor isolation, `Sendable`, strict-concurrency and data-race diagnostics | `swift-concurrency` |
|
|
30
|
+
| Swift Testing syntax (`@Test`, `#expect`, `#require`), fixtures, parameterized cases, test doubles, suite layout | `swift-testing` |
|
|
31
|
+
|
|
32
|
+
When one question touches several of these, answer the architecture part in a
|
|
33
|
+
single block (pattern, modules, dependency direction, migration, test
|
|
34
|
+
strategy). There is no extra block here for how SwiftUI views hold state;
|
|
35
|
+
property wrappers, bindings, navigation wiring, concurrency errors, fixtures and
|
|
36
|
+
parameterized tests are answered by the skills in the table.
|
|
37
|
+
|
|
38
|
+
## Architecture selection
|
|
39
|
+
|
|
40
|
+
| Pattern | Good match | Cost to adopt | How testable |
|
|
41
|
+
| --- | --- | --- | --- |
|
|
42
|
+
| MV (`@Observable` models) | Small or mid-sized SwiftUI apps where speed of change matters | Low | Moderate |
|
|
43
|
+
| MVVM | Mid-sized apps; teams at home with reactive view models | Medium | High |
|
|
44
|
+
| MVI | Features that are really state machines and need a predictable flow | Medium to high | High |
|
|
45
|
+
| TCA | Big apps built from composable features, with heavy testing | High | Very high |
|
|
46
|
+
| Clean Architecture | Enterprise codebases that require strict layer separation | High | Very high |
|
|
47
|
+
| Coordinator | Involved navigation in UIKit or mixed UIKit and SwiftUI apps | Medium | High |
|
|
48
|
+
| VIPER | UIKit modules that were already built as VIPER | Very high | High |
|
|
79
49
|
|
|
50
|
+
Default for a new SwiftUI app: MV with `@Observable`. Escalate to MVVM, and
|
|
51
|
+
later TCA, when the feature has grown enough to need it, not before.
|
|
52
|
+
|
|
53
|
+
### Decision framework
|
|
54
|
+
|
|
55
|
+
- A simple CRUD screen: **MV**.
|
|
56
|
+
- Business logic that should live apart from the view: **MVVM**.
|
|
57
|
+
- State transitions that must be deterministic, with side effects managed
|
|
58
|
+
explicitly: **MVI** or **TCA**.
|
|
59
|
+
- Dozens of separately owned feature modules in one app: **TCA** or **Clean
|
|
60
|
+
Architecture**.
|
|
61
|
+
- Navigation with deep links and conditional flows: add a **Coordinator** on
|
|
62
|
+
top of whichever pattern the features use (in pure SwiftUI, prefer path-based
|
|
63
|
+
routing first; see Coordinator below).
|
|
64
|
+
|
|
65
|
+
## MV pattern
|
|
66
|
+
|
|
67
|
+
Views read `@Observable` model objects themselves; nothing sits between them.
|
|
68
|
+
|
|
69
|
+
```swift
|
|
80
70
|
@MainActor
|
|
81
71
|
@Observable
|
|
82
|
-
final class
|
|
83
|
-
var
|
|
84
|
-
var
|
|
85
|
-
var
|
|
72
|
+
final class WorkoutLog {
|
|
73
|
+
private(set) var sessions: [Workout] = []
|
|
74
|
+
private(set) var loading = false
|
|
75
|
+
var lastError: (any Error)?
|
|
86
76
|
|
|
87
|
-
private let
|
|
77
|
+
private let store: any WorkoutStore
|
|
88
78
|
|
|
89
|
-
init(
|
|
90
|
-
self.service = service
|
|
91
|
-
}
|
|
79
|
+
init(store: any WorkoutStore) { self.store = store }
|
|
92
80
|
|
|
93
|
-
func
|
|
94
|
-
|
|
95
|
-
defer {
|
|
96
|
-
do {
|
|
97
|
-
|
|
98
|
-
} catch {
|
|
99
|
-
self.error = error
|
|
100
|
-
}
|
|
81
|
+
func refresh() async {
|
|
82
|
+
loading = true
|
|
83
|
+
defer { loading = false }
|
|
84
|
+
do { sessions = try await store.allWorkouts() }
|
|
85
|
+
catch { lastError = error }
|
|
101
86
|
}
|
|
102
87
|
|
|
103
|
-
func
|
|
104
|
-
try await
|
|
105
|
-
|
|
88
|
+
func remove(_ workout: Workout) async throws {
|
|
89
|
+
try await store.delete(workout.id)
|
|
90
|
+
sessions.removeAll { $0.id == workout.id }
|
|
106
91
|
}
|
|
107
92
|
}
|
|
108
93
|
|
|
109
|
-
struct
|
|
110
|
-
@State private var
|
|
94
|
+
struct WorkoutLogScreen: View {
|
|
95
|
+
@State private var log: WorkoutLog
|
|
96
|
+
|
|
97
|
+
init(store: any WorkoutStore) {
|
|
98
|
+
_log = State(initialValue: WorkoutLog(store: store))
|
|
99
|
+
}
|
|
111
100
|
|
|
112
101
|
var body: some View {
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
102
|
+
let entries = log.sessions
|
|
103
|
+
return List(entries) { Text($0.title) }
|
|
104
|
+
.overlay { if log.loading { ProgressView() } }
|
|
105
|
+
.task { await log.refresh() }
|
|
117
106
|
}
|
|
118
107
|
}
|
|
119
108
|
```
|
|
120
109
|
|
|
121
|
-
|
|
122
|
-
|
|
110
|
+
The view owns the model with `@State` and loads it in `.task`. The dependency
|
|
111
|
+
comes in through the initializer, never created inside the model.
|
|
123
112
|
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
113
|
+
MV is enough for single-screen features, prototypes and MVPs, small teams and
|
|
114
|
+
straightforward data flow. Move on when business logic keeps growing, when view
|
|
115
|
+
behaviour becomes hard to unit test, or when several views need different
|
|
116
|
+
transformations of the same shared state.
|
|
127
117
|
|
|
128
118
|
## MVVM
|
|
129
119
|
|
|
130
|
-
|
|
131
|
-
|
|
120
|
+
A view model sits between the view and the model: it prepares display values
|
|
121
|
+
and receives user actions, and the view watches it.
|
|
132
122
|
|
|
133
123
|
```swift
|
|
124
|
+
struct WorkoutRow: Identifiable {
|
|
125
|
+
let id: Workout.ID
|
|
126
|
+
let title: String
|
|
127
|
+
let when: String
|
|
128
|
+
|
|
129
|
+
init(_ workout: Workout) {
|
|
130
|
+
id = workout.id
|
|
131
|
+
title = workout.title
|
|
132
|
+
when = workout.start.formatted(.dateTime.month().day())
|
|
133
|
+
}
|
|
134
|
+
}
|
|
135
|
+
|
|
134
136
|
@MainActor
|
|
135
137
|
@Observable
|
|
136
|
-
final class
|
|
137
|
-
private(set) var
|
|
138
|
-
private(set) var
|
|
139
|
-
var
|
|
140
|
-
|
|
141
|
-
var filteredTrips: [TripRowItem] {
|
|
142
|
-
guard !searchText.isEmpty else { return trips }
|
|
143
|
-
return trips.filter { $0.name.localizedStandardContains(searchText) }
|
|
144
|
-
}
|
|
138
|
+
final class WorkoutListModel {
|
|
139
|
+
private(set) var rows: [WorkoutRow] = []
|
|
140
|
+
private(set) var loading = false
|
|
141
|
+
var filter = ""
|
|
145
142
|
|
|
146
|
-
private
|
|
143
|
+
private var all: [Workout] = []
|
|
144
|
+
private let store: any WorkoutStore
|
|
147
145
|
|
|
148
|
-
init(
|
|
149
|
-
self.repository = repository
|
|
150
|
-
}
|
|
146
|
+
init(store: any WorkoutStore) { self.store = store }
|
|
151
147
|
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
defer { isLoading = false }
|
|
155
|
-
let models = (try? await repository.fetchAll()) ?? []
|
|
156
|
-
trips = models.map { TripRowItem(from: $0) }
|
|
148
|
+
var visibleRows: [WorkoutRow] {
|
|
149
|
+
filter.isEmpty ? rows : rows.filter { $0.title.localizedStandardContains(filter) }
|
|
157
150
|
}
|
|
158
151
|
|
|
159
|
-
func
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
await loadTrips()
|
|
152
|
+
func load() async {
|
|
153
|
+
loading = true
|
|
154
|
+
defer { loading = false }
|
|
155
|
+
all = (try? await store.allWorkouts()) ?? []
|
|
156
|
+
rows = all.map(WorkoutRow.init)
|
|
165
157
|
}
|
|
166
|
-
}
|
|
167
|
-
|
|
168
|
-
struct TripRowItem: Identifiable {
|
|
169
|
-
let id: UUID
|
|
170
|
-
let name: String
|
|
171
|
-
let dateRange: String
|
|
172
158
|
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
+ " - " + trip.endDate.formatted(.dateTime.month().day())
|
|
159
|
+
func delete(rowsAt positions: IndexSet) async {
|
|
160
|
+
let doomed = positions.map { visibleRows[$0].id }
|
|
161
|
+
for id in doomed { try? await store.delete(id) }
|
|
162
|
+
await load()
|
|
178
163
|
}
|
|
179
164
|
}
|
|
180
165
|
|
|
181
|
-
struct
|
|
182
|
-
@State private var
|
|
166
|
+
struct WorkoutListScreen: View {
|
|
167
|
+
@State private var model: WorkoutListModel
|
|
183
168
|
|
|
184
|
-
init(
|
|
185
|
-
|
|
169
|
+
init(store: any WorkoutStore) {
|
|
170
|
+
_model = State(initialValue: WorkoutListModel(store: store))
|
|
186
171
|
}
|
|
187
172
|
|
|
188
173
|
var body: some View {
|
|
189
174
|
List {
|
|
190
|
-
ForEach(
|
|
191
|
-
|
|
192
|
-
}
|
|
193
|
-
.onDelete { offsets in
|
|
194
|
-
Task { await viewModel.delete(at: offsets) }
|
|
175
|
+
ForEach(model.visibleRows, id: \.id) { row in
|
|
176
|
+
LabeledContent(row.title, value: row.when)
|
|
195
177
|
}
|
|
178
|
+
.onDelete { positions in Task { await model.delete(rowsAt: positions) } }
|
|
196
179
|
}
|
|
197
|
-
.searchable(text: $
|
|
198
|
-
.task { await
|
|
180
|
+
.searchable(text: $model.filter)
|
|
181
|
+
.task { await model.load() }
|
|
199
182
|
}
|
|
200
183
|
}
|
|
201
184
|
```
|
|
202
185
|
|
|
203
|
-
|
|
186
|
+
The payoff is a view model you can test without any view:
|
|
204
187
|
|
|
205
188
|
```swift
|
|
206
|
-
@Test
|
|
207
|
-
|
|
208
|
-
|
|
209
|
-
|
|
210
|
-
|
|
211
|
-
|
|
212
|
-
vm.searchText = "Paris"
|
|
213
|
-
#expect(vm.filteredTrips.count == 2)
|
|
189
|
+
@Test @MainActor
|
|
190
|
+
func filterMatchesTitle() async {
|
|
191
|
+
let model = WorkoutListModel(store: FakeWorkoutStore(titles: ["Row", "Run", "Swim"]))
|
|
192
|
+
await model.load()
|
|
193
|
+
model.filter = "ru"
|
|
194
|
+
#expect(model.visibleRows.count == 1)
|
|
214
195
|
}
|
|
215
196
|
```
|
|
216
197
|
|
|
217
198
|
## MVI
|
|
218
199
|
|
|
219
|
-
|
|
220
|
-
|
|
200
|
+
Data moves one way. The view emits intents, a single function applies each
|
|
201
|
+
intent to the state, and any work with side effects is spelled out there.
|
|
221
202
|
|
|
222
203
|
```swift
|
|
223
204
|
@MainActor
|
|
224
205
|
@Observable
|
|
225
|
-
final class
|
|
226
|
-
private(set) var state = State()
|
|
227
|
-
|
|
206
|
+
final class WorkoutFeed {
|
|
228
207
|
struct State {
|
|
229
|
-
var
|
|
230
|
-
var
|
|
231
|
-
var
|
|
208
|
+
var sessions: [Workout] = []
|
|
209
|
+
var loading = false
|
|
210
|
+
var message: String?
|
|
232
211
|
}
|
|
233
212
|
|
|
234
213
|
enum Intent {
|
|
235
|
-
case
|
|
236
|
-
case
|
|
237
|
-
case
|
|
214
|
+
case appeared
|
|
215
|
+
case remove(Workout.ID)
|
|
216
|
+
case dismissMessage
|
|
238
217
|
}
|
|
239
218
|
|
|
240
|
-
private
|
|
219
|
+
private(set) var state = State()
|
|
220
|
+
private let store: any WorkoutStore
|
|
241
221
|
|
|
242
|
-
init(
|
|
243
|
-
self.service = service
|
|
244
|
-
}
|
|
222
|
+
init(store: any WorkoutStore) { self.store = store }
|
|
245
223
|
|
|
246
224
|
func send(_ intent: Intent) {
|
|
247
|
-
Task { await
|
|
225
|
+
Task { await apply(intent) }
|
|
248
226
|
}
|
|
249
227
|
|
|
250
|
-
private func
|
|
251
|
-
switch
|
|
252
|
-
case .
|
|
253
|
-
state.
|
|
228
|
+
private func apply(_ next: Intent) async {
|
|
229
|
+
switch next {
|
|
230
|
+
case .appeared:
|
|
231
|
+
state.loading = true
|
|
232
|
+
do { state.sessions = try await store.allWorkouts() }
|
|
233
|
+
catch { state.message = error.localizedDescription }
|
|
234
|
+
state.loading = false
|
|
235
|
+
case .remove(let id):
|
|
254
236
|
do {
|
|
255
|
-
|
|
237
|
+
try await store.delete(id)
|
|
238
|
+
state.sessions.removeAll { $0.id == id }
|
|
256
239
|
} catch {
|
|
257
|
-
state.
|
|
240
|
+
state.message = error.localizedDescription
|
|
258
241
|
}
|
|
259
|
-
|
|
260
|
-
|
|
261
|
-
case .deleteTrip(let trip):
|
|
262
|
-
try? await service.delete(trip)
|
|
263
|
-
state.trips.removeAll { $0.id == trip.id }
|
|
264
|
-
|
|
265
|
-
case .clearError:
|
|
266
|
-
state.error = nil
|
|
242
|
+
case .dismissMessage:
|
|
243
|
+
state.message = nil
|
|
267
244
|
}
|
|
268
245
|
}
|
|
269
246
|
}
|
|
270
247
|
```
|
|
271
248
|
|
|
272
|
-
|
|
273
|
-
|
|
249
|
+
What it buys: every transition is predictable, intents can be logged and
|
|
250
|
+
replayed, and "what happened" (the intent) is kept apart from "what changed"
|
|
251
|
+
(the state).
|
|
274
252
|
|
|
275
253
|
## TCA
|
|
276
254
|
|
|
277
|
-
The Composable Architecture
|
|
278
|
-
|
|
279
|
-
|
|
280
|
-
Docs: [TCA](https://sosumi.ai/external/https://swiftpackageindex.com/pointfreeco/swift-composable-architecture/main/documentation/composablearchitecture)
|
|
255
|
+
The Composable Architecture is an open-source library. Features are reducers
|
|
256
|
+
that compose; dependencies are injected through a registry; effects are values
|
|
257
|
+
the store runs; tests can assert every state change exhaustively.
|
|
281
258
|
|
|
282
259
|
```swift
|
|
283
260
|
import ComposableArchitecture
|
|
284
261
|
|
|
285
262
|
@Reducer
|
|
286
|
-
struct
|
|
263
|
+
struct WorkoutsFeature {
|
|
287
264
|
@ObservableState
|
|
288
265
|
struct State: Equatable {
|
|
289
|
-
var
|
|
290
|
-
var
|
|
291
|
-
var errorMessage: String?
|
|
266
|
+
var sessions: IdentifiedArrayOf<Workout> = []
|
|
267
|
+
var loading = false
|
|
292
268
|
}
|
|
293
269
|
|
|
294
270
|
enum Action {
|
|
295
271
|
case onAppear
|
|
296
|
-
case
|
|
297
|
-
case
|
|
298
|
-
case deleteTrip(Trip.ID)
|
|
272
|
+
case loaded([Workout])
|
|
273
|
+
case removeTapped(Workout.ID)
|
|
299
274
|
}
|
|
300
275
|
|
|
301
|
-
@Dependency(\.
|
|
276
|
+
@Dependency(\.workoutClient) var workoutClient
|
|
302
277
|
|
|
303
278
|
var body: some ReducerOf<Self> {
|
|
304
279
|
Reduce { state, action in
|
|
305
280
|
switch action {
|
|
306
|
-
case .onAppear:
|
|
307
|
-
state.
|
|
308
|
-
state.errorMessage = nil
|
|
281
|
+
case .onAppear where !state.loading:
|
|
282
|
+
state.loading = true
|
|
309
283
|
return .run { send in
|
|
310
|
-
|
|
311
|
-
|
|
312
|
-
await send(.tripsLoaded(trips))
|
|
313
|
-
} catch {
|
|
314
|
-
await send(.tripsFailed(error.localizedDescription))
|
|
315
|
-
}
|
|
284
|
+
let sessions = try await workoutClient.fetchAll()
|
|
285
|
+
await send(.loaded(sessions))
|
|
316
286
|
}
|
|
317
|
-
case .
|
|
318
|
-
state.
|
|
319
|
-
state.
|
|
287
|
+
case .loaded(let sessions):
|
|
288
|
+
state.loading = false
|
|
289
|
+
state.sessions = IdentifiedArray(uniqueElements: sessions)
|
|
320
290
|
return .none
|
|
321
|
-
case .
|
|
322
|
-
state.errorMessage = message
|
|
323
|
-
state.isLoading = false
|
|
291
|
+
case .onAppear:
|
|
324
292
|
return .none
|
|
325
|
-
case .
|
|
326
|
-
state.
|
|
327
|
-
return .run { _ in
|
|
293
|
+
case .removeTapped(let id):
|
|
294
|
+
state.sessions.remove(id: id)
|
|
295
|
+
return .run { [id] _ in
|
|
296
|
+
try await workoutClient.delete(id)
|
|
297
|
+
}
|
|
328
298
|
}
|
|
329
299
|
}
|
|
330
300
|
}
|
|
331
301
|
}
|
|
332
302
|
```
|
|
333
303
|
|
|
334
|
-
|
|
335
|
-
|
|
336
|
-
|
|
304
|
+
Choose it for complex flows that must be deterministic, effects that need
|
|
305
|
+
sequencing, features composed from smaller features, reducer-level testing, and
|
|
306
|
+
dependency injection across the whole app.
|
|
337
307
|
|
|
338
308
|
## Clean Architecture
|
|
339
309
|
|
|
340
|
-
|
|
341
|
-
|
|
342
|
-
|
|
310
|
+
Three layers with dependencies pointing inward:
|
|
311
|
+
|
|
312
|
+
- **Domain**: the entities, the use cases, and the protocols repositories must
|
|
313
|
+
satisfy. It imports no UI or persistence framework.
|
|
314
|
+
- **Data**: repository implementations, networking, persistence.
|
|
315
|
+
- **Presentation**: views and view models.
|
|
343
316
|
|
|
344
317
|
```swift
|
|
345
|
-
// Domain
|
|
346
|
-
protocol
|
|
347
|
-
func fetchAll() async throws -> [
|
|
348
|
-
func save(_
|
|
349
|
-
func delete(id:
|
|
318
|
+
// Domain
|
|
319
|
+
protocol WorkoutRepository: Sendable {
|
|
320
|
+
func fetchAll() async throws -> [Workout]
|
|
321
|
+
func save(_ workout: Workout) async throws
|
|
322
|
+
func delete(_ id: Workout.ID) async throws
|
|
350
323
|
}
|
|
351
324
|
|
|
352
|
-
struct
|
|
353
|
-
|
|
354
|
-
|
|
355
|
-
init(repository: TripRepository) {
|
|
356
|
-
self.repository = repository
|
|
357
|
-
}
|
|
325
|
+
struct UpcomingWorkouts: Sendable {
|
|
326
|
+
let repository: any WorkoutRepository
|
|
358
327
|
|
|
359
|
-
func
|
|
360
|
-
try await repository.fetchAll()
|
|
361
|
-
|
|
362
|
-
.
|
|
328
|
+
func callAsFunction(after now: Date = .now) async throws -> [Workout] {
|
|
329
|
+
let everything = try await repository.fetchAll()
|
|
330
|
+
return everything
|
|
331
|
+
.filter { $0.start > now }
|
|
332
|
+
.sorted { $0.start < $1.start }
|
|
363
333
|
}
|
|
364
334
|
}
|
|
365
335
|
|
|
366
|
-
// Data
|
|
367
|
-
struct
|
|
368
|
-
|
|
336
|
+
// Data
|
|
337
|
+
struct RemoteWorkoutRepository: WorkoutRepository {
|
|
338
|
+
let api: FitnessAPI
|
|
369
339
|
|
|
370
|
-
func fetchAll() async throws -> [
|
|
371
|
-
|
|
372
|
-
}
|
|
373
|
-
// ...
|
|
340
|
+
func fetchAll() async throws -> [Workout] { try await api.get("/workouts") }
|
|
341
|
+
func save(_ workout: Workout) async throws { try await api.post("/workouts", body: workout) }
|
|
342
|
+
func delete(_ id: Workout.ID) async throws { try await api.delete("/workouts/\(id)") }
|
|
374
343
|
}
|
|
375
344
|
|
|
376
|
-
// Presentation
|
|
345
|
+
// Presentation
|
|
377
346
|
@MainActor
|
|
378
347
|
@Observable
|
|
379
|
-
final class
|
|
380
|
-
private(set) var
|
|
381
|
-
private let
|
|
348
|
+
final class PlannerModel {
|
|
349
|
+
private(set) var upcoming: [Workout] = []
|
|
350
|
+
private let upcomingWorkouts: UpcomingWorkouts
|
|
382
351
|
|
|
383
|
-
init(
|
|
384
|
-
self.useCase = useCase
|
|
385
|
-
}
|
|
352
|
+
init(upcomingWorkouts: UpcomingWorkouts) { self.upcomingWorkouts = upcomingWorkouts }
|
|
386
353
|
|
|
387
|
-
func load() async {
|
|
388
|
-
trips = (try? await useCase.execute()) ?? []
|
|
389
|
-
}
|
|
354
|
+
func load() async { upcoming = (try? await upcomingWorkouts()) ?? [] }
|
|
390
355
|
}
|
|
391
356
|
```
|
|
392
357
|
|
|
393
|
-
|
|
394
|
-
|
|
395
|
-
|
|
396
|
-
|
|
397
|
-
## Coordinator Pattern
|
|
398
|
-
|
|
399
|
-
Separates navigation logic from views. Especially useful in UIKit or hybrid
|
|
400
|
-
apps with complex navigation flows.
|
|
401
|
-
|
|
402
|
-
Keep Coordinators `@MainActor`, inject dependencies at coordinator creation,
|
|
403
|
-
and pass user-selection callbacks from view models or controllers back to the
|
|
404
|
-
coordinator. The coordinator owns push/modal decisions; feature models own
|
|
405
|
-
business logic.
|
|
406
|
-
|
|
407
|
-
In pure SwiftUI apps, `NavigationStack` with path-based routing often
|
|
408
|
-
replaces the Coordinator pattern. Use Coordinators when you need UIKit
|
|
409
|
-
integration or shared navigation logic across platforms.
|
|
410
|
-
|
|
411
|
-
## VIPER
|
|
358
|
+
Reach for it when rules or regulation demand hard separation, when domain code
|
|
359
|
+
has to be tested with no framework present, or when an app, a widget and a
|
|
360
|
+
watch app all run one set of business rules.
|
|
412
361
|
|
|
413
|
-
|
|
414
|
-
**Entity**, and **Router** roles. Treat it as a maintenance pattern for apps
|
|
415
|
-
that already have strict UIKit module boundaries rather than a default for new
|
|
416
|
-
SwiftUI work.
|
|
362
|
+
## Coordinator pattern
|
|
417
363
|
|
|
418
|
-
|
|
419
|
-
|
|
420
|
-
business logic, and routing, or a migration must preserve module boundaries
|
|
421
|
-
while modernizing internals.
|
|
364
|
+
Coordinators take navigation out of views and controllers. They pay off mostly
|
|
365
|
+
in UIKit and hybrid apps.
|
|
422
366
|
|
|
423
|
-
|
|
424
|
-
|
|
425
|
-
|
|
426
|
-
|
|
427
|
-
|
|
428
|
-
### ObservableObject → `@Observable`
|
|
367
|
+
- A coordinator is `@MainActor` and receives its dependencies when it is
|
|
368
|
+
created.
|
|
369
|
+
- View models and view controllers report user choices through callbacks; the
|
|
370
|
+
coordinator decides whether to push or present.
|
|
371
|
+
- Feature models keep the business logic. The coordinator only routes.
|
|
429
372
|
|
|
430
373
|
```swift
|
|
431
|
-
// Before (iOS 16)
|
|
432
|
-
class TripStore: ObservableObject {
|
|
433
|
-
@Published var trips: [Trip] = []
|
|
434
|
-
}
|
|
435
|
-
// View uses @ObservedObject or @StateObject
|
|
436
|
-
|
|
437
|
-
// After (iOS 17+)
|
|
438
374
|
@MainActor
|
|
439
|
-
|
|
440
|
-
|
|
441
|
-
|
|
442
|
-
}
|
|
443
|
-
// View uses @State for owned; plain injection or @Bindable only when needed
|
|
444
|
-
```
|
|
445
|
-
|
|
446
|
-
Migration routing: keep Coordinators for UIKit or hybrid boundaries; pure
|
|
447
|
-
SwiftUI flows usually own `NavigationStack`/path state. Route detailed route
|
|
448
|
-
enums, `NavigationSplitView`, sheets, tabs, and deep links to
|
|
449
|
-
`swiftui-navigation`, strict-concurrency diagnostics to `swift-concurrency`,
|
|
450
|
-
and fixtures or parameterized tests to `swift-testing`. Migrate per feature module, not app-wide by default; keep each module internally consistent while allowing different modules to use different patterns during incremental adoption.
|
|
451
|
-
|
|
452
|
-
### MVVM → MV (simplifying)
|
|
375
|
+
final class OnboardingCoordinator {
|
|
376
|
+
private let navigation: UINavigationController
|
|
377
|
+
private let account: AccountService
|
|
453
378
|
|
|
454
|
-
|
|
455
|
-
|
|
456
|
-
|
|
457
|
-
|
|
458
|
-
|
|
459
|
-
Extract business logic and data transformation into a view model when:
|
|
460
|
-
- The view's `body` contains conditional logic for data formatting
|
|
461
|
-
- Multiple views need different projections of the same model
|
|
462
|
-
- You need to test logic without instantiating views
|
|
379
|
+
init(navigation: UINavigationController, account: AccountService) {
|
|
380
|
+
self.navigation = navigation
|
|
381
|
+
self.account = account
|
|
382
|
+
}
|
|
463
383
|
|
|
464
|
-
|
|
384
|
+
func start() {
|
|
385
|
+
let welcome = WelcomeViewController(onContinue: { [weak self] in self?.showSignUp() })
|
|
386
|
+
navigation.setViewControllers([welcome], animated: false)
|
|
387
|
+
}
|
|
465
388
|
|
|
466
|
-
|
|
467
|
-
|
|
389
|
+
private func showSignUp() {
|
|
390
|
+
let form = SignUpViewController(account: account, onDone: { [weak self] in self?.finish() })
|
|
391
|
+
navigation.pushViewController(form, animated: true)
|
|
392
|
+
}
|
|
468
393
|
|
|
469
|
-
|
|
394
|
+
private func finish() { navigation.dismiss(animated: true) }
|
|
395
|
+
}
|
|
396
|
+
```
|
|
470
397
|
|
|
471
|
-
|
|
472
|
-
|
|
473
|
-
|
|
474
|
-
| View model that only forwards model properties | Remove the view model; use MV pattern |
|
|
475
|
-
| Massive view model with navigation, networking, and formatting | Split into focused collaborators (coordinator, service, formatter) |
|
|
476
|
-
| Choosing TCA for a two-screen app | Start with MV; adopt TCA when composition and testing demands justify it |
|
|
477
|
-
| Protocol-heavy Clean Architecture for a simple feature | Match architecture complexity to feature complexity |
|
|
478
|
-
| Coordinator pattern in pure SwiftUI without UIKit needs | Use `NavigationStack` path-based routing instead |
|
|
479
|
-
| Starting new SwiftUI modules with VIPER | Reserve VIPER for legacy UIKit maintenance or strict module-boundary migrations |
|
|
480
|
-
| Mixing architecture patterns inside one feature module | Keep one pattern inside each feature module; migrate different modules independently when needed |
|
|
398
|
+
In a pure SwiftUI app, a `NavigationStack` driven by a path usually does this
|
|
399
|
+
job without a coordinator. Keep coordinators for UIKit integration or for
|
|
400
|
+
navigation logic shared across platforms.
|
|
481
401
|
|
|
482
|
-
##
|
|
402
|
+
## VIPER
|
|
483
403
|
|
|
484
|
-
|
|
485
|
-
|
|
486
|
-
|
|
487
|
-
|
|
488
|
-
|
|
489
|
-
|
|
490
|
-
|
|
491
|
-
|
|
404
|
+
View, Interactor, Presenter, Entity, Router. Its value today is keeping
|
|
405
|
+
existing UIKit modules with hard boundaries maintainable; it is not where new
|
|
406
|
+
SwiftUI work should begin.
|
|
407
|
+
|
|
408
|
+
Use it when an existing UIKit codebase is already organized into VIPER modules,
|
|
409
|
+
when teams depend on its explicit handoff contracts, or when a migration has to
|
|
410
|
+
keep those module boundaries intact. For new SwiftUI features, pick one of the
|
|
411
|
+
lighter patterns above; they need far fewer files for the same result.
|
|
412
|
+
|
|
413
|
+
## Migration between patterns
|
|
414
|
+
|
|
415
|
+
### ObservableObject to @Observable
|
|
416
|
+
|
|
417
|
+
| Before (iOS 16) | After (iOS 17+) |
|
|
418
|
+
| --- | --- |
|
|
419
|
+
| `class Model: ObservableObject` with `@Published` properties | `@MainActor @Observable final class Model` with plain stored properties |
|
|
420
|
+
| `@StateObject var model` | `@State private var model` for a model the view owns |
|
|
421
|
+
| `@ObservedObject var model` | A plain `let` or `var` property for an injected model the view only reads |
|
|
422
|
+
| `$model.property` bindings through `@ObservedObject` | `@Bindable var model` only where a binding is needed |
|
|
423
|
+
|
|
424
|
+
The wiring details belong to `swiftui-patterns`.
|
|
425
|
+
|
|
426
|
+
### Routing during a migration
|
|
427
|
+
|
|
428
|
+
- Keep coordinators at UIKit and hybrid boundaries; let pure SwiftUI areas own
|
|
429
|
+
a path-driven `NavigationStack`.
|
|
430
|
+
- Anything about route enums, split views, sheet or tab presentation, or deep
|
|
431
|
+
link handling goes to `swiftui-navigation`.
|
|
432
|
+
- Isolation and `Sendable` errors that appear during the move go to
|
|
433
|
+
`swift-concurrency`.
|
|
434
|
+
- Test fixtures and parameterized tests go to `swift-testing`.
|
|
435
|
+
|
|
436
|
+
### Strategy
|
|
437
|
+
|
|
438
|
+
Migrate one feature module at a time, not the whole app at once. Each module
|
|
439
|
+
stays internally consistent; different modules may use different patterns
|
|
440
|
+
while adoption is in progress.
|
|
441
|
+
|
|
442
|
+
- **MVVM to MV**: delete view models that only forward model properties.
|
|
443
|
+
- **MV to MVVM**: when `body` fills up with formatting conditionals, when two
|
|
444
|
+
screens want the same data shaped differently, or when logic must be tested
|
|
445
|
+
without a view.
|
|
446
|
+
- **Any pattern to TCA**: go incrementally. Pick one feature, express its state
|
|
447
|
+
and actions as a `Reducer`, move what it depends on behind `@Dependency`,
|
|
448
|
+
cover it with tests, then take the next feature.
|
|
449
|
+
|
|
450
|
+
## Common mistakes
|
|
451
|
+
|
|
452
|
+
| Symptom | Fix |
|
|
453
|
+
| --- | --- |
|
|
454
|
+
| New iOS 17+ code still built on `ObservableObject` | Switch to `@Observable` and keep state the UI reads on `@MainActor`, which Swift 6 needs to rule out data races |
|
|
455
|
+
| A view model whose properties just mirror the model | Delete the layer; MV is enough |
|
|
456
|
+
| One oversized view model that routes, fetches and formats | Give routing, fetching and formatting to separate collaborators |
|
|
457
|
+
| TCA adopted for an app with two screens | Begin with MV and escalate later |
|
|
458
|
+
| Protocols and layers everywhere for a small feature | Size the structure to the problem |
|
|
459
|
+
| Coordinators in an all-SwiftUI app with no UIKit | Let a path-driven `NavigationStack` route |
|
|
460
|
+
| VIPER chosen for a brand-new SwiftUI module | Leave VIPER to the legacy code |
|
|
461
|
+
| Two or three patterns mixed in one feature module | Settle on one per module |
|
|
462
|
+
|
|
463
|
+
## Review checklist
|
|
464
|
+
|
|
465
|
+
- [ ] The pattern is justified by the feature's complexity and the team's needs
|
|
466
|
+
- [ ] Each model or store has a named owner; the `@State`, injection and `@Bindable` details are left to `swiftui-patterns`
|
|
467
|
+
- [ ] Models receive their dependencies; they never construct them
|
|
468
|
+
- [ ] Handoffs are explicit: MV wiring, split view layout, concurrency errors, fixtures and parameterized tests each point at their skill
|
|
469
|
+
- [ ] State changes happen in one clear, auditable place
|
|
470
|
+
- [ ] View models and stores can be exercised in tests with no view involved
|
|
471
|
+
- [ ] No single object has taken over the feature
|
|
472
|
+
- [ ] Each feature module uses one pattern consistently, including mid-migration
|
|
492
473
|
|
|
493
474
|
## References
|
|
494
475
|
|
|
495
|
-
-
|
|
496
|
-
-
|
|
497
|
-
-
|
|
498
|
-
-
|
|
499
|
-
-
|
|
500
|
-
-
|
|
476
|
+
- [Observation](https://developer.apple.com/documentation/observation)
|
|
477
|
+
- [Observable()](https://developer.apple.com/documentation/observation/observable())
|
|
478
|
+
- [Migrating from the Observable Object protocol to the Observable macro](https://developer.apple.com/documentation/swiftui/migrating-from-the-observable-object-protocol-to-the-observable-macro)
|
|
479
|
+
- [State](https://developer.apple.com/documentation/swiftui/state)
|
|
480
|
+
- [Bindable](https://developer.apple.com/documentation/swiftui/bindable)
|
|
481
|
+
- [Environment](https://developer.apple.com/documentation/swiftui/environment)
|
|
482
|
+
- [NavigationStack](https://developer.apple.com/documentation/swiftui/navigationstack)
|
|
483
|
+
- [Swift Testing](https://developer.apple.com/documentation/testing)
|
|
484
|
+
- [The Composable Architecture documentation](https://swiftpackageindex.com/pointfreeco/swift-composable-architecture/main/documentation/composablearchitecture)
|