本篇文章将通过调整@Query的实例显示对应的实例,通过按钮等形式,将实例的参数进行限制或者进行排序,从而实现动态排序的效果。
动态显示视图
首先创建一个接受传参并动态显示的视图:
struct UsersView: View {
@Environment(\.modelContext) var modelContext
@Query var users: [User]
init(minimumJoinDate: Date) {
_users = Query(filter: #Predicate<User> { user in
user.joinDate >= minimumJoinDate
}, sort: \User.name)
}
var body: some View {
List(users) { user in
Text(user.name)
}
}
}
这段代码的含义为从数据库中自动查询数据,并将结果作为数组绑定到视图中,在init初始化时设置动态的查询条件。
代码解析
@Query var users: [User]
首先设置一个没有默认查询条件的@Query,加载所有的User实例。
init(minimumJoinDate: Date) {
_users = Query(filter: #Predicate<User> { user in
user.joinDate >= minimumJoinDate
}, sort: \User.name)
}
在初始化UsersView时,动态的覆盖默认的查询逻辑:
1、过滤器:只加载 joinDate 大于或等于 minimumJoinDate 的 User 实例。
2、排序:按 User.name 升序排列。
minimumJoinDate 是什么?
minimumJoinDate 是通过初始化器传入的普通参数。它的作用是作为查询的一部分,用来动态设置 @Query 的过滤条件。
在这里:
minimumJoinDate 是一个初始化参数,用来生成一个符合条件的 Query。
它是一次性的值传递,用于创建 @Query 属性的初始状态,而不是与外部共享的状态。
因此可以在外部视图中进行动态状态的管理,而不是在UsersView内部。
init(minimumJoinDate: Date) 中的 _users 是什么?
_users 是 Swift 的一种特殊语法,用于直接访问 @Query 属性包装器的底层存储。@Query 是 Swift 提供的属性包装器,用于处理 SwiftData 的查询。正常情况下,我们通过 users 来访问 @Query 的值,而 _users 是其底层支持的实例。
这种方法是如何传递的?
1、动态设置过滤条件
在 UsersView 初始化时,传入一个参数 minimumJoinDate。
使用这个值动态构造了一个新的查询(Query 实例),并将其直接赋值给 _users。
2、传递和绑定 minimumJoinDate
ContentView 创建 UsersView 时,动态决定传入的 minimumJoinDate 是什么:
UsersView(minimumJoinDate: showingUpcomingOnly ? .now : .distantPast)
这会让 UsersView 的查询结果动态响应 minimumJoinDate 的变化,但查询本身不会自动重新生成。
为什么用 _users 而不是直接设置 users?
users 是一个由 @Query 属性包装器生成的值,它的实际行为(如查询逻辑)是由 @Query 的底层实现控制的。要更改查询逻辑,就需要通过 _users 来直接设置底层的 Query 实例。
简单比喻
如果把 @Query 比喻为一个 自动查询机器:
users 是机器产出的结果(查询的用户列表)。
_users 是机器的控制面板,允许重新定义查询的规则。
通过 init(minimumJoinDate:),我们在视图初始化时修改了机器的规则,让它根据传入的日期筛选用户。
外部显示视图
Button("Add Samples", systemImage: "plus") {
// 删除所有现有数据
try? modelContext.delete(model: User.self)
// 添加新数据
let first = User(name: "Ed Sheeran", city: "London", joinDate: .now.addingTimeInterval(86400 * -10))
let second = User(name: "Rosa Diaz", city: "New York", joinDate: .now.addingTimeInterval(86400 * -5))
let third = User(name: "Roy Kent", city: "London", joinDate: .now.addingTimeInterval(86400 * 5))
let fourth = User(name: "Johnny English", city: "London", joinDate: .now.addingTimeInterval(86400 * 10))
modelContext.insert(first)
modelContext.insert(second)
modelContext.insert(third)
modelContext.insert(fourth)
}
在外部显示视图中,新增了四条测试样例。
四条信息的时间分别对应的是当前时间的十天前(Ed Sheeran)、五天前(Rosa Diaz)、五天后(Roy Kent)和十天后(Johnny English)。
视图部分代码
struct ContentView: View {
@Environment(\.modelContext) var modelContext
@State private var showingUpcomingOnly = false
var body: some View {
NavigationStack {
UsersView(minimumJoinDate: showingUpcomingOnly ? .now : .distantPast)
.navigationTitle("Users")
.toolbar {
ToolbarItem(placement: .navigationBarLeading) {
Button(showingUpcomingOnly ? "Show Everyone" : "Show Upcoming") {
showingUpcomingOnly.toggle()
}
}
}
}
}
}
代码解析
@State private var showingUpcomingOnly = false
在视图中添加了一个变量showingUpcomingOnly,当该变量为true时,传入UsersView(minimumJoinDate:)的内容为“.now”,如果为false,则传给UsersView的内容为“.distantPast”。
传入的这两个时间变量为Date预定义的时间点:
Date.distantPast 是一个极早的时间点,通常表示「远过去」的时间。
let distantPast = Date.distantPast
print(distantPast) // "0001-12-30 00:00:00 +0000"
Date.now 是一个动态属性,返回当前系统时间。
let now = Date.now
print(now) // "2024-11-08 13:45:23 +0000"(示例时间)
因此,当传入.now时,users会返回加入时间大于当前时间的users实例。
init(minimumJoinDate: Date) {
_users = Query(filter: #Predicate<User> { user in
user.joinDate >= minimumJoinDate
}, sort: \User.name)
}
也就是前面提到的五天后(Roy Kent)和十天后(Johnny English)
如果传入的是.dastanPast,则会返回所有的users实例。
.toolbar {
ToolbarItem(placement: .navigationBarLeading) {
Button(showingUpcomingOnly ? "Show Everyone" : "Show Upcoming") {
showingUpcomingOnly.toggle()
}
}
}
这段代码则是在顶部的左侧添加了一个切换按钮,点击时会切换showingUpcomingOnly的状态同时改变对应的显示问题。
通过对该按钮的点击,可以动态的显示对应的users实例。
名称和时间排序
目前是对于加入时间字段的筛选,在这个基础之上,还可以进一步实现对于users实例的名称以及加入时间字段的排序。
动态显示视图初始化
首先是动态显示视图UsersView的初始化部分:
init(minimumJoinDate: Date,sortOrder: [SortDescriptor<User>]) {
_users = Query(filter: #Predicate<User> { user in
user.joinDate >= minimumJoinDate
}, sort: sortOrder)
}
与前面的代码相比,新增了sortOrder参数SortDescriptor<User>。
SortDescriptor<User> 是一个用于描述排序规则的类型,其中 User 是模型类型。
它表示需要对 User 类型的属性排序,例如 User.name 或 User.joinDate。
SortDescriptor(\User.name) // 按 name 排序
SortDescriptor(\User.joinDate) // 按 joinDate 排序
为什么使用 [SortDescriptor<User>]?
Query 的 sort 参数要求一个 排序描述符数组([SortDescriptor<T>]),这可以按多种规则组合排序。
let sortOrder: [SortDescriptor<User>] = [
SortDescriptor(\User.name),
SortDescriptor(\User.joinDate)
]
上述代码表示:先按 name 排序,如果 name 相同,则按 joinDate 排序。
为什么 sortOrder: [SortDescriptor] 不行?
当尝试不指定具体模型类型时:
init(minimumJoinDate: Date,sortOrder: [SortDescriptor]) {
_users = Query(filter: #Predicate<User> { user in
user.joinDate >= minimumJoinDate
}, sort: sortOrder)
}
Xcode就会报错:
Reference to generic type 'SortDescriptor' requires arguments in <...>
Insert '<Any>'
这是因为SortDescriptor 是一个泛型类型,必须指定具体的数据模型类型。
如果只写 [SortDescriptor],编译器无法推断排序规则是针对什么类型(例如 User),因此会报错。
[SortDescriptor<User>] 明确指定了排序规则是针对 User 模型。
UsersView预览视图传递一个临时的sort参数:
#Preview {
UsersView(minimumJoinDate: Date(), sortOrder: [SortDescriptor(\User.name)])
.modelContainer(for: User.self)
}
修改外部显示视图
首先定义一个默认的排序顺序:
@State private var sortOrder = [
SortDescriptor(\User.name),
SortDescriptor(\User.joinDate),
]
显示的动态视图改为:
UsersView(minimumJoinDate: showingUpcomingOnly ? .now : .distantPast, sortOrder: sortOrder)
这个代码扩充了sortOrder参数,通过传递当前视图的sortOrder进行排序。
切换排序按钮
Picker("Sort", selection: $sortOrder) {
Text("Sort by Name")
.tag([
SortDescriptor(\User.name),
SortDescriptor(\User.joinDate),
])
Text("Sort by Join Date")
.tag([
SortDescriptor(\User.joinDate),
SortDescriptor(\User.name)
])
}
这里添加了一个Picker选择器,绑定了sortOrder,当点击Picker选择器,选择“Sort by Name”时,动态显示视图会按照name字段正序排序,如果name字段一致则按照joinDate字段排序。
在 SwiftUI 中,.tag() 的主要作用是为控件中的选项附加一个标识符,用于与绑定的状态变量进行匹配。当选择一个选项时,.tag() 的值会被赋给绑定的状态变量。
在这段代码中,.tag() 为每个选项附加了一个 [SortDescriptor] 数组,并将其绑定到 @State private var sortOrder。当用户在 Picker 中选择一个选项时,相应的 tag 值会更新 sortOrder 的值。
关键点解析
1、tag 和 selection 的关系
selection 绑定到一个状态变量(这里是 $sortOrder)。
每个 Text 使用 .tag() 定义一个唯一值,当这个选项被选中时,sortOrder 会被赋值为对应的 tag。
2、tag 的值:
tag 的值可以是任何符合 Hashable 协议的类型,这里是 [SortDescriptor] 数组。
每个 tag 的值是 sortOrder 的一个可能值。
3、Picker 的作用:
当用户选择某个选项时,sortOrder 会自动更新为对应的 tag 值。
在视图刷新时,Picker 会根据 sortOrder 的当前值自动选中对应的选项。
因此,当选择picker中对应的值时,sortOrder会被picker选项的tag值赋值。从而改变对应的动态显示视图。
最后,在Picker代码外面可以考虑添加Menu视图,让代码更美观:
Menu("Sort", systemImage: "arrow.up.arrow.down") {
Picker("Sort", selection: $sortOrder) {
...
}
}
这就是通过@Query动态切换排序,通过改变外部视图的参数,让显示视图能够灵活的根据传参进行调整,并输出筛选后的实例。
完整代码
ContentView视图
import SwiftUI
import SwiftData
struct ContentView: View {
@Environment(\.modelContext) var modelContext
@State private var showingUpcomingOnly = false
@State private var sortOrder = [
SortDescriptor(\User.name),
SortDescriptor(\User.joinDate),
]
var body: some View {
NavigationStack {
Menu("Sort", systemImage: "arrow.up.arrow.down") {
Picker("Sort", selection: $sortOrder) {
Text("Sort by Name")
.tag([
SortDescriptor(\User.name),
SortDescriptor(\User.joinDate),
])
Text("Sort by Join Date")
.tag([
SortDescriptor(\User.joinDate),
SortDescriptor(\User.name)
])
}
}
UsersView(minimumJoinDate: showingUpcomingOnly ? .now : .distantPast, sortOrder: sortOrder)
.navigationTitle("Users")
.toolbar {
ToolbarItem(placement: .navigationBarLeading) {
Button(showingUpcomingOnly ? "Show Everyone" : "Show Upcoming") {
showingUpcomingOnly.toggle()
}
}
ToolbarItem(placement: .navigationBarTrailing) {
Button("Add Samples", systemImage: "plus") {
// 删除所有现有数据
try? modelContext.delete(model: User.self)
// 添加新数据
let first = User(name: "Ed Sheeran", city: "London", joinDate: .now.addingTimeInterval(86400 * -10))
let second = User(name: "Rosa Diaz", city: "New York", joinDate: .now.addingTimeInterval(86400 * -5))
let third = User(name: "Roy Kent", city: "London", joinDate: .now.addingTimeInterval(86400 * 5))
let fourth = User(name: "Johnny English", city: "London", joinDate: .now.addingTimeInterval(86400 * 10))
modelContext.insert(first)
modelContext.insert(second)
modelContext.insert(third)
modelContext.insert(fourth)
}
}
}
}
}
}
#Preview {
ContentView()
.modelContainer(for:User.self)
}
UsersView视图
import SwiftUI
import SwiftData
struct UsersView: View {
@Environment(\.modelContext) var modelContext
@Query var users: [User]
init(minimumJoinDate: Date,sortOrder: [SortDescriptor<User>]) {
_users = Query(filter: #Predicate<User> { user in
user.joinDate >= minimumJoinDate
}, sort: sortOrder)
}
var body: some View {
List(users) { user in
Text(user.name)
}
}
}
#Preview {
UsersView(minimumJoinDate: Date(), sortOrder: [SortDescriptor(\User.name)])
.modelContainer(for: User.self)
}
User视图
import Foundation
import SwiftData
@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
}
}
参考资料
Dynamically sorting and filtering @Query with SwiftUI:https://www.hackingwithswift.com/books/ios-swiftui/dynamically-sorting-and-filtering-query-with-swiftui