查询数据

温馨提示:本文最后更新于2025-01-19 17:10:25,某些文章具有时效性,若有错误或已失效,请在下方留言

SwiftData 提供了 @Query 宏,用于从 SwiftUI 视图中查询模型对象,并可选择提供排序顺序、过滤谓词,以及用于平滑处理结果变化的自定义动画或自定义事务。更棒的是,@Query 会在每次数据变化时自动保持最新,并重新调用你的 SwiftUI 视图,使其保持同步。

@Model class School {
    var name: String
    var students: [Student]
    init(name: String, students: [Student]) {
        self.name = name
        self.students = students
    }
}
@Model class Student {
    var id: UUID = UUID()
    var name: String
    var school: School?
    init(name: String, school: School? = nil) {
        self.name = name
        self.school = school
    }
}

现在我们可以显示一个按姓名排序的所有学生列表,如下所示:

struct AuthorsView: View {
    @Query(sort: \Student.name) var students: [Student]

    var body: some View {
        NavigationStack {
            List(students) { student in
                Text(student.name)
                Text("School: \(student.school?.name ?? "")")
            }
        }
    }
}

正如你所看到的,SwiftData 自动为我们提供了学校姓名属性,尽管这是通过关系提供的。SwiftData 会延迟加载这些关系——如果你没有使用某个关系,它就不会被获取。

当你在视图中使用 @Query 来加载SwiftData对象时,查询会在视图显示时立即运行。因此,你需要小心避免加载大量数据,因为这可能会导致用户界面暂时冻结。

通过 #Predicate 添加过滤条件

有很多方法可以自定义 @Query,比如使用 #Predicate 提供过滤条件。例如,我们可以编写一个过滤条件,只显示天涯小学的学生:

@Query(
    filter: #Predicate<Student> { student in
        student.school?.name == "天涯小学"
    },
    sort: \Student.name
) var students: [Student]

倒序排列

你还可以对排序顺序进行更细粒度的控制,例如要求将其倒序排列:

@Query(sort: \Student.name, order: .reverse) var students: [Student]

多个排序条件

或者通过提供一个排序描述符数组,按顺序应用它们:

@Query(
    sort: [
        SortDescriptor(\Student.name),
        SortDescriptor(\Student.yearOfBirth, order: .reverse)
    ]
) var students: [Student]

这段代码会按学生姓名的字母顺序进行排序,但如果有学生姓名相同,则按出生年份倒序排列。

你的视图可以拥有任意数量的 @Query 属性,不过如果超过三个,可能需要考虑是否有更高效的实现方式。

数量限制

对于更复杂的需求,你可以提供自定义的 FetchDescriptor,并使用额外的选项进行配置,比如设置获取数量限制或偏移量。需要注意的是,某些 FetchDescriptor 选项只有在描述符初始化后才可用。最简单的方式是使用描述符的静态属性,这样可以在查询中自由引用它。例如,如果你想按出生年份倒序排列学生,并获取最近的10个学生,可以这样实现:

static var descriptor: FetchDescriptor<Student> {
    var descriptor = FetchDescriptor<Student>(sortBy: [SortDescriptor(\.yearOfBirth, order: .reverse)])
    descriptor.fetchLimit = 10
    return descriptor
}

@Query(descriptor) var latestStudents: [Student]

无论如何创建, @Query 宏只能在 SwiftUI 视图中使用。Swift 不会阻止你在其他地方使用它,但它不会起作用!

下面视图通过自动获取和显示最喜欢的学生,简化了数据的获取和展示逻辑,确保界面在数据变化时自动更新。通过限制获取的学生数量和应用排序条件,提升了性能并优化了用户体验。

struct StudentList: View {
    static var fetchDescriptor: FetchDescriptor<Student> {
        let descriptor = FetchDescriptor<Student>(
            predicate: #Predicate { $0.isFavorite == true },
            sortBy: [ .init(\.createdAt) ]
        )
        descriptor.fetchLimit = 10
        return descriptor
    }

    @Query(fetchDescriptor) private var favoriteStudents: [Student]
    var body: some View {
        List(favoriteStudents) { StudentRowView($0) }
    }
} 

自定义返回结果

一次性将整个对象加载到内存中可能会导致性能损失。我们可以通过 FetchDescriptor 自定义请求,只获取计划使用的属性。例如,如果只想显示学生的名称和年龄,可以使用以下描述符:

var fetchDescriptor = FetchDescriptor<Student>(sortBy: [SortDescriptor(\.name, order: .reverse)])
fetchDescriptor.propertiesToFetch = [\.name, \.arg]

预先加载的相关模型

