SwiftData 实战

利用 Swift 宏技术构建现代化的持久化层,探索模型定义、关系映射与 iCloud 自动化同步。

告别 Core Data 的复杂配置,拥抱纯 Swift 的声明式持久化体验。iOS 17+ 推出的全新框架。


1. SwiftData 简介

SwiftData 是 Apple 在 WWDC23 推出的现代化数据持久化框架,它基于 Core Data 构建,但提供了更简洁的 Swift 原生 API。

核心优势

  • 声明式定义:使用 @Model 宏定义数据模型
  • 自动同步:与 SwiftUI 深度集成,数据变化自动刷新 UI
  • 类型安全:使用 #Predicate 进行编译时检查的查询
  • 零配置:无需手动管理 Schema 迁移

2. @Model 宏

基础定义

import SwiftData

@Model
final class TodoItem {
    var title: String
    var timestamp: Date
    var isDone: Bool
    var priority: Int
    
    init(title: String, timestamp: Date = .now, isDone: Bool = false, priority: Int = 0) {
        self.title = title
        self.timestamp = timestamp
        self.isDone = isDone
        self.priority = priority
    }
}

属性修饰符

@Model
final class User {
    // 唯一约束
    @Attribute(.unique) var email: String
    
    var name: String
    
    // 不持久化的属性
    @Transient var temporaryData: String = ""
    
    // 自定义属性名
    @Attribute(originalName: "user_avatar") var avatar: Data?
    
    // 加密存储
    @Attribute(.encrypt) var sensitiveInfo: String?
    
    init(email: String, name: String) {
        self.email = email
        self.name = name
    }
}

关系定义

@Model
final class Author {
    var name: String
    
    // 一对多关系
    @Relationship(deleteRule: .cascade, inverse: \Book.author)
    var books: [Book] = []
    
    init(name: String) {
        self.name = name
    }
}

@Model
final class Book {
    var title: String
    var author: Author?
    
    init(title: String, author: Author? = nil) {
        self.title = title
        self.author = author
    }
}

3. 配置与初始化

基础配置

import SwiftUI
import SwiftData

@main
struct MyApp: App {
    var body: some Scene {
        WindowGroup {
            ContentView()
        }
        .modelContainer(for: [TodoItem.self, User.self])
    }
}

自定义配置

@main
struct MyApp: App {
    let container: ModelContainer
    
    init() {
        let schema = Schema([TodoItem.self, User.self])
        let config = ModelConfiguration(
            schema: schema,
            isStoredInMemoryOnly: false,  // 是否仅内存存储
            allowsSave: true,              // 是否允许保存
            groupContainer: .automatic     // App Group 支持
        )
        
        do {
            container = try ModelContainer(for: schema, configurations: config)
        } catch {
            fatalError("无法创建 ModelContainer: \(error)")
        }
    }
    
    var body: some Scene {
        WindowGroup {
            ContentView()
        }
        .modelContainer(container)
    }
}

多配置场景

// 主数据库 + 缓存数据库
let mainConfig = ModelConfiguration(
    "Main",
    schema: Schema([User.self, Post.self])
)

let cacheConfig = ModelConfiguration(
    "Cache",
    schema: Schema([CachedImage.self]),
    isStoredInMemoryOnly: true
)

let container = try ModelContainer(
    for: Schema([User.self, Post.self, CachedImage.self]),
    configurations: mainConfig, cacheConfig
)

4. CRUD 操作

创建 (Create)

struct AddTodoView: View {
    @Environment(\.modelContext) private var context
    @State private var title = ""
    
    var body: some View {
        Form {
            TextField("标题", text: $title)
            Button("添加") {
                let todo = TodoItem(title: title)
                context.insert(todo)
                // 自动保存,无需手动调用 save()
            }
        }
    }
}

读取 (Read)

struct TodoListView: View {
    // 自动查询并监听变化
    @Query var todos: [TodoItem]
    
    // 带排序
    @Query(sort: \TodoItem.timestamp, order: .reverse)
    var sortedTodos: [TodoItem]
    
    // 带过滤
    @Query(filter: #Predicate<TodoItem> { !$0.isDone })
    var pendingTodos: [TodoItem]
    
    // 复杂查询
    @Query(
        filter: #Predicate<TodoItem> { $0.priority > 5 && !$0.isDone },
        sort: [SortDescriptor(\TodoItem.priority, order: .reverse)],
        animation: .default
    )
    var urgentTodos: [TodoItem]
    
    var body: some View {
        List(todos) { todo in
            TodoRow(todo: todo)
        }
    }
}

动态查询

struct SearchableListView: View {
    @State private var searchText = ""
    
    var body: some View {
        TodoListContent(searchText: searchText)
            .searchable(text: $searchText)
    }
}

struct TodoListContent: View {
    @Query var todos: [TodoItem]
    
    init(searchText: String) {
        let predicate = #Predicate<TodoItem> { todo in
            searchText.isEmpty || todo.title.contains(searchText)
        }
        _todos = Query(filter: predicate, sort: \TodoItem.timestamp)
    }
    
    var body: some View {
        List(todos) { todo in
            Text(todo.title)
        }
    }
}

