swift-persistence
npx machina-cli add skill jeremieb/swift-unit-test-instructions/swift-persistence --openclawSwift Persistence
Instructions
Step 1: Choose the framework
Ask if not already specified:
SwiftData or CoreData?
| SwiftData | CoreData | |
|---|---|---|
| Min iOS | iOS 17+ | iOS 8+ |
| Syntax | @Model macro, Swift-native | NSManagedObject, Objective-C roots |
| Recommended for | New projects on iOS 17+ | Legacy projects, complex migration needs |
Then load the matching reference:
- SwiftData →
references/swiftdata.md - CoreData →
references/coredata.md
Step 2: Define the models
Ask: what entities are needed? For each entity:
- Name (singular, UpperCamelCase:
Task,UserProfile,Transaction) - Properties and their types
- Relationships to other entities (one-to-one, one-to-many, many-to-many)
- Any computed properties needed
Step 3: Generate the model layer
Models go in Core/Models/ — they are plain data types, framework-agnostic where possible.
Persistence infrastructure goes in Services/Storage/:
PersistenceController.swift— container setup, in-memory preview/test variant[Entity]Repository.swift— CRUD operations for each entity[Entity]RepositoryProtocol.swift— protocol for testability
Rule: ViewModels inject a repository protocol. They never touch ModelContext or NSManagedObjectContext directly.
Services/Storage/
├── PersistenceController.swift
├── TaskRepository.swift
└── TaskRepositoryProtocol.swift
Step 4: Generate the repository protocol
Always define a protocol first so ViewModels can be tested with a mock:
// Services/Storage/TaskRepositoryProtocol.swift
protocol TaskRepositoryProtocol {
func fetchAll() throws -> [Task]
func save(_ task: Task) throws
func delete(_ task: Task) throws
}
Step 5: Wire into the ViewModel
ViewModels receive the repository via init:
@MainActor
@Observable
final class TaskListViewModel {
private(set) var tasks: [Task] = []
private let repository: TaskRepositoryProtocol
init(repository: TaskRepositoryProtocol = TaskRepository()) {
self.repository = repository
}
func loadTasks() {
tasks = (try? repository.fetchAll()) ?? []
}
}
Step 6: Generate mock for tests
final class MockTaskRepository: TaskRepositoryProtocol {
var stubbedTasks: [Task] = []
var saveCallCount = 0
var deleteCallCount = 0
func fetchAll() throws -> [Task] { stubbedTasks }
func save(_ task: Task) throws { saveCallCount += 1 }
func delete(_ task: Task) throws { deleteCallCount += 1 }
}
Step 7: Quality checklist
-
ModelContext/NSManagedObjectContextis never used in aViewModelorView - Repository protocol exists for every entity accessed by a ViewModel
- In-memory container/store exists for tests and previews
- Background context used for heavy writes (batch imports, migrations)
- Models in
Core/Models/have no direct framework import (SwiftDataorCoreData) in the ViewModel layer - Migrations are handled explicitly — no silent schema changes
Examples
Example 1: SwiftData task app
User says: "Add SwiftData to persist tasks with title, completion status, and due date"
Actions:
- Load
references/swiftdata.md - Generate
Taskmodel with@Model - Generate
PersistenceControllerwith.inMemorypreview variant - Generate
TaskRepository+TaskRepositoryProtocol - Update
TaskListViewModelto inject repository - Generate
MockTaskRepositoryfor tests
Example 2: CoreData in existing UIKit app
User says: "I need to add CoreData to my existing UIKit project to cache user profiles"
Actions:
- Load
references/coredata.md - Generate
UserProfileNSManagedObject subclass - Generate
PersistenceControllerwith in-memory option - Generate
UserProfileRepository+ protocol - Wire into existing
ProfileViewModel
Troubleshooting
SwiftData: "Context is not available" — ModelContext is being accessed outside a SwiftData-aware environment. Ensure the ModelContainer is set up at the App level and injected via .modelContainer() modifier. Never create contexts manually in ViewModels.
CoreData: merge conflicts — You are writing on the main context from a background thread. Use performBackgroundTask or a dedicated background context. Always call context.save() on the correct thread.
Tests failing because model can't be found — The in-memory store is not configured. Use PersistenceController.preview (SwiftData) or an in-memory NSPersistentContainer in your test setup.
Source
git clone https://github.com/jeremieb/swift-unit-test-instructions/blob/main/skills/swift-persistence/SKILL.mdView on GitHub Overview
Build a framework-agnostic MVVM persistence layer using SwiftData or CoreData. It defines plain models, repository protocols, and in-memory test variants to keep logic testable. This approach separates concerns and supports both modern SwiftData and legacy CoreData workflows.
How This Skill Works
Models live in Core/Models and persistence logic resides in Services/Storage with a repository-based interface. ViewModels depend on a repository protocol, enabling mock implementations for tests and a concrete repository for production. Framework selection (SwiftData or CoreData) is determined upfront and the corresponding references are loaded to drive code generation.
When to Use It
- Add CoreData to an app or migrate an existing data model
- Set up SwiftData models using @Model in MVVM
- Persist data via a repository abstraction rather than directly in the context
- Create in-memory/test variant for previews and tests
- Define and wire a repository protocol to ViewModels for testability
Quick Start
- Step 1: Pick SwiftData or CoreData and load the matching reference
- Step 2: Define entities in Core/Models and create a per-entity repository (e.g., TaskRepository.swift and TaskRepositoryProtocol.swift)
- Step 3: Wire the repository into a ViewModel, add a Mock repository for tests, and verify with in-memory storage
Best Practices
- ModelContext / NSManagedObjectContext is never used directly in a ViewModel
- Create a repository protocol for every entity accessed by a ViewModel
- Provide an in-memory container or store for tests and previews
- Use a background context for heavy writes (batch imports, migrations)
- Migrations are explicit — avoid silent schema changes
Example Use Cases
- SwiftData task app: define Task with @Model, in-memory PersistenceController, TaskRepository, and MockTaskRepository for tests
- CoreData in UIKit app: add Task as NSManagedObject-derived entity, wire TaskRepository, and use mocks for unit tests
- MVVM data-sync app: replace direct managed object contexts with a repository-backed ViewModel
- Preview-driven UI: rely on in-memory storage to power SwiftUI previews without a real database
- Unit testing persistence: inject a MockTaskRepository implementing TaskRepositoryProtocol for deterministic tests