预取使 SwiftData能够在一次提取中获取所有相关模型,而不是在访问每个相关模型时再次访问持久存储。如下示例,假设我们有个学校与学生的模型相关联,并且提取学生信息的时候,显示它们所在的学校,这种情况下,模型上下文可能需要对每个部门执行额外的提取,导致显著的性能开销。通过提取学生时预先提取学校关系,可以避免这种情况

static var fetchDescriptor: FetchDescriptor<Student> {
    var descriptor = FetchDescriptor<Student>()
    descriptor.relationshipKeyPathsForPrefetching = [\.school]
    return descriptor
}

@Query(fetchDescriptor) private var student: [Student]

includePendingChanges 选项用于控制在提取数据时是否包含尚未保存的更改。默认情况下,此选项为 true,意味着在提取数据时,你可以看到所有当前状态,包括已修改但未保存的内容。换句话说,如果你对数据进行了修改但尚未保存,设置此选项为 true 将允许你在提取时查看这些未保存的更改。

分页查询

假设页面大小为100,且当前位于第三页(从0开始计数),我们可以编写如下代码:

let pageSize = 100
let pageNumber = 2

var fetchDescriptor = FetchDescriptor<Student>(sortBy: [SortDescriptor(\.name, order: .reverse)])
fetchDescriptor.fetchOffset = pageNumber * pageSize
fetchDescriptor.fetchLimit = pageSize

在这个示例中, fetchOffset 设置为 200, fetchLimit 设置为 100。这意味着 SwiftData 将尝试在结果中返回对象 201至 300。

字符串过滤条件

使用谓词通过 contains() 来过滤字符串,因此这将返回所有带 wang 的学生名称:

@Query(filter: #Predicate<Student> { student in
    student.name.starts(with: "Wang")
}) var students: [Student]

starts(with:)在谓词中是支持的,但 hasPrefix()hasSuffix()不支持。

字符串比较如 starts(with:)contains()是区分大小写的。如果你尝试使用 movie.name.uppercased(),你会发现 uppercassed()(和 lowercased())在谓词中不被支持,而如果你尝试使用movie.name.localizedUppercase,虽然可以编译,但在运行时会崩溃。

相反,你应该像这样执行不区分大小写的搜索:

@Query(filter: #Predicate<Student> { student in
    student.name.localizedStandardContains("Wang")
}) var students: [Student]

关系条件

你可以创建依赖于关系的谓词,例如只显示学生小于 10学校:

@Query(filter: #Predicate<School> { school in
    school.students.count > 10
}) var schools: [School]

排除学校中名为‘小明”的所有同名同姓学生的子集:

@Query(filter: #Predicate<School> { school in
    !school.students.contains { $0.name == "Tom Cruise" }
}) var schools: [School]

通过 allSatisfy 判断序列中的每个元素是否满足给定的谓词条件,例如过滤出所有学生名字长度都大于等于3的学校:

@Query(filter: #Predicate<School> { school in
    school.students.allSatisfy { $0.name.count >= 3 }
}) var schools6: [School]

通过 filter 过滤序列中的每个元素,并判断结果是否为空的谓词条件,例如仅返回所有学生名字长度都小于3的学校:

@Query(filter: #Predicate<School> { school in
    school.students.filter { $0.name.count >= 3 }.isEmpty
}) var schools6: [School]

表达式 AND(&&) 和OR(||)

可以使用 OR(||)来过滤数据范围,例如仅显示有10到 100个学生的学校:

@Query(filter: #Predicate<School> { school in
    school.students.count > 10 || school.students.count < 100
}) var schools: [School]

可以使用 AND(&&) 添加多个过滤条件,例如仅显示名字为 Wang 且年龄为20的学生:

@Query(filter: #Predicate<Student> { student in
    student.name.starts(with: "Wang") && student.age == 20
}) var students: [Student]

条件空的判断

使用isEmpty 条件空的判断,我们需要再次小心。以下代码是可以正常工作的:

@Query(filter: #Predicate<School> { school in
    school.students.isEmpty
}) var schools: [School]

但这段代码会在运行时崩溃:

@Query(filter: #Predicate<School> { school in
    school.students.isEmpty == false
}) var schools: [School]

巧妙的是,这段代码实际上可以正确构建并运行:

@Query(filter: #Predicate<School> { school in
    !school.students.isEmpty
}) var schools: [School]

与编写 movie.cast.isEmpty == false 相同,但由于 #Predicate 宏的不同构建方式,结果是一个有效的谓词。

自然的字符串排序

使用默认排序对字符串进行排序时,结果将是:School 1、School 10、School 100、School 11、School2。这种排序方式并不符合人们的直观认知。为了解决这个问题,我们可以使用.localizedstandard 比较器创建一个排序描述符,它将生成更符合自然语言的排序方式,类似于 Finder 中的排序。

