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 Best Practices — Modular MVVM-C Data Layer4---5# SwiftData Best Practices — Modular MVVM-C Data Layer67Comprehensive data modeling, persistence, sync architecture, and error handling guide for SwiftData aligned with the clinic modular MVVM-C stack.89## Architecture Alignment1011This skill enforces the same modular architecture mandated by `swift-ui-architect`:1213```14┌───────────────────────────────────────────────────────────────┐15│ Feature modules: View + ViewModel, no SwiftData imports │16├───────────────────────────────────────────────────────────────┤17│ Domain: models + repository/coordinator/error protocols │18├───────────────────────────────────────────────────────────────┤19│ Data: @Model entities, SwiftData stores, repository impls, │20│ remote clients, retry executor, sync queue, conflict handling │21└───────────────────────────────────────────────────────────────┘22```2324**Key principle:** SwiftData types (`@Model`, `ModelContext`, `@Query`, `FetchDescriptor`) live in Data-only implementation code. Feature Views/ViewModels work with Domain types and protocol dependencies.252627## Clinic Architecture Contract (iOS 26 / Swift 6.2)2829All guidance in this skill assumes the clinic modular MVVM-C architecture:3031- Feature modules import `Domain` + `DesignSystem` only (never `Data`, never sibling features)32- App target is the convergence point and owns `DependencyContainer`, concrete coordinators, and Route Shell wiring33- `Domain` stays pure Swift and defines models plus repository, `*Coordinating`, `ErrorRouting`, and `AppError` contracts34- `Data` owns SwiftData/network/sync/retry/background I/O and implements Domain protocols35- Read/write flow defaults to stale-while-revalidate reads and optimistic queued writes36- ViewModels call repository protocols directly (no default use-case/interactor layer)3738## When to Apply3940Reference these guidelines when:41- Defining @Model entity classes and mapping them to domain structs42- Setting up ModelContainer and ModelContext in the Data layer43- Implementing repository protocols backed by SwiftData44- Writing stale-while-revalidate repository reads (`AsyncStream`)45- Implementing optimistic writes plus queued sync operations46- Configuring entity relationships (one-to-many, inverse)47- Fetching from APIs and persisting to SwiftData via sync coordinators48- Handling save failures, corrupt stores, and migration errors49- Routing AppError traits to centralized error UI infrastructure50- Building preview infrastructure with sample data51- Planning schema migrations for app updates5253## Workflow5455Use this workflow when designing or refactoring a SwiftData-backed feature:56571. Domain design: define domain structs (`Trip`, `Friend`) with validation/computed rules (see `model-domain-mapping`, `state-business-logic-placement`)582. Entity design: define `@Model` entity classes with mapping methods (see `model-*`, `model-domain-mapping`)593. Repository protocol: define in Domain layer, implement with SwiftData in Data layer (see `persist-repository-wrapper`)604. Container wiring: configure `ModelContainer` once at the app boundary with error recovery (see `persist-container-setup`, `persist-container-error-recovery`)615. Dependency injection: inject repository protocols via @Environment (see `state-dependency-injection`)626. ViewModel: create @Observable ViewModel that delegates directly to repository protocols (see `state-query-vs-viewmodel`)637. CRUD flows: route all insert/delete/update through ViewModel -> Repository (see `crud-*`)648. Sync architecture: queue writes, execute via sync coordinator with retry policy (see `sync-*`)659. Relationships: model to-many relationships as arrays; define delete rules (see `rel-*`)6610. Previews: create in-memory containers and sample data for fast iteration (see `preview-*`)6711. Schema evolution: plan migrations with versioned schemas (see `schema-*`)6869## Troubleshooting7071- Data not persisting -> `persist-model-macro`, `persist-container-setup`, `persist-autosave`, `schema-configuration`72- List not updating after background import -> `query-background-refresh`, `persist-model-actor`73- List not updating (same-context) -> `query-property-wrapper`, `state-wrapper-views`74- Duplicates from API sync -> `schema-unique-attributes`, `sync-conflict-resolution`75- App crashes on launch after model change -> `schema-migration-recovery`, `persist-container-error-recovery`76- Save failures silently losing data -> `crud-save-error-handling`77- Stale data from network -> `sync-offline-first`, `sync-fetch-persist`78- Widget/extension can't see data -> `persist-app-group`, `schema-configuration`79- Choosing architecture pattern for data views -> `state-query-vs-viewmodel`, `persist-repository-wrapper`8081## Rule Categories by Priority8283| Priority | Category | Impact | Prefix |84|----------|----------|--------|--------|85| 1 | Data Modeling | CRITICAL | `model-` |86| 2 | Persistence Setup | CRITICAL | `persist-` |87| 3 | Querying & Filtering | HIGH | `query-` |88| 4 | CRUD Operations | HIGH | `crud-` |89| 5 | Sync & Networking | HIGH | `sync-` |90| 6 | Relationships | MEDIUM-HIGH | `rel-` |91| 7 | SwiftUI State Flow | MEDIUM-HIGH | `state-` |92| 8 | Schema & Migration | MEDIUM-HIGH | `schema-` |93| 9 | Sample Data & Previews | MEDIUM | `preview-` |9495## Quick Reference9697### 1. Data Modeling (CRITICAL)9899- [`model-domain-mapping`](references/model-domain-mapping.md) - Map @Model entities to domain structs across Domain/Data boundaries100- [`model-custom-types`](references/model-custom-types.md) - Use custom types over parallel arrays101- [`model-class-for-persistence`](references/model-class-for-persistence.md) - Use classes for SwiftData entity types102- [`model-identifiable`](references/model-identifiable.md) - Conform entities to Identifiable with UUID103- [`model-initializer`](references/model-initializer.md) - Provide custom initializers for entity classes104- [`model-computed-properties`](references/model-computed-properties.md) - Use computed properties for derived data105- [`model-defaults`](references/model-defaults.md) - Provide sensible default values for entity properties106- [`model-transient`](references/model-transient.md) - Mark non-persistent properties with @Transient107- [`model-external-storage`](references/model-external-storage.md) - Use external storage for large binary data108109### 2. Persistence Setup (CRITICAL)110111- [`persist-repository-wrapper`](references/persist-repository-wrapper.md) - Wrap SwiftData behind Domain repository protocols112- [`persist-model-macro`](references/persist-model-macro.md) - Apply @Model macro to all persistent types113- [`persist-container-setup`](references/persist-container-setup.md) - Configure ModelContainer at the App level114- [`persist-container-error-recovery`](references/persist-container-error-recovery.md) - Handle ModelContainer creation failure with store recovery115- [`persist-context-environment`](references/persist-context-environment.md) - Access ModelContext via @Environment (Data layer)116- [`persist-autosave`](references/persist-autosave.md) - Enable autosave for manually created contexts117- [`persist-enumerate-batch`](references/persist-enumerate-batch.md) - Use ModelContext.enumerate for large traversals118- [`persist-in-memory-config`](references/persist-in-memory-config.md) - Use in-memory configuration for tests and previews119- [`persist-app-group`](references/persist-app-group.md) - Use App Groups for shared data storage120- [`persist-model-actor`](references/persist-model-actor.md) - Use @ModelActor for background SwiftData work121- [`persist-identifier-transfer`](references/persist-identifier-transfer.md) - Pass PersistentIdentifier across actors122123### 3. Querying & Filtering (HIGH)124125- [`query-property-wrapper`](references/query-property-wrapper.md) - Use @Query for declarative data fetching (Data layer)126- [`query-background-refresh`](references/query-background-refresh.md) - Force view refresh after background context inserts127- [`query-sort-descriptors`](references/query-sort-descriptors.md) - Apply sort descriptors to @Query128- [`query-predicates`](references/query-predicates.md) - Use #Predicate for type-safe filtering129- [`query-dynamic-init`](references/query-dynamic-init.md) - Use custom view initializers for dynamic queries130- [`query-fetch-descriptor`](references/query-fetch-descriptor.md) - Use FetchDescriptor outside SwiftUI views131- [`query-fetch-tuning`](references/query-fetch-tuning.md) - Tune FetchDescriptor paging and pending-change behavior132- [`query-localized-search`](references/query-localized-search.md) - Use localizedStandardContains for search133- [`query-expression`](references/query-expression.md) - Use #Expression for reusable predicate components (iOS 18+)134135### 4. CRUD Operations (HIGH)136137- [`crud-insert-context`](references/crud-insert-context.md) - Insert models via repository implementations138- [`crud-delete-indexset`](references/crud-delete-indexset.md) - Delete via repository with IndexSet from onDelete139- [`crud-sheet-creation`](references/crud-sheet-creation.md) - Use sheets for focused data creation via ViewModel140- [`crud-cancel-delete`](references/crud-cancel-delete.md) - Avoid orphaned records by persisting only on save141- [`crud-undo-cancel`](references/crud-undo-cancel.md) - Enable undo and use it to cancel edits142- [`crud-edit-button`](references/crud-edit-button.md) - Provide EditButton for list management143- [`crud-dismiss-save`](references/crud-dismiss-save.md) - Dismiss modal after ViewModel save completes144- [`crud-save-error-handling`](references/crud-save-error-handling.md) - Handle repository save failures with user feedback145146### 5. Sync & Networking (HIGH)147148- [`sync-fetch-persist`](references/sync-fetch-persist.md) - Use injected sync services to fetch and persist API data149- [`sync-offline-first`](references/sync-offline-first.md) - Design offline-first architecture with repository reads and background sync150- [`sync-conflict-resolution`](references/sync-conflict-resolution.md) - Implement conflict resolution for bidirectional sync151152### 6. Relationships (MEDIUM-HIGH)153154- [`rel-optional-single`](references/rel-optional-single.md) - Use optionals for optional relationships155- [`rel-array-many`](references/rel-array-many.md) - Use arrays for one-to-many relationships156- [`rel-inverse-auto`](references/rel-inverse-auto.md) - Rely on SwiftData automatic inverse maintenance157- [`rel-delete-rules`](references/rel-delete-rules.md) - Configure cascade delete rules for owned relationships158- [`rel-explicit-sort`](references/rel-explicit-sort.md) - Sort relationship arrays explicitly159160### 7. SwiftUI State Flow (MEDIUM-HIGH)161162- [`state-query-vs-viewmodel`](references/state-query-vs-viewmodel.md) - Route all data access through @Observable ViewModels163- [`state-business-logic-placement`](references/state-business-logic-placement.md) - Place business logic in domain value types and repository-backed ViewModels164- [`state-dependency-injection`](references/state-dependency-injection.md) - Inject repository protocols via @Environment165- [`state-bindable`](references/state-bindable.md) - Use @Bindable for two-way model binding166- [`state-local-state`](references/state-local-state.md) - Use @State for view-local transient data167- [`state-wrapper-views`](references/state-wrapper-views.md) - Extract wrapper views for dynamic query state168169### 8. Schema & Migration (MEDIUM-HIGH)170171- [`schema-define-all-types`](references/schema-define-all-types.md) - Define schema with all model types172- [`schema-unique-attributes`](references/schema-unique-attributes.md) - Use @Attribute(.unique) for natural keys173- [`schema-unique-macro`](references/schema-unique-macro.md) - Use #Unique for compound uniqueness (iOS 18+)174- [`schema-index`](references/schema-index.md) - Use #Index for hot predicates and sorts (iOS 18+)175- [`schema-migration-plan`](references/schema-migration-plan.md) - Plan migrations before changing models176- [`schema-migration-recovery`](references/schema-migration-recovery.md) - Plan migration recovery for schema changes177- [`schema-configuration`](references/schema-configuration.md) - Customize storage with ModelConfiguration178179### 9. Sample Data & Previews (MEDIUM)180181- [`preview-sample-singleton`](references/preview-sample-singleton.md) - Create a SampleData singleton for previews182- [`preview-in-memory`](references/preview-in-memory.md) - Use in-memory containers for preview isolation183- [`preview-static-data`](references/preview-static-data.md) - Define static sample data on model types184- [`preview-main-actor`](references/preview-main-actor.md) - Annotate SampleData with @MainActor185186## How to Use187188Read individual reference files for detailed explanations and code examples:189190- [Section definitions](references/_sections.md) - Category structure and impact levels191- [Rule template](assets/templates/_template.md) - Template for adding new rules192193## Reference Files194195| File | Description |196|------|-------------|197| [references/_sections.md](references/_sections.md) | Category definitions and ordering |198| [assets/templates/_template.md](assets/templates/_template.md) | Template for new rules |199| [metadata.json](metadata.json) | Version and reference information |