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
- 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
- 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
- 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+)
- 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
- 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
- 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
- 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
- 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
- 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