import SwiftData其实就是一种 官方提供的 ORM,背后数据库不用关心。 重点其实还是 ORM 可以与 UI 状态更新良好配合。
SwiftData 运行的核心支点是 @Model。
@Model 是一个宏(类似于
@Observable),它能够让你的变量(vars)实现持久化。
@Model class CodeBreaker { ... }仅适用于类 class,因此你可能需要将结构体 struct
转换为类。@Model 默认遵守 Observable 协议。
因此,你不需要(也不应该)将一个已经标记为 @Model
的类再标记为 @Observable。
对 @Model 中变量 类型的限制。在 @Model 中,只有特定类型的变量才能正常工作…
你可以放在变量(vars)上的属性(又称 架构属性 / schema attributes)
@Attribute(.unique) 唯一约束。数据库中只能有一个 @Model
实例在该变量上拥有给定的值。@Attribute(.externalStorage)
外部存储。将该变量的数据存储在数据库之外(例如:JPEG
图片等大文件)。@Attribute(.transformable(by:))
可转换。提供一个转换器(transformer),以便将数据作为另一种不同的类型进行存储。@Attribute(.ephemeral)
瞬态/临时。跟踪其变化,但在持久化(保存)时会重置为默认值?@Transient
不持久化。不保存到数据库中;不过似乎它也不会被 SwiftUI 跟踪更改?@Relationship(deleteRule: .cascade / .nullify / .deny / .noAction)
关系删除规则。定义当对象被删除时,关联的数据该如何处理(级联删除 / 置空
/ 拒绝删除 / 无操作)。@Relationship(inverse: \.nameOfVar)
反向关系。用于明确这组关系是一对一(to-one)还是一对多(to-many)。当你把你的模型 Model 转换成了 @Model 之后,能用它做什么?
核心概念:在 SwiftData 中,要执行绝大多数操作,你都需要一个数据库的“句柄”(handle)。
声明方式
@Environment(\.modelContext) var modelContext允许你对数据库中的 @Models 进行添加(add)、删除(delete)和获取(fetch)操作。允许你控制保存(saving)等操作(包括禁用自动保存)以及撤销(undo)。
这个 @Environment 变量只能在指定了 modelContainer
的视图(Views)中工作。
我们通常在 App 中使用
.modelContainer(for: CodeBreaker.self)
修饰符来完成这个配置。
ModelContainer 指定了 @Models 实际存储的位置。
示例:例如在应用程序沙盒中某个文件夹下的特定 SQL 数据库中。
它还有助于架构迁移(schema migration)。
在一个给定的应用程序中,你通常只需要使用一次这个修饰符(在最顶层)。
我们之所以关心 container(容器),主要是因为它能为我们提供上面提到的 modelContext 变量!
向数据库添加一个 @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
就有点复杂了,它是用来规定查询过滤条件和排序规则的。
要获取数据,你需要指定你想要哪些 @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)是一个受限的闭包 (limited closure),其中的代码用来描述你正在寻找哪些 @Models。
let myPredicate = #Predicate<CodeBreaker> { game in
game.attempts.contains(where: { $0.pegs.contains("blue") })
}在 #Predicate 的闭包内部可以做的事情:
+ - * / %< > != 等if 或带有 && ||
的三元运算符allSatisfy, filter, contains, starts(with:), max(), min()[ ] 以及成员(点语法
.)访问contains, caseInsensitiveCompare, localizedComparelet 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>。
但实际上我们并不会经常使用
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 的参数,你必须在视图(View)的初始化方法(init)中进行。
与向 init 中传递 Binding(绑定)的方式类似。
一个Person有多本书。
import Foundation
import SwiftData
@Model
class Book {
var title: String
@Relationship(inverse: \Person.books)
var owner: Person?
init(title: String) {
self.title = title
}
}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()
}
}设置 Container
import SwiftUI
import SwiftData
@main
struct MeetXCodeApp: App {
var body: some Scene {
WindowGroup {
ContentView()
}.modelContainer(for: [Person.self, Book.self])
}
}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()
}