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 集成需要适配
SwiftDataSwiftUI 原生集成、代码量少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 踩了几个:

  1. @Relationship 的 inverse 必须写:不写 inverse 会导致数据不一致
  2. @Query 不支持动态 predicate:必须用 init 初始化
  3. Preview 时数据会保留:preview 和真机跑共用同一个 modelContainer,需要在 preview 里清理
  4. 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)
    }
}

总结

选型建议:

  1. iOS 17+ 新项目:直接上 SwiftData,开发体验好,代码量少
  2. 需要支持 iOS 16:Core Data + @FetchRequest
  3. 跨平台需求(iOS + Android):Realm
  4. 简单数据模型:UserDefaults + JSON 文件
  5. 数据量特别大且查询复杂:SQLite.swift + 手写 SQL

选 SwiftData 的核心原因:它让数据层代码从 200 行降到 50 行,而且和 SwiftUI 的响应式模型天然契合。如果你还在用 Core Data 的老写法,不妨评估一下迁移成本。

最后更新 4/20/2026, 4:48:48 AM