SwiftUI 数据持久化:Core Data 和 SwiftData 我是怎么选的
SwiftUI 数据持久化:Core Data 和 SwiftData 我是怎么选的
最近在选新项目的数据持久化方案,评估了一圈:Core Data、SwiftData、Realm、SQLite.swift,最后根据团队情况和项目特点选了 SwiftData。
说说评估逻辑和实际落地的坑。
选型背景
项目情况:
- iOS 17+ 的新项目
- 团队 3 人,之前没人在生产环境用过 Core Data
- 数据模型中等复杂度(10+ 实体,有关联关系)
- 需要支持离线优先
Core Data vs SwiftData vs 其他
先说结论:
| 方案 | 优点 | 缺点 |
|---|---|---|
| Core Data | 成熟、生态完整、Apple 官方 | 笨重、SwiftUI 集成需要适配 |
| SwiftData | SwiftUI 原生集成、代码量少 | iOS 17+ 限定、能力较新 |
| Realm | 跨平台、性能好 | 闭源、学习曲线 |
| SQLite.swift | 轻量、简单直接 | 需要手写迁移逻辑 |
如果你_TARGET_是 iOS 16 及以下,Core Data 是唯一官方方案。如果你只需要存几个简单模型,用户偏好设置之类的,UserDefaults 加 JSON 文件就够了。
SwiftData 入门
SwiftData 的核心是 @Model macro:
import SwiftData
@Model
final class Article {
@Attribute(.unique) var id: String
var title: String
var content: String
var publishedAt: Date
var author: Author?
init(id: String, title: String, content: String, publishedAt: Date = .now) {
self.id = id
self.title = title
self.content = content
self.publishedAt = publishedAt
}
}
@Model
final class Author {
@Attribute(.unique) var id: String
var name: String
@Relationship(deleteRule: .cascade, inverse: \Article.author)
var articles: [Article] = []
init(id: String, name: String) {
self.id = id
self.name = name
}
}
然后在 App 里启用:
@main
struct MyApp: App {
var body: some Scene {
WindowGroup {
ContentView()
}
.modelContainer(for: [Article.self, Author.self])
}
}
在 View 里访问:
struct ArticleListView: View {
@Query private var articles: [Article]
var body: some View {
List(articles) { article in
VStack(alignment: .leading) {
Text(article.title)
.font(.headline)
Text(article.publishedAt, style: .date)
.font(.caption)
}
}
}
}
@Query 是 SwiftData 的核心:它自动监听数据变化并更新 UI,不需要手动 fetch。
SwiftData 的过滤和排序
@Query 支持丰富的过滤选项:
struct FilteredArticleList: View {
@Query private var articles: [Article]
init(filter: String) {
if filter.isEmpty {
_articles = Query()
} else {
_articles = Query(filter: #Predicate<Article> { article in
article.title.contains(filter)
})
}
}
var body: some View {
List(articles) { article in
Text(article.title)
}
}
}
struct SortedArticleList: View {
@Query(sort: \Article.publishedAt, order: .reverse)
private var articles: [Article]
var body: some View {
List(articles) { article in
Text(article.title)
}
}
}
SwiftData + SwiftUI 的 CRUD 操作
创建:
struct AddArticleView: View {
@Environment(\.modelContext) private var modelContext
@State private var title = ""
@State private var content = ""
var body: some View {
Form {
TextField("标题", text: $title)
TextEditor(text: $content)
Button("保存") {
let article = Article(
id: UUID().uuidString,
title: title,
content: content
)
modelContext.insert(article)
}
}
}
}
更新:
struct EditArticleView: View {
@Environment(\.modelContext) private var modelContext
@Bindable var article: Article
var body: some View {
Form {
TextField("标题", text: $article.title)
TextEditor(text: $article.content)
}
// 修改自动保存(SwiftData 默认 AutoSave)
}
}
删除:
struct ArticleRow: View {
@Environment(\.modelContext) private var modelContext
let article: Article
var body: some View {
VStack(alignment: .leading) {
Text(article.title)
Text(article.content)
}
.swipeActions {
Button(role: .destructive) {
modelContext.delete(article)
} label: {
Label("删除", systemImage: "trash")
}
}
}
}
Core Data 的适用场景
如果你的项目需要支持 iOS 16,SwiftData 就没法用了(或者用 swiftDataUnavailable 前缀跑在老系统上)。这时候还是得用 Core Data。
Core Data 在 SwiftUI 里的标准用法:
struct PersistenceController {
static let shared = PersistenceController()
let container: NSPersistentContainer
init() {
container = NSPersistentContainer(name: "DataModel")
container.loadPersistentStores { _, error in
if let error = error {
fatalError("Failed to load: \(error)")
}
}
container.viewContext.automaticallyMergesChangesFromParent = true
container.viewContext.mergePolicy = NSMergeByPropertyObjectTrumpMergePolicy
}
}
struct ContentView: View {
let persistence = PersistenceController.shared
var body: some View {
Text("Hello")
.persistentStoreRemoteChange { notification in
// 处理远程变化
}
}
}
Core Data 的问题是 fetch 需要手动触发:
struct ArticleListView: View {
@FetchRequest(
sortDescriptors: [NSSortDescriptor(keyPath: \Article.publishedAt, ascending: false)],
animation: .default
)
private var articles: FetchedResults<Article>
var body: some View {
List(articles) { article in
Text(article.title)
}
}
}
对比 SwiftData 的 @Query,Core Data 的 @FetchRequest 已经封装得很好了,但还是要写 sortDescriptors、predicate 等参数,代码量多一些。
数据迁移:两者都要面对的问题
无论选哪个,Schema 变更时的迁移都是头疼问题。
SwiftData 用 version marker:
enum Schema {
static let models: [any PersistentModel.Type] = [
Article.self,
Author.self
]
}
@main
struct MyApp: App {
var body: some Scene {
WindowGroup {
ContentView()
}
.modelContainer(for: Schema.models)
}
}
当 model 定义变化时(比如加了新属性),只要 version marker 没变,SwiftData 会自动做轻量迁移。重量迁移需要自己写 migrate closure。
Core Data 的迁移更复杂:
let description = NSPersistentStoreDescription(url: storeURL)
description.shouldMigrateStoreAutomatically = true
description.shouldInferMappingModelAutomatically = true
有时候 mapping model 推断不出来,就得手动写 Mapping Model 文件。
性能对比
SwiftData 在内存占用上更优,因为它用 @Model macro 在编译时生成了更高效的访问器。但 SwiftData 的查询能力比 Core Data 弱一些,复杂的 aggregate 查询(比如跨实体统计)做起来比较费劲。
Realm 在写入性能上领先,但读性能差不多。SQLite.swift 在简单场景下最快(因为最直接),但复杂查询要手写 SQL。
实际落地时的坑
用 SwiftData 踩了几个:
@Relationship的 inverse 必须写:不写 inverse 会导致数据不一致@Query不支持动态 predicate:必须用 init 初始化- Preview 时数据会保留:preview 和真机跑共用同一个 modelContainer,需要在 preview 里清理
- SwiftData 的 ModelContainer 是 App 级别的:如果需要在测试里 mock 数据,用
ModelContainer(for: ...inMemory: true)
struct ArticleListView_Previews: PreviewProvider {
static var previews: some View {
let config = ModelConfiguration(isStoredInMemoryOnly: true)
let container = try! ModelContainer(for: Article.self, configurations: config)
for i in 0..<5 {
let article = Article(id: "\(i)", title: "Article \(i)", content: "")
container.mainContext.insert(article)
}
return ArticleListView()
.modelContainer(container)
}
}
总结
选型建议:
- iOS 17+ 新项目:直接上 SwiftData,开发体验好,代码量少
- 需要支持 iOS 16:Core Data +
@FetchRequest - 跨平台需求(iOS + Android):Realm
- 简单数据模型:UserDefaults + JSON 文件
- 数据量特别大且查询复杂:SQLite.swift + 手写 SQL
选 SwiftData 的核心原因:它让数据层代码从 200 行降到 50 行,而且和 SwiftUI 的响应式模型天然契合。如果你还在用 Core Data 的老写法,不妨评估一下迁移成本。