更新 (Update)

struct TodoRow: View {
    @Bindable var todo: TodoItem  // iOS 17+ @Bindable
    
    var body: some View {
        HStack {
            TextField("标题", text: $todo.title)
            Toggle("完成", isOn: $todo.isDone)
        }
        // 修改自动保存
    }
}

删除 (Delete)

struct TodoListView: View {
    @Environment(\.modelContext) private var context
    @Query var todos: [TodoItem]
    
    var body: some View {
        List {
            ForEach(todos) { todo in
                TodoRow(todo: todo)
            }
            .onDelete(perform: deleteTodos)
        }
    }
    
    private func deleteTodos(at offsets: IndexSet) {
        for index in offsets {
            context.delete(todos[index])
        }
    }
}

5. #Predicate 查询

基础用法

// 简单条件
let completed = #Predicate<TodoItem> { $0.isDone }

// 多条件
let urgent = #Predicate<TodoItem> { item in
    item.priority > 5 && !item.isDone
}

// 字符串匹配
let search = #Predicate<TodoItem> { item in
    item.title.localizedStandardContains("Swift")
}

// 日期比较
let today = Date()
let recentItems = #Predicate<TodoItem> { item in
    item.timestamp > today.addingTimeInterval(-86400)
}

动态 Predicate

func createPredicate(searchText: String, showCompleted: Bool) -> Predicate<TodoItem> {
    return #Predicate<TodoItem> { item in
        (searchText.isEmpty || item.title.contains(searchText)) &&
        (showCompleted || !item.isDone)
    }
}

6. 后台操作

ModelActor

@ModelActor
actor DataManager {
    func importData(_ items: [ImportItem]) throws {
        for item in items {
            let todo = TodoItem(title: item.title)
            modelContext.insert(todo)
        }
        try modelContext.save()
    }
    
    func fetchCount() -> Int {
        let descriptor = FetchDescriptor<TodoItem>()
        return (try? modelContext.fetchCount(descriptor)) ?? 0
    }
}

// 使用
struct ContentView: View {
    @Environment(\.modelContext) private var context
    
    var body: some View {
        Button("导入数据") {
            Task {
                let manager = DataManager(modelContainer: context.container)
                try await manager.importData(importItems)
            }
        }
    }
}

7. 数据迁移

轻量迁移

SwiftData 支持自动轻量迁移:

  • 添加新属性(需提供默认值)
  • 删除属性
  • 重命名属性(使用 originalName
@Model
final class TodoItem {
    var title: String
    var isDone: Bool
    
    // 新增属性,提供默认值
    var createdAt: Date = .now
    
    // 重命名属性
    @Attribute(originalName: "done") var isCompleted: Bool
}

自定义迁移

enum TodoSchemaV1: VersionedSchema {
    static var models: [any PersistentModel.Type] { [TodoItem.self] }
    static var versionIdentifier = Schema.Version(1, 0, 0)
    
    @Model
    final class TodoItem {
        var title: String
        var isDone: Bool
    }
}

enum TodoSchemaV2: VersionedSchema {
    static var models: [any PersistentModel.Type] { [TodoItem.self] }
    static var versionIdentifier = Schema.Version(2, 0, 0)
    
    @Model
    final class TodoItem {
        var title: String
        var isDone: Bool
        var priority: Int = 0  // 新增
    }
}

enum TodoMigrationPlan: SchemaMigrationPlan {
    static var schemas: [any VersionedSchema.Type] {
        [TodoSchemaV1.self, TodoSchemaV2.self]
    }
    
    static var stages: [MigrationStage] {
        [migrateV1toV2]
    }
    
    static let migrateV1toV2 = MigrationStage.lightweight(
        fromVersion: TodoSchemaV1.self,
        toVersion: TodoSchemaV2.self
    )
}

8. 与 Core Data 对比

特性 Core Data SwiftData
模型定义 .xcdatamodeld 文件 Swift 代码 + @Model
上下文管理 手动 NSManagedObjectContext 自动 @Environment
查询语法 NSPredicate 字符串 #Predicate 类型安全
SwiftUI 集成 @FetchRequest @Query
学习曲线 陡峭 平缓
最低版本 iOS 3+ iOS 17+

9. 最佳实践

✅ 推荐

// 1. 使用 @Query 自动监听数据变化
@Query var items: [Item]

// 2. 在 @Model 类中提供完整的初始化器
@Model
final class Item {
    var name: String
    var createdAt: Date
    
    init(name: String, createdAt: Date = .now) {
        self.name = name
        self.createdAt = createdAt
    }
}

// 3. 使用 @ModelActor 进行后台操作
@ModelActor
actor BackgroundProcessor { }

❌ 避免

// 1. 不要在主线程进行大量数据操作
for item in hugeArray {
    context.insert(Item(name: item))  // ❌ 阻塞 UI
}

// 2. 不要忘记处理可选关系
@Model class Book {
    var author: Author  // ❌ 应该是可选的
}

// 3. 不要在 @Transient 属性上依赖持久化
@Transient var cache: [String] = []  // ❌ 重启后丢失