SwiftData Best Practices — Modular MVVM-C Data Layer
Comprehensive data modeling, persistence, sync architecture, and error handling guide for SwiftData aligned with the clinic modular MVVM-C stack.
Architecture Alignment
This skill enforces the same modular architecture mandated by swift-ui-architect:
┌───────────────────────────────────────────────────────────────┐
│ Feature modules: View + ViewModel, no SwiftData imports │
├───────────────────────────────────────────────────────────────┤
│ Domain: models + repository/coordinator/error protocols │
├───────────────────────────────────────────────────────────────┤
│ Data: @Model entities, SwiftData stores, repository impls, │
│ remote clients, retry executor, sync queue, conflict handling │
└───────────────────────────────────────────────────────────────┘
Key principle: SwiftData types (@Model, ModelContext, @Query, FetchDescriptor) live in Data-only implementation code. Feature Views/ViewModels work with Domain types and protocol dependencies.
Clinic Architecture Contract (iOS 26 / Swift 6.2)
All guidance in this skill assumes the clinic modular MVVM-C architecture:
- Feature modules import
Domain + DesignSystem only (never Data, never sibling features)
- App target is the convergence point and owns
DependencyContainer, concrete coordinators, and Route Shell wiring
Domain stays pure Swift and defines models plus repository, *Coordinating, ErrorRouting, and AppError contracts
Data owns SwiftData/network/sync/retry/background I/O and implements Domain protocols
- Read/write flow defaults to stale-while-revalidate reads and optimistic queued writes
- ViewModels call repository protocols directly (no default use-case/interactor layer)
When to Apply
Reference these guidelines when:
- Defining @Model entity classes and mapping them to domain structs
- Setting up ModelContainer and ModelContext in the Data layer
- Implementing repository protocols backed by SwiftData
- Writing stale-while-revalidate repository reads (
AsyncStream)
- Implementing optimistic writes plus queued sync operations
- Configuring entity relationships (one-to-many, inverse)
- Fetching from APIs and persisting to SwiftData via sync coordinators
- Handling save failures, corrupt stores, and migration errors
- Routing AppError traits to centralized error UI infrastructure
- Building preview infrastructure with sample data
- Planning schema migrations for app updates
Workflow
Use this workflow when designing or refactoring a SwiftData-backed feature:
- Domain design: define domain structs (
Trip, Friend) with validation/computed rules (see model-domain-mapping, state-business-logic-placement)
- Entity design: define
@Model entity classes with mapping methods (see model-*, model-domain-mapping)
- Repository protocol: define in Domain layer, implement with SwiftData in Data layer (see
persist-repository-wrapper)
- Container wiring: configure
ModelContainer once at the app boundary with error recovery (see persist-container-setup, persist-container-error-recovery)
- Dependency injection: inject repository protocols via @Environment (see
state-dependency-injection)
- ViewModel: create @Observable ViewModel that delegates directly to repository protocols (see
state-query-vs-viewmodel)
- CRUD flows: route all insert/delete/update through ViewModel -> Repository (see
crud-*)
- Sync architecture: queue writes, execute via sync coordinator with retry policy (see
sync-*)
- Relationships: model to-many relationships as arrays; define delete rules (see
rel-*)
- Previews: create in-memory containers and sample data for fast iteration (see
preview-*)
- Schema evolution: plan migrations with versioned schemas (see
schema-*)
Troubleshooting
- Data not persisting ->
persist-model-macro, persist-container-setup, persist-autosave, schema-configuration
- List not updating after background import ->
query-background-refresh, persist-model-actor
- List not updating (same-context) ->
query-property-wrapper, state-wrapper-views
- Duplicates from API sync ->
schema-unique-attributes, sync-conflict-resolution
- App crashes on launch after model change ->
schema-migration-recovery, persist-container-error-recovery
- Save failures silently losing data ->
crud-save-error-handling
- Stale data from network ->
sync-offline-first, sync-fetch-persist
- Widget/extension can't see data ->
persist-app-group, schema-configuration
- Choosing architecture pattern for data views ->
state-query-vs-viewmodel, persist-repository-wrapper
Rule Categories by Priority
| Priority |
Category |
Impact |
Prefix |
| 1 |
Data Modeling |
CRITICAL |
model- |
| 2 |
Persistence Setup |
CRITICAL |
persist- |
| 3 |
Querying & Filtering |
HIGH |
query- |
| 4 |
CRUD Operations |
HIGH |
crud- |
| 5 |
Sync & Networking |
HIGH |
sync- |
| 6 |
Relationships |
MEDIUM-HIGH |
rel- |
| 7 |
SwiftUI State Flow |
MEDIUM-HIGH |
state- |
| 8 |
Schema & Migration |
MEDIUM-HIGH |
schema- |
| 9 |
Sample Data & Previews |
MEDIUM |
preview- |
Quick Reference
1. Data Modeling (CRITICAL)
model-domain-mapping - Map @Model entities to domain structs across Domain/Data boundaries
model-custom-types - Use custom types over parallel arrays
model-class-for-persistence - Use classes for SwiftData entity types
model-identifiable - Conform entities to Identifiable with UUID
model-initializer - Provide custom initializers for entity classes
model-computed-properties - Use computed properties for derived data
model-defaults - Provide sensible default values for entity properties
model-transient - Mark non-persistent properties with @Transient
model-external-storage - Use external storage for large binary data
2. Persistence Setup (CRITICAL)
persist-repository-wrapper - Wrap SwiftData behind Domain repository protocols
persist-model-macro - Apply @Model macro to all persistent types
persist-container-setup - Configure ModelContainer at the App level
persist-container-error-recovery - Handle ModelContainer creation failure with store recovery
persist-context-environment - Access ModelContext via @Environment (Data layer)
persist-autosave - Enable autosave for manually created contexts
persist-enumerate-batch - Use ModelContext.enumerate for large traversals
persist-in-memory-config - Use in-memory configuration for tests and previews
persist-app-group - Use App Groups for shared data storage
persist-model-actor - Use @ModelActor for background SwiftData work
persist-identifier-transfer - Pass PersistentIdentifier across actors
3. Querying & Filtering (HIGH)
query-property-wrapper - Use @Query for declarative data fetching (Data layer)
query-background-refresh - Force view refresh after background context inserts
query-sort-descriptors - Apply sort descriptors to @Query
query-predicates - Use #Predicate for type-safe filtering
query-dynamic-init - Use custom view initializers for dynamic queries
query-fetch-descriptor - Use FetchDescriptor outside SwiftUI views
query-fetch-tuning - Tune FetchDescriptor paging and pending-change behavior
query-localized-search - Use localizedStandardContains for search
query-expression - Use #Expression for reusable predicate components (iOS 18+)
4. CRUD Operations (HIGH)
crud-insert-context - Insert models via repository implementations
crud-delete-indexset - Delete via repository with IndexSet from onDelete
crud-sheet-creation - Use sheets for focused data creation via ViewModel
crud-cancel-delete - Avoid orphaned records by persisting only on save
crud-undo-cancel - Enable undo and use it to cancel edits
crud-edit-button - Provide EditButton for list management
crud-dismiss-save - Dismiss modal after ViewModel save completes
crud-save-error-handling - Handle repository save failures with user feedback
5. Sync & Networking (HIGH)
sync-fetch-persist - Use injected sync services to fetch and persist API data
sync-offline-first - Design offline-first architecture with repository reads and background sync
sync-conflict-resolution - Implement conflict resolution for bidirectional sync
6. Relationships (MEDIUM-HIGH)
rel-optional-single - Use optionals for optional relationships
rel-array-many - Use arrays for one-to-many relationships
rel-inverse-auto - Rely on SwiftData automatic inverse maintenance
rel-delete-rules - Configure cascade delete rules for owned relationships
rel-explicit-sort - Sort relationship arrays explicitly
7. SwiftUI State Flow (MEDIUM-HIGH)
state-query-vs-viewmodel - Route all data access through @Observable ViewModels
state-business-logic-placement - Place business logic in domain value types and repository-backed ViewModels
state-dependency-injection - Inject repository protocols via @Environment
state-bindable - Use @Bindable for two-way model binding
state-local-state - Use @State for view-local transient data
state-wrapper-views - Extract wrapper views for dynamic query state
8. Schema & Migration (MEDIUM-HIGH)
schema-define-all-types - Define schema with all model types
schema-unique-attributes - Use @Attribute(.unique) for natural keys
schema-unique-macro - Use #Unique for compound uniqueness (iOS 18+)
schema-index - Use #Index for hot predicates and sorts (iOS 18+)
schema-migration-plan - Plan migrations before changing models
schema-migration-recovery - Plan migration recovery for schema changes
schema-configuration - Customize storage with ModelConfiguration
9. Sample Data & Previews (MEDIUM)
preview-sample-singleton - Create a SampleData singleton for previews
preview-in-memory - Use in-memory containers for preview isolation
preview-static-data - Define static sample data on model types
preview-main-actor - Annotate SampleData with @MainActor
How to Use
Read individual reference files for detailed explanations and code examples:
- Section definitions - Category structure and impact levels
- Rule template - Template for adding new rules
Reference Files
| File |
Description |
| references/_sections.md |
Category definitions and ordering |
| assets/templates/_template.md |
Template for new rules |
| metadata.json |
Version and reference information |
1---2name: swift-data3description: SwiftData persistence and data-layer architecture for iOS 26 / Swift 6.2 clinic modular MVVM-C apps. Use when writing, reviewing, or refactoring @Model entities, repository implementations, stale-while-revalidate reads, optimistic queued writes, sync/retry behavior, and SwiftUI integration that keeps SwiftData types inside Data-only boundaries.4---5
6# SwiftData Best Practices — Modular MVVM-C Data Layer
7
8Comprehensive data modeling, persistence, sync architecture, and error handling guide for SwiftData aligned with the clinic modular MVVM-C stack.
9
10## Architecture Alignment
11
12This skill enforces the same modular architecture mandated by `swift-ui-architect`:
13
14```
15┌───────────────────────────────────────────────────────────────┐
16│ Feature modules: View + ViewModel, no SwiftData imports │
17├───────────────────────────────────────────────────────────────┤
18│ Domain: models + repository/coordinator/error protocols │
19├───────────────────────────────────────────────────────────────┤
20│ Data: @Model entities, SwiftData stores, repository impls, │
21│ remote clients, retry executor, sync queue, conflict handling │
22└───────────────────────────────────────────────────────────────┘
23```
24
25**Key principle:** SwiftData types (`@Model`, `ModelContext`, `@Query`, `FetchDescriptor`) live in Data-only implementation code. Feature Views/ViewModels work with Domain types and protocol dependencies.
26
27
28## Clinic Architecture Contract (iOS 26 / Swift 6.2)
29
30All guidance in this skill assumes the clinic modular MVVM-C architecture:
31
32- Feature modules import `Domain` + `DesignSystem` only (never `Data`, never sibling features)
33- App target is the convergence point and owns `DependencyContainer`, concrete coordinators, and Route Shell wiring
34- `Domain` stays pure Swift and defines models plus repository, `*Coordinating`, `ErrorRouting`, and `AppError` contracts
35- `Data` owns SwiftData/network/sync/retry/background I/O and implements Domain protocols
36- Read/write flow defaults to stale-while-revalidate reads and optimistic queued writes
37- ViewModels call repository protocols directly (no default use-case/interactor layer)
38
39## When to Apply
40
41Reference these guidelines when:
42- Defining @Model entity classes and mapping them to domain structs
43- Setting up ModelContainer and ModelContext in the Data layer
44- Implementing repository protocols backed by SwiftData
45- Writing stale-while-revalidate repository reads (`AsyncStream`)
46- Implementing optimistic writes plus queued sync operations
47- Configuring entity relationships (one-to-many, inverse)
48- Fetching from APIs and persisting to SwiftData via sync coordinators
49- Handling save failures, corrupt stores, and migration errors
50- Routing AppError traits to centralized error UI infrastructure
51- Building preview infrastructure with sample data
52- Planning schema migrations for app updates
53
54## Workflow
55
56Use this workflow when designing or refactoring a SwiftData-backed feature:
57
581. Domain design: define domain structs (`Trip`, `Friend`) with validation/computed rules (see `model-domain-mapping`, `state-business-logic-placement`)
592. Entity design: define `@Model` entity classes with mapping methods (see `model-*`, `model-domain-mapping`)
603. Repository protocol: define in Domain layer, implement with SwiftData in Data layer (see `persist-repository-wrapper`)
614. Container wiring: configure `ModelContainer` once at the app boundary with error recovery (see `persist-container-setup`, `persist-container-error-recovery`)
625. Dependency injection: inject repository protocols via @Environment (see `state-dependency-injection`)
636. ViewModel: create @Observable ViewModel that delegates directly to repository protocols (see `state-query-vs-viewmodel`)
647. CRUD flows: route all insert/delete/update through ViewModel -> Repository (see `crud-*`)
658. Sync architecture: queue writes, execute via sync coordinator with retry policy (see `sync-*`)
669. Relationships: model to-many relationships as arrays; define delete rules (see `rel-*`)
6710. Previews: create in-memory containers and sample data for fast iteration (see `preview-*`)
6811. Schema evolution: plan migrations with versioned schemas (see `schema-*`)
69
70## Troubleshooting
71
72- Data not persisting -> `persist-model-macro`, `persist-container-setup`, `persist-autosave`, `schema-configuration`
73- List not updating after background import -> `query-background-refresh`, `persist-model-actor`
74- List not updating (same-context) -> `query-property-wrapper`, `state-wrapper-views`
75- Duplicates from API sync -> `schema-unique-attributes`, `sync-conflict-resolution`
76- App crashes on launch after model change -> `schema-migration-recovery`, `persist-container-error-recovery`
77- Save failures silently losing data -> `crud-save-error-handling`
78- Stale data from network -> `sync-offline-first`, `sync-fetch-persist`
79- Widget/extension can't see data -> `persist-app-group`, `schema-configuration`
80- Choosing architecture pattern for data views -> `state-query-vs-viewmodel`, `persist-repository-wrapper`
81
82## Rule Categories by Priority
83
84| Priority | Category | Impact | Prefix |
85|----------|----------|--------|--------|
86| 1 | Data Modeling | CRITICAL | `model-` |
87| 2 | Persistence Setup | CRITICAL | `persist-` |
88| 3 | Querying & Filtering | HIGH | `query-` |
89| 4 | CRUD Operations | HIGH | `crud-` |
90| 5 | Sync & Networking | HIGH | `sync-` |
91| 6 | Relationships | MEDIUM-HIGH | `rel-` |
92| 7 | SwiftUI State Flow | MEDIUM-HIGH | `state-` |
93| 8 | Schema & Migration | MEDIUM-HIGH | `schema-` |
94| 9 | Sample Data & Previews | MEDIUM | `preview-` |
95
96## Quick Reference
97
98### 1. Data Modeling (CRITICAL)
99
100- [`model-domain-mapping`](references/model-domain-mapping.md) - Map @Model entities to domain structs across Domain/Data boundaries
101- [`model-custom-types`](references/model-custom-types.md) - Use custom types over parallel arrays
102- [`model-class-for-persistence`](references/model-class-for-persistence.md) - Use classes for SwiftData entity types
103- [`model-identifiable`](references/model-identifiable.md) - Conform entities to Identifiable with UUID
104- [`model-initializer`](references/model-initializer.md) - Provide custom initializers for entity classes
105- [`model-computed-properties`](references/model-computed-properties.md) - Use computed properties for derived data
106- [`model-defaults`](references/model-defaults.md) - Provide sensible default values for entity properties
107- [`model-transient`](references/model-transient.md) - Mark non-persistent properties with @Transient
108- [`model-external-storage`](references/model-external-storage.md) - Use external storage for large binary data
109
110### 2. Persistence Setup (CRITICAL)
111
112- [`persist-repository-wrapper`](references/persist-repository-wrapper.md) - Wrap SwiftData behind Domain repository protocols
113- [`persist-model-macro`](references/persist-model-macro.md) - Apply @Model macro to all persistent types
114- [`persist-container-setup`](references/persist-container-setup.md) - Configure ModelContainer at the App level
115- [`persist-container-error-recovery`](references/persist-container-error-recovery.md) - Handle ModelContainer creation failure with store recovery
116- [`persist-context-environment`](references/persist-context-environment.md) - Access ModelContext via @Environment (Data layer)
117- [`persist-autosave`](references/persist-autosave.md) - Enable autosave for manually created contexts
118- [`persist-enumerate-batch`](references/persist-enumerate-batch.md) - Use ModelContext.enumerate for large traversals
119- [`persist-in-memory-config`](references/persist-in-memory-config.md) - Use in-memory configuration for tests and previews
120- [`persist-app-group`](references/persist-app-group.md) - Use App Groups for shared data storage
121- [`persist-model-actor`](references/persist-model-actor.md) - Use @ModelActor for background SwiftData work
122- [`persist-identifier-transfer`](references/persist-identifier-transfer.md) - Pass PersistentIdentifier across actors
123
124### 3. Querying & Filtering (HIGH)
125
126- [`query-property-wrapper`](references/query-property-wrapper.md) - Use @Query for declarative data fetching (Data layer)
127- [`query-background-refresh`](references/query-background-refresh.md) - Force view refresh after background context inserts
128- [`query-sort-descriptors`](references/query-sort-descriptors.md) - Apply sort descriptors to @Query
129- [`query-predicates`](references/query-predicates.md) - Use #Predicate for type-safe filtering
130- [`query-dynamic-init`](references/query-dynamic-init.md) - Use custom view initializers for dynamic queries
131- [`query-fetch-descriptor`](references/query-fetch-descriptor.md) - Use FetchDescriptor outside SwiftUI views
132- [`query-fetch-tuning`](references/query-fetch-tuning.md) - Tune FetchDescriptor paging and pending-change behavior
133- [`query-localized-search`](references/query-localized-search.md) - Use localizedStandardContains for search
134- [`query-expression`](references/query-expression.md) - Use #Expression for reusable predicate components (iOS 18+)
135
136### 4. CRUD Operations (HIGH)
137
138- [`crud-insert-context`](references/crud-insert-context.md) - Insert models via repository implementations
139- [`crud-delete-indexset`](references/crud-delete-indexset.md) - Delete via repository with IndexSet from onDelete
140- [`crud-sheet-creation`](references/crud-sheet-creation.md) - Use sheets for focused data creation via ViewModel
141- [`crud-cancel-delete`](references/crud-cancel-delete.md) - Avoid orphaned records by persisting only on save
142- [`crud-undo-cancel`](references/crud-undo-cancel.md) - Enable undo and use it to cancel edits
143- [`crud-edit-button`](references/crud-edit-button.md) - Provide EditButton for list management
144- [`crud-dismiss-save`](references/crud-dismiss-save.md) - Dismiss modal after ViewModel save completes
145- [`crud-save-error-handling`](references/crud-save-error-handling.md) - Handle repository save failures with user feedback
146
147### 5. Sync & Networking (HIGH)
148
149- [`sync-fetch-persist`](references/sync-fetch-persist.md) - Use injected sync services to fetch and persist API data
150- [`sync-offline-first`](references/sync-offline-first.md) - Design offline-first architecture with repository reads and background sync
151- [`sync-conflict-resolution`](references/sync-conflict-resolution.md) - Implement conflict resolution for bidirectional sync
152
153### 6. Relationships (MEDIUM-HIGH)
154
155- [`rel-optional-single`](references/rel-optional-single.md) - Use optionals for optional relationships
156- [`rel-array-many`](references/rel-array-many.md) - Use arrays for one-to-many relationships
157- [`rel-inverse-auto`](references/rel-inverse-auto.md) - Rely on SwiftData automatic inverse maintenance
158- [`rel-delete-rules`](references/rel-delete-rules.md) - Configure cascade delete rules for owned relationships
159- [`rel-explicit-sort`](references/rel-explicit-sort.md) - Sort relationship arrays explicitly
160
161### 7. SwiftUI State Flow (MEDIUM-HIGH)
162
163- [`state-query-vs-viewmodel`](references/state-query-vs-viewmodel.md) - Route all data access through @Observable ViewModels
164- [`state-business-logic-placement`](references/state-business-logic-placement.md) - Place business logic in domain value types and repository-backed ViewModels
165- [`state-dependency-injection`](references/state-dependency-injection.md) - Inject repository protocols via @Environment
166- [`state-bindable`](references/state-bindable.md) - Use @Bindable for two-way model binding
167- [`state-local-state`](references/state-local-state.md) - Use @State for view-local transient data
168- [`state-wrapper-views`](references/state-wrapper-views.md) - Extract wrapper views for dynamic query state
169
170### 8. Schema & Migration (MEDIUM-HIGH)
171
172- [`schema-define-all-types`](references/schema-define-all-types.md) - Define schema with all model types
173- [`schema-unique-attributes`](references/schema-unique-attributes.md) - Use @Attribute(.unique) for natural keys
174- [`schema-unique-macro`](references/schema-unique-macro.md) - Use #Unique for compound uniqueness (iOS 18+)
175- [`schema-index`](references/schema-index.md) - Use #Index for hot predicates and sorts (iOS 18+)
176- [`schema-migration-plan`](references/schema-migration-plan.md) - Plan migrations before changing models
177- [`schema-migration-recovery`](references/schema-migration-recovery.md) - Plan migration recovery for schema changes
178- [`schema-configuration`](references/schema-configuration.md) - Customize storage with ModelConfiguration
179
180### 9. Sample Data & Previews (MEDIUM)
181
182- [`preview-sample-singleton`](references/preview-sample-singleton.md) - Create a SampleData singleton for previews
183- [`preview-in-memory`](references/preview-in-memory.md) - Use in-memory containers for preview isolation
184- [`preview-static-data`](references/preview-static-data.md) - Define static sample data on model types
185- [`preview-main-actor`](references/preview-main-actor.md) - Annotate SampleData with @MainActor
186
187## How to Use
188
189Read individual reference files for detailed explanations and code examples:
190
191- [Section definitions](references/_sections.md) - Category structure and impact levels
192- [Rule template](assets/templates/_template.md) - Template for adding new rules
193
194## Reference Files
195
196| File | Description |
197|------|-------------|
198| [references/_sections.md](references/_sections.md) | Category definitions and ordering |
199| [assets/templates/_template.md](assets/templates/_template.md) | Template for new rules |
200| [metadata.json](metadata.json) | Version and reference information |