swiftdata-coredata-persistence
Compare original and translation side by side
🇺🇸
Original
English🇨🇳
Translation
ChineseSwiftData & CoreData Persistence
SwiftData & CoreData 持久化方案
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.
SwiftData与CoreData不能互换。两者各有特定优势、迁移陷阱和性能模式。错误的框架选择或迁移策略会导致生产环境崩溃。
核心原则:根据应用约束选择框架,从项目初期就对模型进行版本控制,绝不在数据库中存储BLOB数据。
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"];
}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
选择SwiftData的场景
- 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
- 全新iOS应用,部署目标为iOS 17及以上
- 数据模型简单(多为1:1或1:N关系,复杂查询较少)
- SwiftUI优先的架构
- 需要iCloud同步且希望配置尽可能简洁
Choose CoreData When
选择CoreData的场景
- 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
- 需要支持iOS 16或更早版本
- 涉及复杂查询,需使用NSCompoundPredicate、子查询
- 以UIKit为主的应用,需使用NSFetchedResultsController
- 已有CoreData代码库(迁移复杂度高,建议继续使用CoreData)
- 对性能要求极高(截至2025年,CoreData仍更快)
- 需要抽象实体或继承层级
Migration Death Traps
迁移致命陷阱
Trap 1: Unversioned → Versioned with iCloud
陷阱1:未版本化 → 带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.
严重警告:如果你的应用已发布未版本化的SwiftData并启用iCloud同步,用户在首次迁移时会遭遇崩溃。
错误信息:"Cannot use staged migration with an unknown model version"
原因:CloudKit需要版本化的 schema 来保证同步一致性。
解决方案:
swift
// ❌ 错误做法:发布未版本化+iCloud同步后再添加版本控制
// 这会导致所有现有用户崩溃
// ✅ 正确做法:分两阶段迁移
// 版本1.1:添加版本控制但不修改schema
enum SchemaV1: VersionedSchema {
static var models: [any PersistentModel.Type] { [User.self] }
@Model
final class User {
var name: String
var email: String
// 与已发布的v1.0完全一致的schema
}
}
// 版本1.2:此时安全添加新属性
enum SchemaV2: VersionedSchema {
static var models: [any PersistentModel.Type] { [User.self] }
@Model
final class User {
var name: String
var email: String
var createdAt: Date? // 新属性
}
}规则:从项目初期就使用版本化schema,或先发布版本化包装器。
Trap 2: Unique Constraint with Duplicates
陷阱2:存在重复数据时添加唯一约束
Scenario: Adding when data has duplicates.
@Attribute(.unique)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<OldSchema.Product>())
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.
场景:当数据中存在重复项时,添加。
@Attribute(.unique)结果:轻量迁移失败,应用无法正常使用。
解决方案:在应用约束前,先执行去重的复杂迁移。
swift
struct DeduplicateThenUnique: MigrationStage {
func migrate(context: ModelContext) throws {
let items = try context.fetch(FetchDescriptor<OldSchema.Product>())
let grouped = Dictionary(grouping: items, by: { $0.sku })
// 保留第一个,删除重复项
for (_, duplicates) in grouped where duplicates.count > 1 {
for item in duplicates.dropFirst() {
context.delete(item)
}
}
try context.save()
}
}规则:在应用约束前清理数据。务必使用生产数据备份进行测试。
Trap 3: iCloud Sync Blocks Migrations
陷阱3:iCloud同步阻止迁移
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.
严重警告:一旦启用iCloud同步,SwiftData会阻止破坏轻量兼容性的迁移。
具体影响:
- 无法重命名属性(会破坏同步)
- 无法修改关系删除规则(会破坏同步)
- 无法移除必填属性(会破坏同步)
替代方案:
swift
// 不要直接重命名(会被阻止):
// @Model class User {
// var userName: String // 从'name'重命名而来
// }
// 添加新属性,弃用旧属性(允许操作):
@Model class User {
var name: String // 保留以兼容旧版本
var userName: String? // 新的首选字段
var displayName: String {
userName ?? name // 从两个字段计算得出
}
}规则:启用iCloud后,仅允许添加式修改。需提前精心设计schema。
Trap 4: Main Thread Migration Blocking
陷阱4:主线程迁移导致阻塞
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).
症状:应用更新后启动时出现3-5秒的白屏。
原因:大型数据集的迁移在主线程运行。
解决方案:后台初始化。
swift
// ✅ 在后台线程初始化容器
Task.detached {
let container = try await ModelContainer(
for: User.self,
migrationPlan: MigrationPlan.self
)
await MainActor.run {
self.container = container
}
}规则:务必使用真实数据规模(1000+条记录)测试迁移。
Performance Patterns
性能优化方案
Pattern 1: Batch Fetching
方案1:批量查询
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<Post> = Post.fetchRequest()
let posts = try context.fetch(request)
// ✅ CORRECT: Loads 20 at a time as needed
let request: NSFetchRequest<Post> = Post.fetchRequest()
request.fetchBatchSize = 20
let posts = try context.fetch(request)Rule: Always set fetchBatchSize for feeds, lists, paginated views.
问题:一次性加载1000+个对象会占用大量内存,阻塞UI。
解决方案:使用fetchBatchSize实现懒加载。
swift
// ❌ 错误做法:一次性加载全部1000篇帖子到内存
let request: NSFetchRequest<Post> = Post.fetchRequest()
let posts = try context.fetch(request)
// ✅ 正确做法:按需每次加载20条
let request: NSFetchRequest<Post> = Post.fetchRequest()
request.fetchBatchSize = 20
let posts = try context.fetch(request)规则:对于信息流、列表、分页视图,务必设置fetchBatchSize。
Pattern 2: BLOB Storage
方案2:BLOB存储
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.
问题:在CoreData/SwiftData中存储图片/视频会严重影响性能。
规则:
- 小于100KB:可内联存储(安全)
- 100KB - 1MB:使用独立实体加关联关系
- 大于1MB:存储到文件系统,仅在数据库中保存路径
swift
// ❌ 错误做法:在Post实体中存储2MB图片
@Model
class Post {
var title: String
@Attribute(.externalStorage) var image: Data // 仍会在查询时加载到内存!
}
// ✅ 正确做法:文件系统存储
@Model
class Post {
var title: String
var imagePath: String? // "images/post-123.jpg"
}
// 按需加载图片
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)
}原因:数据库查询会加载完整对象。即使不显示,BLOB也会占用大量内存。
Pattern 3: Faulting for Relationships
方案3:关系延迟加载
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<Post> = Post.fetchRequest()
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.
问题:查询帖子时会自动加载所有关联的用户和图片。
解决方案:返回故障对象,访问时再加载。
swift
// ✅ 关系默认是故障对象(合理设置)
let request: NSFetchRequest<Post> = Post.fetchRequest()
request.relationshipKeyPathsForPrefetching = [] // 不要预加载
let posts = try context.fetch(request)
// 访问时才会触发单独加载:
let authorName = posts[0].author.name // 此时才会加载User
// ✅ 确定需要访问所有对象的关系时再预加载:
request.relationshipKeyPathsForPrefetching = ["author"]规则:仅预加载你会访问到的所有查询对象的关系。
Pattern 4: NSFetchedResultsController (CoreData)
方案4:NSFetchedResultsController(CoreData)
For: Table/collection views with live updates.
Why: Lazy loads, automatic UI updates, sectioning built-in.
swift
let request: NSFetchRequest<Post> = Post.fetchRequest()
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.fetchedObjectsRule: Use FRC for all CoreData-backed table/collection views.
适用场景:需要实时更新的表格/集合视图。
优势:懒加载、自动更新UI、内置分组功能。
swift
let request: NSFetchRequest<Post> = Post.fetchRequest()
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 使用 frc.fetchedObjects规则:所有基于CoreData的表格/集合视图都应使用FRC。
SwiftData Limitations (as of iOS 18)
SwiftData的局限性(截至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 |
| 功能 | CoreData | SwiftData | 替代方案 |
|---|---|---|---|
| NSCompoundPredicate | ✅ | ❌ | 多轮查询 |
| NSFetchedResultsController | ✅ | ❌ | SwiftUI中使用@Query |
| 抽象实体 | ✅ | ❌ | 使用协议 |
| 子上下文 | ✅ | ❌ | 使用ModelContext |
| 模型子类化 | ✅ | ❌ | 不要子类化 |
| 性能 | 更快 | 更慢 | 等待官方更新 |
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 |
| 错误认知 | 实际情况 | 修复方案 |
|---|---|---|
| "迁移是自动的" | 只有轻量迁移是自动的。唯一约束、重命名等需要复杂迁移。 | 使用生产数据测试 |
| "以后再做版本化" | 未版本化+iCloud同步=首次迁移必崩溃 | 从项目初期就做版本化 |
| "SwiftData是CoreData的简化版" | SwiftData有不同的规则(不能子类化、删除规则不同等) | 学习SwiftData的特定约束 |
| "externalStorage属性能解决BLOB问题" | 查询时仍会加载到内存 | 存储到文件系统 |
| "我可以随意重命名属性" | 启用iCloud后,重命名会被阻止 | 添加新属性,保留旧属性 |
| "迁移不会阻塞UI" | 大型数据集会阻塞主线程 | 后台初始化迁移 |
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
发布schema变更前:
- 已定义版本化schema(V1、V2)
- 如需复杂操作(去重、重命名),已编写迁移阶段代码
- 使用生产数据备份测试(1000+条记录)
- 测试了后台线程迁移(无白屏)
- 验证迁移后iCloud同步正常
- 确认新属性有默认值或为可选类型
- 确保仅在数据干净时应用唯一约束
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<User> = User.fetchRequest()
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()
}SwiftData:
swift
// 基础模型
@Model
class User {
@Attribute(.unique) var email: String
var name: String
@Relationship(deleteRule: .cascade) var posts: [Post]
}
// 版本化schema
enum SchemaV1: VersionedSchema {
static var models: [any PersistentModel.Type] { [User.self] }
@Model final class User { /* ... */ }
}
// 查询(SwiftUI)
@Query(sort: \.createdAt, order: .reverse) var users: [User]CoreData:
swift
// 带批量加载的查询
let request: NSFetchRequest<User> = User.fetchRequest()
request.fetchBatchSize = 20
request.predicate = NSPredicate(format: "name CONTAINS[cd] %@", searchText)
// 后台上下文
let bgContext = container.newBackgroundContext()
bgContext.perform {
// 在此处理繁重任务
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
- 已发布未版本化schema且启用iCloud → 先发布版本化包装器
- 向脏数据添加唯一约束 → 先执行去重迁移
- 在数据库中存储多MB级文件 → 转移到文件系统
- 更新后启动时出现白屏 → 改为后台迁移
- 查询耗时>500ms → 添加fetchBatchSize,检查是否有BLOB
- "等用户反馈再处理迁移问题" → 立即使用生产数据测试
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.
优化前:应用在首次迁移(未版本化→版本化陷阱)时,所有启用iCloud的用户100%崩溃。
优化后:先发布v1.1版本(仅添加版本化包装器,无schema变更),再发布v1.2版本(实际变更)。零崩溃。
优化前:信息流加载1000篇帖子(每篇含2MB图片)耗时4秒。
优化后:图片转移到文件系统,fetchBatchSize设为20。信息流加载耗时300ms,图片按需懒加载。