SwiftData

什么是 SwiftData

import SwiftData

其实就是一种 官方提供的 ORM,背后数据库不用关心。 重点其实还是 ORM 可以与 UI 状态更新良好配合。

@Model

SwiftData 运行的核心支点是 @Model

@Model 是一个宏(类似于 @Observable),它能够让你的变量(vars)实现持久化。

@Model class CodeBreaker { ... }

仅适用于类 class,因此你可能需要将结构体 struct 转换为类。@Model 默认遵守 Observable 协议。

因此,你不需要(也不应该)将一个已经标记为 @Model 的类再标记为 @Observable

vars 变量

@Model 中变量 类型的限制。在 @Model 中,只有特定类型的变量才能正常工作…

属性 Attributes

你可以放在变量(vars)上的属性(又称 架构属性 / schema attributes)

访问你的数据

当你把你的模型 Model 转换成了 @Model 之后,能用它做什么?

ModelContext

什么是 ModelContext

核心概念:在 SwiftData 中,要执行绝大多数操作,你都需要一个数据库的“句柄”(handle)。

声明方式

@Environment(\.modelContext) var modelContext

ModelContext 的功能

允许你对数据库中的 @Models 进行添加(add)、删除(delete)和获取(fetch)操作。允许你控制保存(saving)等操作(包括禁用自动保存)以及撤销(undo)。

使用条件与配置

这个 @Environment 变量只能在指定了 modelContainer 的视图(Views)中工作。

我们通常在 App 中使用 .modelContainer(for: CodeBreaker.self) 修饰符来完成这个配置。

关于 ModelContainer

ModelContainer 指定了 @Models 实际存储的位置。

示例:例如在应用程序沙盒中某个文件夹下的特定 SQL 数据库中。

它还有助于架构迁移(schema migration)。

在一个给定的应用程序中,你通常只需要使用一次这个修饰符(在最顶层)。

我们之所以关心 container(容器),主要是因为它能为我们提供上面提到的 modelContext 变量!

insert delete fetch

向数据库添加一个 @Model,使用 modelContext.insert() 将新创建的模型对象插入到数据库中。

let codeBreaker = CodeBreaker(name: "Test", pegChoices: [.red, .blue, .green])
modelContext.insert(codeBreaker)

从数据库删除一个 @Model,使用 modelContext.delete() 将指定的对象从数据库中移除。

modelContext.delete(codeBreaker)

从数据库获取 @Model

modelContext.fetch(fetchDescriptor)

在能够深入理解 modelContext.fetch 之前,我们首先需要理解什么是 FetchDescriptor(获取描述符)。

函数 fetch() 很简单,但是参数 fetchDescriptor 就有点复杂了,它是用来规定查询过滤条件和排序规则的。

获取数据 Fetching

要获取数据,你需要指定你想要哪些 @Models 以及你希望它们以什么顺序排列。

可以通过创建一个 FetchDescriptor 然后调用 modelContext.fetch() 来实现这一点。

通过 FetchDescriptor 的泛型参数来指定你需要哪种类的 @Model

通过 predicate 参数来指定你需要这些 @Model 中的哪一些。

通过 sortBy 参数来制定 @Model 返回时的顺序。

let fetchDescriptor = FetchDescriptor<CodeBreaker>(
    predicate: #Predicate { game in game.name.contains("foo") },
    sortBy: [.init(\.name), .init(\.lastAttemptDate)]
)

这里的 sortBy 是一个 Array<SortDescriptor<CodeBreaker>> 排序描述符数组)。

类型推导让我们可以在这里使用这种 .init(\.varName) 语法。

可以根据多个变量进行排序(首先按第一个排序,然后按其他变量进行次级排序)。

谓词 Predicate

谓词(Predicate)是一个受限的闭包 (limited closure),其中的代码用来描述你正在寻找哪些 @Models

let myPredicate = #Predicate<CodeBreaker> { game in
    game.attempts.contains(where: { $0.pegs.contains("blue") })
}

#Predicate 的闭包内部可以做的事情:

Fetching 失败异常

let results = modelContext.fetch(fetchDescriptor)

在这个例子中,results 将会是一个 Array<CodeBreaker>(数组中会先按 name 排序,然后按 lastAttemptDate 排序)。 不幸的是,这个调用可能会失败 (can fail)。

在 Swift 中,我们可以使用 do-catch 和 try 来“捕获”此类错误并进行恢复。

do {
    let results = try modelContext.fetch(fetchDescriptor)
    print(results)
    // 使用 results 执行其他操作
} catch let error { // 这里的 let error 并不是严格要求的
    // 从错误中恢复 且/或 报告错误;error 会告诉你出了什么问题
    print("couldn't fetch: error = \(error)")
}

错误处理

任何可能会失败 (can fail)的函数调用,其前面必须 (must) 带有某种形式的 try。

