SwiftData 是一个强大的现代框架,用于存储、查询和过滤数据 。它让我们定义对象和这些对象的属性,然后让我们从永久存储中读取和写入它们。
此外,SwiftData 能够对数据进行排序和过滤,并且可以处理更大的数据 - 它可以存储的数据量实际上没有限制。更好的是,当您真正需要依赖时,SwiftData 实现了各种更高级的功能:iCloud 同步、延迟加载数据、撤消和重做等等。
当您创建 Xcode 项目时,我要求您不要启用 SwiftData 支持,因为虽然它消除了一些无聊的设置代码,但它还添加了一大堆额外的示例代码,这些代码毫无意义,只需要删除。
手动配置SwiftData
三个步骤
- 使用 @Model 创建数据模型
对于类,将其转换为 SwiftData 对象,只需进行两个小更改。
此类称为 SwiftData模型:它定义了我们想要在应用程序中使用的某种数据。在幕后,@Model构建在@Observable所使用的相同观察系统之上,这意味着它与 SwiftUI 配合得非常好。
- 模型容器的建立
这项工作最好在App结构体中完成。它充当我们正在运行的整个应用程序的启动板。
var body: some Scene {
WindowGroup {
xxx()
}
.modelContainer(for: Student.self)
}
.modelContainer(for: Student.self),以便 SwiftData 在我们的应用程序中随处可用。
模型容器 是SwiftData 存储数据的位置的名称。您的App第一次运行时,这意味着 SwiftData 必须创建底层数据库文件,但在以后的运行中它将加载之前创建的数据库。
- 模型上下文
每个 SwiftData 应用程序都需要一个模型上下文来使用,并且我们已经创建了我们的模型上下文 - 当我们使用修饰符modelContainer()时它会自动创建,称为主上下文,并将其存储在 SwiftUI 的环境中,
它实际上是数据的“实时”版本——当您加载对象并更改它们时,这些更改仅存在于内存中,直到保存为止。因此,模型上下文的工作是让我们处理内存中的所有数据,这比不断读取和写入磁盘数据要快得多。
这就完成了我们所有的 SwiftData 配置
读取数据并写入数据
- 读取数据
从 SwiftData 检索信息是使用 查询 完成的 - 我们描述我们想要什么、应该如何排序以及是否应该使用任何过滤器,然后 SwiftData 会发回所有匹配的数据。我们需要确保此查询随着时间的推移保持最新,以便在创建或删除学生时我们的 UI 保持同步。
@Query
导入SwiftData,然后使用@Query,就可从其模型容器加载Student
@Query var students: [Student]
它会自动找到放置到环境中的主上下文,并通过那里查询容器。我们尚未指定要加载哪些学生,或如何对结果进行排序,因此我们将获取所有学生。
现在,可以开始students像常规 Swift 数组一样使用
NavigationStack {
List(students) { student in
Text(student.name)
}
}
- 添加/保存数据
首先我们需要一个新属性来,访问之前创建的模型上下文。
@Environment(\.modelContext) var modelContext
要求模型上下文添加 对象,这意味着它将被保存
// 创建一个Student对象
let student = Student(id: UUID(), name: "Tom")
// 添加
modelContext.insert(student)
如果您重新启动应用程序,您会发现您的学生仍然在那里,因为 SwiftData 自动保存了他们。
- 删除数据
// 找到对象
let book = books[offset]
// 删除
modelContext.delete(book)
// 删除所有
try? modelContext.delete(model: Book.self)
使用 SortDescriptor 对 SwiftData 查询进行排序
当您从@Query中提取SwiftData 对象时,您需要指定数据的排序方式 。
查询排序可以通过两种方式完成:一种是仅允许一个排序字段,另一种是允许使用名为 SortDescriptor的新类型的数组的更高级版本。
方式1
可能会要求根据书名按字母顺序提供书籍。
@Query(sort: \Book.title) var books: [Book]
或者我们可以要求按评分,从高到低对它们进行排序:
@Query(sort: \Book.rating, order: .reverse) var books: [Book]
方式2:SortDescriptor
当您只需要一个字段时,方式1很有效,但一般来说,可能希望有多个排序条件,如“按评级排序,然后按标题排序 ”。这是使用SortDescriptor类型完成的,我们可以从一个或两个值创建它们:我们想要排序的属性,以及是否应该反转(可选)。
按字母顺序对 title 属性进行排序:
@Query(sort: [SortDescriptor(\Book.title)]) var books: [Book]
与方式1 的排序方法一样,SortDescriptor默认情况下按升序对结果进行排序,这意味着文本按字母顺序排列,但如果您想反转排序顺序,则可以使用以下方法:
@Query(sort: [SortDescriptor(\Book.title, order: .reverse)]) var books: [Book]
您可以指定多个排序描述符,它们将按照您提供的顺序应用。 如:
首先按书名升序排序,然后按书作者升序排序 。
我们可能要求首先按书名升序排序,然后按书作者升序排序,如下所示:
@Query(sort: [
SortDescriptor(\Book.title),
SortDescriptor(\Book.author)
]) var books: [Book]
拥有第二个甚至第三个排序字段对性能几乎没有影响,除非您有大量具有相似值的数据。
编辑SwiftData模型对象
SwiftData 的模型对象由使类工作的相同观察系统@Observable提供支持,这意味着 SwiftUI 会自动拾取对模型对象的更改,以便我们的数据和用户界面保持同步。
这种支持扩展到我们之前看到的属性包装器@Bindable,这意味着我们可以进行令人愉快的简单对象编辑。
为了演示这一点,我们可以创建一个具有少量属性的简单类 User
@Model
class User {
var name: String
var city: String
var joinDate: Date
init(name: String, city: String, joinDate: Date) {
self.name = name
self.city = city
self.joinDate = joinDate
}
}
在App结构体中 配置
WindowGroup {
ContentView()
}
.modelContainer(for: User.self)
在视图中 编辑
struct EditUserView: View {
@Bindable var user: User
var body: some View {
Form {
TextField("Name", text: $user.name)
TextField("City", text: $user.city)
DatePicker("Join Date", selection: $user.joinDate)
}
}
}
这与我们使用常规类的@Observable 方式相同,但 SwiftData 仍然负责自动 把所有更改,写入永久存储
正如您所看到的,使用 SwiftData 对象进行编辑,与@Observable编辑常规类没有什么不同- 只是额外的好处是,我们所有的数据都被整齐地加载和保存!
使用谓词过滤@Query
之前,我们了解了如何@Query用于按特定顺序对 SwiftData 对象进行排序,但它也可以使用谓词来过滤数据,以决定返回什么。
我们还是使用 相同模型 User,尝试 过滤该数据,以便只显示姓名包含大写 R 的用户,并name 按字母排序 。
@Query(filter: #Predicate<User> { user in
user.name.contains("R")
}, sort: \User.name) var users: [User]
解释:过滤器以#Predicate<User> 开头,这意味着我们正在编写一个谓词 。该谓词为我们提供了一个要检查的用户实例(实际上, SwiftData 将为 加载的每个用户调用一次)。如果该用户应包含在结果中,我们需要返回 true。 接着,contains("R") 检查用户名是否包含大写字母 R。如果包含,则该用户将包含在结果中,否则不会包含在结果中。
如果想自动忽略字母大小写,请使用方法
localizedStandardContains()
此外,可以叠加过滤条件:用 Swift 的 运算符 &&
@Query(filter: #Predicate<User> { user in
user.name.contains("R") && user.city == "London"
}, sort: \User.name) var users: [User]
叠加过滤条件的另一种写法 :使用if else
@Query(filter: #Predicate<User> { user in
if user.name.contains("R") {
if user.city == "London" {
return true
} else {
return false
}
} else {
return false
}
}, sort: \User.name) var users: [User]
注意:if else 必须成对出现,不可省略
ModelConfiguration 临时数据 用于预览
ModelConfiguration类型允许我们请求临时内存存储。一旦我们有了它,我们就可以像平常一样创建一个SwiftData对象,而不需要模型容器和配置。 这在解决预览视图 困难很实用。
如:
struct BookDetailView: View {
let book: Book
var body: some View {
//....
}
}
#Preview {
do {
let config = ModelConfiguration(isStoredInMemoryOnly: true)
let container = try ModelContainer(for: Book.self, configurations: config)
let example = Book(title: "测试书", author: "测试作者", genre: "Fantasy", review: "这是一本好书", rating: 4)
return BookDetailView(book: example)
.modelContainer(container)
} catch {
return Text("Failed to create preview: \(error.localizedDescription)")
}
}