SwiftData的使用

1,519 阅读7分钟

SwiftData 是一个强大的现代框架,用于存储、查询和过滤数据 。它让我们定义对象和这些对象的属性,然后让我们从永久存储中读取和写入它们。

此外,SwiftData 能够对数据进行排序和过滤,并且可以处理更大的数据 - 它可以存储的数据量实际上没有限制。更好的是,当您真正需要依赖时,SwiftData 实现了各种更高级的功能:iCloud 同步、延迟加载数据、撤消和重做等等。

当您创建 Xcode 项目时,我要求您不要启用 SwiftData 支持,因为虽然它消除了一些无聊的设置代码,但它还添加了一大堆额外的示例代码,这些代码毫无意义,只需要删除。

手动配置SwiftData

三个步骤

  1. 使用 @Model 创建数据模型

对于类,将其转换为 SwiftData 对象,只需进行两个小更改。

image.png

此类称为 SwiftData模型:它定义了我们想要在应用程序中使用的某种数据。在幕后,@Model构建在@Observable所使用的相同观察系统之上,这意味着它与 SwiftUI 配合得非常好。

  1. 模型容器的建立

这项工作最好在App结构体中完成。它充当我们正在运行的整个应用程序的启动板。

var body: some Scene {
    WindowGroup {

        xxx()
    }
    .modelContainer(for: Student.self)
}

.modelContainer(for: Student.self),以便 SwiftData 在我们的应用程序中随处可用。

模型容器 是SwiftData 存储数据的位置的名称。您的App第一次运行时,这意味着 SwiftData 必须创建底层数据库文件,但在以后的运行中它将加载之前创建的数据库。

  1. 模型上下文

每个 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)")
    }
}