Skip to content

SwiftData

SwiftData 教程

CRUD

modelContainer 存储数据的默认 sqlite 文件路径

默认路径在 URL.applicationSupportDirectory.path(percentEncoded: false)

swift
import SwiftUI
import SwiftData

@main
struct SwiftUI_BookApp: App {
    var body: some Scene {
        WindowGroup {
            ContentView()
        }
        .modelContainer(for: Book.self)
    }

    init() {
        print(URL.applicationSupportDirectory.path(percentEncoded: false))
    }
}

输出:

/Users/luca/Library/Developer/CoreSimulator/Devices/B5AFEF8F-FA98-4FCD-8C43-10B51B3561B0/data/Containers/Data/Application/437D58C9-6F43-4E1E-A2C6-8BDFEDE4B064/Library/Application Support/
  • 实现:1.新增 2.查询 3.删除 4.更新

modelContainer

自定义 SQLite 数据库路径

在构造器里自定义 modelContainer,这里只是改了下 SQLite 的文件名

也可以使用 ModelConfiguration 的 url 参数来调整文件的存储路径

swift
import SwiftUI
import SwiftData

@main
struct SwiftUI_BookApp: App {
    let modelContainer: ModelContainer

    var body: some Scene {
        WindowGroup {
            BookListView()
        }
        .modelContainer(modelContainer)
    }

    init() {
        let schema = Schema([Book.self])
        let config = ModelConfiguration("MyBooks", schema: schema)

        do {
            modelContainer = try ModelContainer(for: schema, configurations: config)
        } catch {
            fatalError("Could not configure the container")
        }
    }
}
配置 Preview 的 modelContainer
swift
import Foundation
import SwiftData

struct Preview {
    let container: ModelContainer
    init(_ models: any PersistentModel.Type...) {
        let config = ModelConfiguration(isStoredInMemoryOnly: true)
        let schema = Schema(models)
        do {
            container = try ModelContainer(for: schema, configurations: config)
        } catch {
            fatalError("Could not create preview container")
        }
    }

    func addExamples(_ examples: [any PersistentModel]) {
        Task { @MainActor in
            examples.forEach { example in
                container.mainContext.insert(example)
            }
        }
    }
}

Dynamic Sorts and Filters

排序和过滤的核心逻辑
swift
@Query(sort: \Book.status) private var books: [Book]

init(sortOrder: SortOrder, filterString: String) {
    let sort = switch sortOrder {
    case .status:
        [SortDescriptor(\Book.status), SortDescriptor(\Book.title)]
    case .title:
        [SortDescriptor(\Book.title)]
    case .author:
        [SortDescriptor(\Book.author)]
    }

    let filter = #Predicate<Book> { book in
        book.title.localizedStandardContains(filterString)
            || book.author.localizedStandardContains(filterString)
            || filterString.isEmpty
    }

    // 当你使用 @Query 属性包装器时,Swift 编译器会自动生成两个属性:
    // _books - 访问 Query 包装器本身,可以重新初始化
    // books - 只能访问查询结果数组,无法改变查询配置
    _books = Query(filter: filter, sort: sort)
}

Migration

  • 新增字段: Must be optional or have default value
  • 修改字段名称
swift
@Attribute(originalName: "summary")
var avatar: Data
  • 删除字段
其它 Migration
swift
// 图片、音频、视频等媒体文件
@Attribute(.externalStorage)
var avatar: Data

// 在 iCloud 同步时需要进行端到端加密
@Attribute(.allowsCloudEncryption)
var sin: String

@Attribute(.unique)
var title: String

@Attribute(.deleteRules: .cascade)
var quotes: [Quotes]?

拓展:MVVM 改造

  • @Observable 替代 Combine
  • @Bindable:接收 + 延续绑定能力
  • let:接收 + 只读(不需要绑定)
  • @Bindable 和 @Binding 的区别
@Observable 什么时候需要 @MainActor?
  1. 有后台任务,需要更新 UI
swift
@Observable
class DataViewModel {
    var data: [Item] = []
    var isLoading = false

    func fetchData() async {
        isLoading = true

        // 后台网络请求
        let result = await networkService.fetch()

        // ⚠️ 这里需要确保在主线程更新 UI
        await MainActor.run {
            self.data = result
            self.isLoading = false
        }
    }
}

或者更优雅的方式:

swift
@Observable
class DataViewModel {
    var data: [Item] = []
    var isLoading = false

    @MainActor  // ← 只标记这个函数
    func updateUI(with result: [Item]) {
        self.data = result
        self.isLoading = false
    }

    func fetchData() async {
        isLoading = true
        let result = await networkService.fetch()
        await updateUI(with: result)
    }
}
  1. 整个 ViewModel 都涉及复杂异步操作
swift
@MainActor  // ← 整个类都在主线程
@Observable
class ComplexViewModel {
    var data: [Item] = []
    var status: String = ""

    func fetchData() async {
        // 复杂的异步操作
        data = await service.fetch()
        status = "完成"  // 自动在主线程
    }

    func processData() async {
        // 更多异步操作
        data = await processor.process(data)
    }
}
actor 关键词
  • 请求在访问 actor 的状态时是会加锁的,这样其它请求就无法访问这个状态了,从而保证线程安全
  • Actor 保证 "永远不会有两个线程同时访问状态",但不保证 "await 前后状态不变"
  • 片段级别的线程安全,而不是方法级别的线程安全

One to Many Relationships

  • 一对多关系:Book -> Quote
swift
@Model
class Quote {
    @Relationship(deleteRule: .cascade)
    var book: Book?
}

@Model
class Book {
    var quotes: [Quote]? // 需要设置为 ?,否则后续集成 CloudKit 会有问题
}
  • 删除级联:@Relationship(deleteRule: .cascade)

Many to Many Relationships

  • 多对多关系:Book <-> Genre
swift
@Model
class Genre {
    var books: [Book]?
}

@Model
class Book {
    @Relationship(inverse: \Genre.books)
    var genres: [Genre]?
}
  • 做了啥?
    • 创建 GenresView
    • 创建 NewGenreView 并通过 sheet 集成到 GenresView 里
    • 在 EditBookView 通过 sheet 集成到 GenresView 里
    • 创建 GenresStackView
  • 使用 ViewThatFits 和 ScrollView 解决 Genre 移除