SwiftData — @Query

SwiftData(Apple 最新的数据持久化框架)中,可以通过 Filtering(过滤)和 Sorting(排序)对数据进行查询和组织。这些操作在 SwiftData 的 @Query 属性包装器中得到了优雅且易用的处理。

以下我将通过一些常用的过滤和排序操作演示如何使用 SwiftData 有效地管理数据。


前置理解 #

  1. SwiftData 的核心组件:

    • Model: 数据模型使用 @Model 注解来定义。
    • @Query: 一个强大的属性包装器,用于通过 SwiftData 管理和查询数据。
    • Predicate 和 SortDescriptor: 在过滤和排序中,这些对象用于定义查询条件和排序规则。
  2. 过滤和排序的目标:

    • Filtering:通过条件限制查询结果,例如查找指定属性值、范围或符合谓词的数据。
    • Sorting:以特定的顺序排序查询结果,例如升序、降序或自定义比较逻辑。

1. 定义 @Model 数据模型 #

在 SwiftData 中,所有数据模型都需要用 @Model 注解定义。以下我们定义一个简单的任务模型:

import SwiftData

@Model
class Task {
    var id: UUID
    var title: String
    var isCompleted: Bool
    var dueDate: Date

    init(title: String, isCompleted: Bool, dueDate: Date) {
        self.id = UUID()
        self.title = title
        self.isCompleted = isCompleted
        self.dueDate = dueDate
    }
}

说明:

  • Task 模型代表一个任务,包含以下属性:
    • id: 唯一标识符。
    • title: 任务标题。
    • isCompleted: 是否已完成。
    • dueDate: 任务截止日期。

之后我们会对这些任务数据进行 过滤排序


2. Filtering 的例子 #

例子 1: 基于布尔属性的过滤 #

从数据中筛选出所有未完成的任务:

@Query(filter: #Predicate { $0.isCompleted == false }) 
private var pendingTasks: [Task]
  • 用法:
    • 使用 #Predicate 构建过滤条件。
    • $0.isCompleted == false 表示过滤出 isCompleted 属性为 false 的数据。

例子 2: 按字符串部分匹配过滤(模糊搜索) #

筛选出任务标题包含关键字 “important” 的任务:

@Query(filter: #Predicate { $0.title.contains("important") })
private var importantTasks: [Task]
  • 说明
    • 使用字符串方法 .contains() 进行部分匹配。
    • 确保模糊搜索对大小写敏感。如果需要大小写不敏感,请使用 .localizedCaseInsensitiveContains()

例子 3: 基于时间范围过滤 #

筛选出未来 7 天到期的任务:

@Query(filter: #Predicate {
    let nextWeek = Calendar.current.date(byAdding: .day, value: 7, to: Date())
    return $0.dueDate <= nextWeek && $0.dueDate >= Date()
})
private var tasksDueNextWeek: [Task]
  • 说明:
    • 使用 CalendarDate 计算未来 7 天的日期范围。
    • 通过日期比较 <=>= 过滤符合条件的任务。

例子 4: 组合多个条件 #

筛选出未来任务(未完成任务且在一周内到期):

@Query(filter: #Predicate {
    let nextWeek = Calendar.current.date(byAdding: .day, value: 7, to: Date())
    return $0.isCompleted == false && $0.dueDate <= nextWeek
})
private var futureTasks: [Task]
  • 组合条件:通过 &&|| 实现多个条件的组合。

例子 5: 动态过滤 #

可以根据用户输入动态调整条件,比如通过任务标题关键字搜索:

struct SearchView: View {
    @State private var searchText = ""
    
    @Query var filteredTasks: [Task]
    
    var body: some View {
        VStack {
            TextField("Search tasks", text: $searchText)
                .onChange(of: searchText) { text in
                    $filteredTasks = Query(Task.self, filter: #Predicate { 
                        $0.title.contains(text)
                    })
                }
            
            List(filteredTasks) { task in
                Text(task.title)
            }
        }
    }
}
  • 使用动态条件
    • 动态更新 @Queryfilter
    • 基于用户输入的 searchText 创建过滤条件。