这类函数会被标记上关键字 throws(即它们可以“抛出”一个错误)。

当你调用这样一个函数时,编译器会进行提示。

任何带有 try 的表达式,其外层必须 (must) 包裹一个 do-catch(或者处于一个本身带有 throws 关键字的函数中)。

不过,还有 try!(尝试执行,如果失败则崩溃)和 try?(尝试执行,如果失败则返回 nil)。

let results = try? modelContext.fetch(fetchDescriptor)

此时,results 变成了一个 Optional<Array<CodeBreaker>>(可选类型),而不再仅仅是一个 Array<CodeBreaker>

@Query

但实际上我们并不会经常使用 modelContext.fetch()。相反,我们在视图(Views)中使用 @Query 宏。

@Query(filter: #Predicate { $0.name.contains("foo") }, sort: \.name, order: .forward)
private var games: [CodeBreaker]

行代码的意思是:查询所有名称中包含 “foo” 的 CodeBreaker 游戏数据,并按照名称(name)的正序(forward)进行排序,最后赋值给私有变量 games。

这会创建一个类似于 @State 的东西(即 games),它是一个 CodeBreaker 模型(@Models)的集合。

当数据库发生变化时,它会高效且自动地更新。因此,它完美地融入了 SwiftUI 的“响应式”用户界面范式。

它本质上是一个“常驻的获取描述符(FetchDescriptor)”,会被不断地重新评估/计算。

在初始化方法中动态设置 @Query

如果你想通过变量来设置 @Query 的参数,你必须在视图(View)的初始化方法(init)中进行。

与向 init 中传递 Binding(绑定)的方式类似。

简单实例

一个Person有多本书。

Book.swift

import Foundation
import SwiftData

@Model
class Book {

    var title: String

    @Relationship(inverse: \Person.books)
    var owner: Person?

    init(title: String) {
        self.title = title
    }
}

Person.swift

import Foundation
import SwiftData

@Model
class Person {

    @Attribute(.unique)
    var id: UUID

    var name: String

    var age: Int

    var birthday: Date

    @Attribute(.externalStorage)
    var avatar: Data?

    @Relationship(deleteRule: .cascade)
    var books: [Book] = []

    @Transient
    var isSelected = false

    init(name: String, age: Int) {
        self.id = UUID()
        self.name = name
        self.age = age
        self.birthday = Date()
    }
}

MeetXCodeApp.swift

设置 Container

import SwiftUI
import SwiftData

@main
struct MeetXCodeApp: App {
    var body: some Scene {
        WindowGroup {
            ContentView()
        }.modelContainer(for: [Person.self, Book.self])
    }
}

ContentView.swift

import SwiftUI
import SwiftData

struct ContentView: View {

    @Environment(\.modelContext)
    private var modelContext

    // 自动查询数据库
    @Query(sort: \Person.age)
    private var persons: [Person]

    @State private var keyword = ""

    var body: some View {

        NavigationStack {

            VStack {

                Text("SwiftData Demo")
                    .font(.title)

                TextField("输入查询关键字", text: $keyword)
                    .textFieldStyle(.roundedBorder)

                List {

                    ForEach(persons) { person in

                        VStack(alignment: .leading) {

                            Text(person.name)
                                .font(.headline)

                            Text("年龄:\(person.age)")

                            Text("书籍数量:\(person.books.count)")
                                .foregroundStyle(.gray)
                        }
                    }
                    .onDelete(perform: deletePerson)
                }

                VStack(spacing: 10) {

                    Button("插入一个 Person") {
                        insertPerson()
                    }

                    Button("Fetch 查询 age >= 20") {
                        fetchPersons()
                    }

                    Button("删除第一条") {

                        guard let first = persons.first else {
                            return
                        }

                        modelContext.delete(first)
                    }
                }
                .buttonStyle(.borderedProminent)
            }
            .padding()
        }
    }
}

extension ContentView {

    func insertPerson() {

        let person = Person(
            name: "Tom \(Int.random(in: 1...1000))",
            age: Int.random(in: 18...50)
        )

        person.books.append(Book(title: "Swift"))
        person.books.append(Book(title: "SwiftUI"))

        modelContext.insert(person)
    }
}

extension ContentView {

    func deletePerson(offsets: IndexSet) {

        for index in offsets {
            modelContext.delete(persons[index])
        }
    }
}

extension ContentView {

    func fetchPersons() {

        let descriptor = FetchDescriptor<Person>(
            predicate: #Predicate {

                $0.age >= 20
            },
            sortBy: [

                SortDescriptor(\Person.name),

                SortDescriptor(\Person.age)
            ]
        )

        do {

            let result = try modelContext.fetch(descriptor)

            print("查询结果:")

            for person in result {

                print(person.name, person.age)
            }

        } catch {

            print(error)
        }
    }
}

#Preview {
    ContentView()
}