swiftdata-coredata-persistence
π―Skillfrom dagba/ios-mcp
swiftdata-coredata-persistence skill from dagba/ios-mcp
Part of
dagba/ios-mcp(7 items)
Installation
git clone https://github.com/yourusername/ios-dev-mcp-server.gitnpm run buildpython3 -m venv ~/.idb-venvpip install fb-idb{
"mcpServers": {
"ios-dev": {
"command": "node",
"args": ["/a...Skill Details
Use when implementing data persistence in iOS apps with SwiftData or CoreData, encountering migration errors, performance issues with fetches, or choosing between persistence frameworks
Overview
# SwiftData & CoreData Persistence
Overview
SwiftData and CoreData are NOT interchangeable. Each has specific strengths, migration traps, and performance patterns. Wrong framework choice or migration strategy causes production crashes.
Core principle: Choose framework based on app constraints, version models from day one, never store BLOBs in the database.
Framework Selection
```dot
digraph framework_choice {
"New iOS app?" [shape=diamond];
"iOS 17+ only?" [shape=diamond];
"Simple data model?" [shape=diamond];
"iCloud sync?" [shape=diamond];
"Existing CoreData?" [shape=diamond];
"Use SwiftData" [shape=box, style=filled, fillcolor=lightgreen];
"Use CoreData" [shape=box, style=filled, fillcolor=lightblue];
"Stay with CoreData" [shape=box, style=filled, fillcolor=lightblue];
"New iOS app?" -> "iOS 17+ only?" [label="yes"];
"New iOS app?" -> "Existing CoreData?" [label="no"];
"Existing CoreData?" -> "Stay with CoreData" [label="yes"];
"Existing CoreData?" -> "iOS 17+ only?" [label="no"];
"iOS 17+ only?" -> "Simple data model?" [label="yes"];
"iOS 17+ only?" -> "Use CoreData" [label="no"];
"Simple data model?" -> "iCloud sync?" [label="yes"];
"Simple data model?" -> "Use CoreData" [label="no"];
"iCloud sync?" -> "Use SwiftData" [label="yes"];
"iCloud sync?" -> "Use SwiftData" [label="no"];
}
```
Choose SwiftData When
- New app, iOS 17+ deployment target
- Simple relationships (mostly 1:1 or 1:N, few complex queries)
- SwiftUI-first architecture
- iCloud sync needed with minimal configuration
Choose CoreData When
- iOS 16 or earlier support required
- Complex queries with NSCompoundPredicate, subqueries
- UIKit-heavy app with NSFetchedResultsController
- Existing CoreData codebase (migration complex, stay with CoreData)
- Performance critical (as of 2025, CoreData still faster)
- Abstract entities or inheritance hierarchies
Migration Death Traps
Trap 1: Unversioned β Versioned with iCloud
CRITICAL: If you shipped unversioned SwiftData with iCloud sync, users will crash on first migration.
Error: "Cannot use staged migration with an unknown model version"
Why: CloudKit requires versioned schemas for sync consistency.
Solution:
```swift
// β WRONG: Adding versioning after shipping unversioned + iCloud
// This will crash for all existing users
// β CORRECT: Two-phase migration
// Version 1.1: Add versioning WITHOUT schema changes
enum SchemaV1: VersionedSchema {
static var models: [any PersistentModel.Type] { [User.self] }
@Model
final class User {
var name: String
var email: String
// EXACT same schema as shipped v1.0
}
}
// Version 1.2: Now safe to add new properties
enum SchemaV2: VersionedSchema {
static var models: [any PersistentModel.Type] { [User.self] }
@Model
final class User {
var name: String
var email: String
var createdAt: Date? // New property
}
}
```
Rule: Start with versioned schemas from day one, or ship versioning wrapper first.
Trap 2: Unique Constraint with Duplicates
Scenario: Adding @Attribute(.unique) when data has duplicates.
Result: Lightweight migration fails, app becomes non-functional.
Solution: Complex migration with deduplication BEFORE applying constraint.
```swift
struct DeduplicateThenUnique: MigrationStage {
func migrate(context: ModelContext) throws {
let items = try context.fetch(FetchDescriptor
let grouped = Dictionary(grouping: items, by: { $0.sku })
// Keep first, delete duplicates
for (_, duplicates) in grouped where duplicates.count > 1 {
for item in duplicates.dropFirst() {
context.delete(item)
}
}
try context.save()
}
}
```
Rule: Clean data BEFORE applying constraints. Test with production data backup.
Trap 3: iCloud Sync Blocks Migrations
CRITICAL: Once iCloud sync enabled, SwiftData blocks migrations that break lightweight compatibility.
What this means:
- Cannot rename properties (breaks sync)
- Cannot change relationship delete rules (breaks sync)
- Cannot remove required properties (breaks sync)
Workaround:
```swift
// Instead of renaming (blocked):
// @Model class User {
// var userName: String // Renamed from 'name'
// }
// Add new, deprecate old (allowed):
@Model class User {
var name: String // Keep for backward compatibility
var userName: String? // New preferred field
var displayName: String {
userName ?? name // Compute from both
}
}
```
Rule: With iCloud, only additive changes allowed. Design schema carefully upfront.
Trap 4: Main Thread Migration Blocking
Symptom: White screen for 3-5 seconds on app launch after update.
Cause: Large dataset migration runs on main thread.
Solution: Background initialization.
```swift
// β Initialize container on background thread
Task.detached {
let container = try await ModelContainer(
for: User.self,
migrationPlan: MigrationPlan.self
)
await MainActor.run {
self.container = container
}
}
```
Rule: Always test migrations with realistic data size (1000+ records).
Performance Patterns
Pattern 1: Batch Fetching
Problem: Loading 1000+ objects at once consumes memory, blocks UI.
Solution: fetchBatchSize for lazy loading.
```swift
// β WRONG: Loads all 1000 posts into memory
let request: NSFetchRequest
let posts = try context.fetch(request)
// β CORRECT: Loads 20 at a time as needed
let request: NSFetchRequest
request.fetchBatchSize = 20
let posts = try context.fetch(request)
```
Rule: Always set fetchBatchSize for feeds, lists, paginated views.
Pattern 2: BLOB Storage
Problem: Storing images/videos in CoreData/SwiftData kills performance.
Rule:
- < 100KB: Store inline (safe)
- 100KB - 1MB: Separate entity with relationship
- > 1MB: File system + store path in database
```swift
// β WRONG: 2MB images in Post entity
@Model
class Post {
var title: String
@Attribute(.externalStorage) var image: Data // Still loads eagerly!
}
// β CORRECT: File system storage
@Model
class Post {
var title: String
var imagePath: String? // "images/post-123.jpg"
}
// Load image on-demand
func loadImage(for post: Post) -> UIImage? {
guard let path = post.imagePath else { return nil }
let url = FileManager.documentsDirectory.appendingPathComponent(path)
return UIImage(contentsOfFile: url.path)
}
```
Why: Database fetches load full objects. BLOBs inflate memory usage even when not displayed.
Pattern 3: Faulting for Relationships
Problem: Fetching posts eagerly loads all related users and images.
Solution: Return faults, load on access.
```swift
// β Relationships default to faults (good)
let request: NSFetchRequest
request.relationshipKeyPathsForPrefetching = [] // Don't prefetch
let posts = try context.fetch(request)
// Access triggers individual fault:
let authorName = posts[0].author.name // Only now loads User
// β Prefetch when you know you'll need it:
request.relationshipKeyPathsForPrefetching = ["author"]
```
Rule: Only prefetch relationships you'll access for ALL fetched objects.
Pattern 4: NSFetchedResultsController (CoreData)
For: Table/collection views with live updates.
Why: Lazy loads, automatic UI updates, sectioning built-in.
```swift
let request: NSFetchRequest
request.sortDescriptors = [NSSortDescriptor(key: "createdAt", ascending: false)]
request.fetchBatchSize = 20
let frc = NSFetchedResultsController(
fetchRequest: request,
managedObjectContext: context,
sectionNameKeyPath: nil,
cacheName: "PostsCache"
)
frc.delegate = self
try frc.performFetch()
// UITableView uses frc.fetchedObjects
```
Rule: Use FRC for all CoreData-backed table/collection views.
SwiftData Limitations (as of iOS 18)
| Feature | CoreData | SwiftData | Workaround |
|---------|----------|-----------|------------|
| NSCompoundPredicate | β | β | Multiple queries |
| NSFetchedResultsController | β | β | @Query in SwiftUI |
| Abstract entities | β | β | Use protocols |
| Child contexts | β | β | Use ModelContext |
| Subclassing models | β | β | Don't subclass |
| Performance | Faster | Slower | Wait for updates |
Common Mistakes
| Mistake | Reality | Fix |
|---------|---------|-----|
| "Migrations are automatic" | Only lightweight migrations. Unique constraints, renames require complex migrations. | Test with production data |
| "I'll version later" | Unversioned + iCloud = crash on first migration | Version from day one |
| "SwiftData = CoreData simplified" | SwiftData has different rules (no subclassing, different delete rules) | Learn SwiftData-specific constraints |
| "externalStorage attribute fixes BLOBs" | Still loaded into memory on fetch | Store in file system |
| "I can rename properties freely" | With iCloud, renames blocked | Add new property, keep old |
| "Migration won't block UI" | Large datasets block main thread | Background initialization |
Migration Checklist
Before shipping schema change:
- [ ] Defined versioned schemas (V1, V2)
- [ ] Written migration stage if complex (deduplication, renames)
- [ ] Tested with production data backup (1000+ records)
- [ ] Tested migration on background thread (no white screen)
- [ ] Verified iCloud sync works post-migration
- [ ] Checked new properties have defaults or are optional
- [ ] Confirmed unique constraints applied to clean data only
Quick Reference
SwiftData:
```swift
// Basic model
@Model
class User {
@Attribute(.unique) var email: String
var name: String
@Relationship(deleteRule: .cascade) var posts: [Post]
}
// Versioned schemas
enum SchemaV1: VersionedSchema {
static var models: [any PersistentModel.Type] { [User.self] }
@Model final class User { / ... / }
}
// Query (SwiftUI)
@Query(sort: \.createdAt, order: .reverse) var users: [User]
```
CoreData:
```swift
// Fetch with batching
let request: NSFetchRequest
request.fetchBatchSize = 20
request.predicate = NSPredicate(format: "name CONTAINS[cd] %@", searchText)
// Background context
let bgContext = container.newBackgroundContext()
bgContext.perform {
// Heavy work here
try? bgContext.save()
}
```
Red Flags - STOP and Reconsider
- Shipped unversioned schema with iCloud β Ship versioning wrapper first
- Adding unique constraint to dirty data β Run deduplication migration
- Storing multi-MB files in database β Move to file system
- White screen on launch after update β Background migration
- Fetch taking >500ms β Add fetchBatchSize, check for BLOBs
- "I'll handle migration when users complain" β Test NOW with production data
Real-World Impact
Before: App crashes for 100% of users with iCloud enabled after first migration (unversionedβversioned trap).
After: Ship v1.1 with versioning wrapper (no schema change), then v1.2 with actual changes. Zero crashes.
---
Before: Feed loads in 4 seconds with 1000 posts + 2MB images each.
After: Images moved to file system, fetchBatchSize = 20. Feed loads in 300ms, lazy-loads images.
More from this repository6
programmatic-uikit-layout skill from dagba/ios-mcp
swift-codable-json skill from dagba/ios-mcp
viper-architecture-rambler skill from dagba/ios-mcp
realm-persistence skill from dagba/ios-mcp
Refactors and implements iOS applications using the VIP (View-Interactor-Presenter) clean architecture pattern, ensuring modular, testable, and maintainable code structure.
Streamlines iOS app development workflows by automating code generation, dependency management, and build configurations for Swift and Xcode projects.