如果使用 @Query 宏,可以像下面的代码那样实现:

@Query(
    sort: [SortDescriptor(
        \School.name,
        comparator: .localizedStandard
    )]
) var schools: [School]

查询得到的结果将是:School 1、School 2、School 3 …

查询数量

要获取学生的数量,我们可以使用 @Query var students:[Student]来获取数据,并通过 students.count 来获取学生数量。然而,这种方法可能会导致不必要的性能损失。我们可以直接使用 fetchCount 来获取学生的数量,示例代码如下:

let descriptor = FetchDescriptor<School>(predicate: #Predicate { $0.students.count > 10 })
let count = (try? modelContext.fetchCount(descriptor)) ?? 0

这样可以在数据库中完成整个计数过程,避免浪费时间和内存。相反,使用常规的取值方法然后执行计数会不必要地将所有对象加载到内存中。因此,这种做法是低质量的:

let descriptor = FetchDescriptor<School>(predicate: #Predicate { $0.students.count > 10 })
let objects = (try? modelContext.fetch(descriptor)) ?? []
let count = objects.count

查询提取到模型

将 SwiftData 查询的创建提取到模型中,以便复用。

@Model class Student {
    var id: UUID = UUID()
    var name: String
    var school: School?
    init(name: String, school: School? = nil) {
        self.name = name
        self.school = school
    }

    static var students: FetchDescriptor<Student> {
        FetchDescriptor(predicate: #Predicate { $0.name.starts(with: "Wang") && $0.age == 20 })
    }
}

struct StudentList: View {
    @Query(Student.students) var students: [Student]
    var body: some View {
        List(students) { StudentRowView($0) }
    }
} 

SwiftUl 中最佳实践

下面示例展示了如何使用 QueryView 来动态获取和展示模型数据。在 ContentView 中,我们指定查询类型为Student,并通过 ForEach循环展示每个学生的姓名:

struct ContentView: View {
    var body: some View {
        QueryView(type: Student.self) { data in
            ForEach(data, id: \.self) { item in
                Text("\(item.name)").id(item.id)
            }
        }
    }
}

通过使用泛型,QueryView 变得非常灵活,可以支持任何符合 PersistentModel 协议的数据类型,不仅仅是 Student

struct QueryView<Model: PersistentModel, Content: View>: View {
    @Query private var query: [Model]
    var type: Model.Type
    @ViewBuilder var content: ([Model]) -> (Content)
    var body: some View {
        content(query)
    }
}

通过在 QueryView 基础上添加过滤条件,重新封装一个 QueryFilterView 组件,便于在查询数据时灵活应用各种过滤条件。

struct QueryFilterView<Model: PersistentModel, Content: View>: View {
    @Query private var query: [Model]
    private var content: ([Model]) -> (Content)
    
    init(for type: Model.Type,
         sort: [SortDescriptor<Model>] = [],
         @ViewBuilder content: @escaping ([Model]) -> Content,
         filter: (() -> (Predicate<Model>))? = nil
    ) {
        _query = Query(filter: filter?(), sort: sort)
        self.content = content
    }
    
    var body: some View {
        content(query)
    }
}

QueryFilterView 是一个灵活的组件,允许在查询数据时根据用户输入应用自定义的过滤条件和排序规则。通过结合 PredicateSortDescriptor,可以轻松实现动态的筛选和排序功能,例如根据用户输入的关键字进行搜索,并按特定字段升序或降序排列数据,使得数据展示更加智能和互动。

import SwiftUI
import SwiftData

struct ContentView: View {
    @State var searcWord = ""
    @State var ageSort: SortDescriptor<Student> = .init(\.age, order: .forward)
    var body: some View {
        VStack {
            HStack {
                TextField("Search", text: $searcWord)
                Button("Age", systemImage: "arrow.\(ageSort.order == .forward ? "up" : "down")", action: {
                    ageSort.order = ageSort.order == .forward ? .reverse : .forward
                })
            }
            QueryFilterView(for: Student.self, sort: [ageSort]) { data in
                ForEach(data, content: { item in
                    Text("\(item.name) ~ \(item.age)").id(item.id)
                })
            } filter: {
                #Predicate { item in
                    if searcWord.isEmpty {
                        true
                    } else {
                        item.name.contains(searcWord)
                    }
                }
            }
        }
    }
}

本示例演示了如何使用 QueryFilterView 在 SwiftUl 中集成动态筛选和排序功能。

© 版权声明
THE END
喜欢就支持一下吧
点赞0 分享
评论 抢沙发

请登录后发表评论

    暂无评论内容