Swift UI利用@Query动态切换排序
Swift UI利用@Query动态切换排序

Swift UI利用@Query动态切换排序

本篇文章将通过调整@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

如果您认为这篇文章给您带来了帮助,您可以在此通过支付宝或者微信打赏网站开放者。

发表回复

您的电子邮箱地址不会被公开。 必填项已用 * 标注