3. Sorting 的例子 #

例子 1: 基于单个属性排序 #

将任务按照截止日期升序排列:

@Query(sort: #SortDescriptor(\.dueDate)) 
private var tasksSortedByDueDate: [Task]
  • 说明:
    • 使用 SortDescriptor 指定排序规则。
    • \.dueDate 表示对 dueDate 属性进行排序。

例子 2: 降序排列 #

将任务按截止日期降序排列:

@Query(sort: #SortDescriptor(\.dueDate, order: .reverse)) 
private var recentTasks: [Task]
  • 用法:
    • #SortDescriptor 中设置 order: .reverse,将排序规则切换为降序。

例子 3: 多重排序 #

如果需要按多个属性排序,可以使用多个 SortDescriptor

@Query(sort: [
    #SortDescriptor(\.isCompleted), // 未完成的任务优先
    #SortDescriptor(\.dueDate)     // 截止日期升序排列
])
private var prioritizedTasks: [Task]
  • 说明:
    • 第一排序规则:isCompleted,优先排列未完成任务。
    • 第二排序规则:dueDate,相同完成状态时按截止日期升序排列。

例子 4: 动态排序 #

结合用户选择,动态切换不同的排序规则:

struct TasksView: View {
    @State private var sortOption: String = "Due Date"
    
    @Query var sortedTasks: [Task]
    
    var body: some View {
        Picker("Sort by", selection: $sortOption) {
            Text("Due Date").tag("Due Date")
            Text("Title").tag("Title")
        }
        .pickerStyle(SegmentedPickerStyle())
        .onChange(of: sortOption) { option in
            $sortedTasks = Query(
                Task.self,
                sort: option == "Due Date" ? #SortDescriptor(\.dueDate) : #SortDescriptor(\.title)
            )
        }
        
        List(sortedTasks) { task in
            Text(task.title)
        }
    }
}
  • 说明:
    • 动态更新排序规则。
    • 使用用户选择控制 @Query 的排序方式。

4. Filtering 和 Sorting 结合使用 #

多条件过滤与排序可以联合使用。例如,筛选未完成的任务并按截止日期升序排列:

@Query(
    filter: #Predicate { $0.isCompleted == false },
    sort: #SortDescriptor(\.dueDate)
)
private var prioritizedIncompleteTasks: [Task]

5. 自定义方式通过代码查询 #

在某些特定情况下,你可能需要通过编程方式(非属性包装器)直接查询和使用过滤与排序。

func fetchIncompleteTasks(in context: ModelContext) -> [Task] {
    do {
        let results = try context.fetch(Query(
            filter: #Predicate { $0.isCompleted == false },
            sort: #SortDescriptor(\.dueDate)
        ))
        return results
    } catch {
        print("Error fetching tasks: \(error)")
        return []
    }
}

总结 #

  • SwiftData 使用 @Query 简化了常见的 FilteringSorting 操作。
  • 过滤(Filtering) 是通过 #Predicate 定义条件实现,比如部分匹配或日期范围。
  • 排序(Sorting) 是通过 #SortDescriptor 定义规则控制顺序,可支持单一或多重排序。
  • 结合动态过滤和排序,开发者可以实现更灵活的、用户可控的数据查询功能。

通过这些示例,您可以轻松构建复杂的业务逻辑。

在 SwiftUI 中,@Query 属性包装器与 FetchDescriptor 的结合使用,是 SwiftData 框架下实现数据驱动 UI 的核心模式。以下是具体实践和原理分析:


场景 #

动态更新 @Query 的查询条件 #

与 FetchDescriptor #

一、基础概念与作用 #

  1. @Query 的作用
    @Query 是 SwiftData 提供的属性包装器,自动管理数据查询与界面更新。当底层数据变化时,其修饰的数组(如 [Item])会自动触发关联视图的刷新。

  2. FetchDescriptor 的功能
    FetchDescriptor 用于定义数据查询的条件,包括排序规则、过滤条件(Predicate)、分页设置等,类似于 Core Data 的 NSFetchRequest


二、结合使用步骤与代码示例 #

步骤 1:声明 @Query 属性 #

在视图中通过 @Query 声明需要观察的数据集合,并直接绑定 FetchDescriptor

struct EventListView: View {
    // 使用 @Query 绑定 FetchDescriptor
    @Query(FetchDescriptor<Item>(sortBy: [SortDescriptor(\.timestamp)])) 
    private var items: [Item]
    
    var body: some View {
        List(items) { item in
            Text(item.timestamp.formatted())
        }
    }
}

说明:此处 FetchDescriptor 定义了按 timestamp 排序的查询条件,@Query 自动执行查询并监听数据变化。

步骤 2:动态修改查询条件 #

通过视图初始化参数传递动态 FetchDescriptor

struct FilteredListView: View {
    let filterKeyword: String
    // 动态生成 FetchDescriptor
    private var descriptor: FetchDescriptor<Item> {
        let predicate = #Predicate<Item> { $0.title.contains(filterKeyword) }
        return FetchDescriptor(predicate: predicate)
    }
    
    @Query(descriptor) 
    private var filteredItems: [Item]
    
    var body: some View { /* ... */ }
}

说明:通过计算属性动态生成包含关键词过滤条件的 FetchDescriptor,实现动态数据筛选。


三、高级用法与最佳实践 #

1. 跨视图共享查询逻辑 #

FetchDescriptor 封装为静态属性以复用:

extension FetchDescriptor where T == Item {
    static var recentItems: FetchDescriptor<Item> {
        let predicate = #Predicate<Item> { $0.timestamp > Date().addingTimeInterval(-86400) }
        return FetchDescriptor(predicate: predicate, sortBy: [SortDescriptor(\.timestamp)])
    }
}

// 在视图中引用
@Query(.recentItems) private var recentItems: [Item]

2. 结合 @Environment 注入动态参数 #

通过环境变量控制查询条件:

struct UserScopeView: View {
    @Environment(\.userID) private var userID
    
    private var descriptor: FetchDescriptor<Item> {
        let predicate = #Predicate<Item> { $0.ownerID == userID }
        return FetchDescriptor(predicate: predicate)
    }
    
    @Query(descriptor) private var userItems: [Item]
    
    var body: some View { /* ... */ }
}

3. 性能优化:分页与批量加载 #

// 定义分页 FetchDescriptor
var pagedDescriptor: FetchDescriptor<Item> {
    var descriptor = FetchDescriptor<Item>(sortBy: [SortDescriptor(\.timestamp)])
    descriptor.fetchLimit = 20 // 每页 20 条
    descriptor.fetchOffset = currentPage * 20
    return descriptor
}

@Query(pagedDescriptor) private var pagedItems: [Item]

四、常见问题与调试 #

  1. 谓词(Predicate)语法验证
    SwiftData 的 #Predicate 宏在编译时检查语法,若条件表达式不合法会直接触发编译错误。

  2. 数据更新延迟问题
    若手动修改 modelContext 后视图未刷新,需确认是否在主线程执行修改:

    DispatchQueue.main.async {
        modelContext.insert(newItem)
    }
    
  3. 调试查询条件
    打印 FetchDescriptor 的 SQL 语句(需启用 SwiftData 调试模式):

    print(descriptor.debugDescription)
    

五、与 @StateObject 的对比 #

特性@Query + FetchDescriptor@StateObject + 手动查询
数据绑定自动同步 SwiftData 变更需手动调用 fetch 方法
适用场景直接依赖 SwiftData 的标准化查询复杂数据聚合或跨模型操作
性能内置优化,适合列表渲染需自行实现缓存和批量加载逻辑

总结@QueryFetchDescriptor 的结合是 SwiftData 在 SwiftUI 中的标志性设计,通过声明式语法实现高效的数据-UI 同步。开发者应优先使用此模式处理标准化数据查询,仅在需要复杂逻辑时回退到手动管理。

本文共 2580 字,创建于 Dec 30, 2024
相关标签: Xcode