在 SwiftData 中使用枚举进行谓词查询
了解如何在 SwiftData 的查询谓词中使用枚举属性。

2025 年 3 月 10 日 • 5 分钟阅读
在 Swift 开发中,枚举是组织相关状态或类型数据的理想选择。例如,在一个书影音收藏应用中,我们可能会使用枚举来表示收藏项的类型(书籍、电影、音乐)。
当我尝试在 SwiftData 中直接使用枚举进行数据过滤时,会遇到以下问题:
- 如果在内存中对检索到的所有数据进行过滤,会导致主线程卡顿,影响用户体验。这个问题会随着数据量的增加越发明显。
- 尝试在
Predicate
中直接使用枚举类型进行查询会导致编译错误或运行时崩溃。
无法直接使用枚举的困境
截至 iOS 18,SwiftData 的 Predicate
查询谓词中仍然不支持直接使用枚举类型。例如,以下代码将无法通过编译:
let predicate = #Predicate<BookmarkItem> {
$0.bookmarkType == .movie
}
即使尝试通过创建 rawValue 来进行查询,虽然可以解决编译错误,但在运行时应用仍会崩溃:
let rawValue = BookmarkType.movie.rawValue
let predicate = #Predicate<BookmarkItem> { bookmark in
bookmark.bookmarkType.rawValue == rawValue
}
可行的解决方案
目前发现的最佳实践,是在创建 Model 时存储枚举的 RawValue,而不是直接存储枚举类型。这种方法需要在模型中创建两个变量:
- 一个存储属性,用于存储 rawValue 的值(使用基本类型如 Int、String)
- 一个计算属性,用于获取和设置枚举值。
- 在
init
初始化器中,通过self.type = bookmarkType.rawValue
的形式存储原始值。
下面是一个完整的实现示例:
@Model
class BookmarkItem: Identifiable {
var name: String
var type: Int
var bookmarkType: BookmarkType {
BookmarkType(rawValue: type) ?? .movie
}
init(name: String, bookmarkType: BookmarkType) {
self.name = name
self.type = bookmarkType.rawValue
}
}
enum BookmarkType: Int, Codable, CaseIterable, Identifiable {
case book // 自动为 0
case movie // 自动为 1
case music // 自动为 2
var id: Self { self }
var title: String {
switch self {
case .book:
return "书籍"
case .movie:
return "电影"
case .music:
return "音乐"
}
}
}
在这个示例中,BookmarkItem
模型使用 type
属性(Int 类型)来存储枚举的原始值,同时提供 bookmarkType
计算属性来获取实际的枚举值。
枚举原始值的选择与自动分配
在 Swift 中,当枚举声明为具有 Int 原始值类型但没有显式指定每个 case 的原始值时,Swift 会自动为每个 case 分配原始值,从 0 开始递增:
enum SimplifiedType: Int {
case book, movie, music
// book 的 rawValue 是 0
// movie 的 rawValue 是 1
// music 的 rawValue 是 2
}
对于不同类型的原始值:
- Int 类型:如果不显式指定原始值,第一个 case 的原始值为 0,之后每个 case 的原始值依次递增 1
- String 类型:如果不显式指定原始值,每个 case 的原始值默认为 case 名称的字符串
对于类似 BookmarkType 这类分类枚举,Int 类型通常是更常见的选择,因为:
- 整数比较和存储通常比字符串更高效
- 它们通常用于内部状态或类型标识
- 数据库存储和检索时整数更高效
在谓词查询中使用存储属性
使用这种设计模式时,在谓词查询中使用存储属性的具体示例:
// BookmarkItemResults.swift
struct BookmarkItemResults: View {
@Query private var items: [BookmarkItem]
init(bookmarkType: BookmarkType) {
// 使用存储属性 type 进行查询,而不是枚举值
let typeValue = bookmarkType.rawValue
_items = Query(filter: #Predicate<BookmarkItem> { item in
item.type == typeValue
})
}
var body: some View {
List(items) { item in
VStack(alignment: .leading) {
Text(item.name)
.font(.headline)
Text(item.bookmarkType.title)
.font(.subheadline)
.foregroundColor(.secondary)
}
}
}
}
// ContentView.swift
struct ContentView: View {
@State private var selectedBookmarkType: BookmarkType = .book
var body: some View {
VStack {
Picker("选择收藏类型", selection: $selectedBookmarkType) {
ForEach(BookmarkType.allCases) { bookmarkType in
Text(bookmarkType.title)
.tag(bookmarkType)
}
}
.pickerStyle(.segmented)
BookmarkItemResults(bookmarkType: selectedBookmarkType)
.padding()
}
}
}
创建了一个 BookmarkItemResults 视图:
- 使用 @Query 属性包装器获取过滤后的数据
- 在初始化时接受 BookmarkType 参数
- 在查询谓词中使用 type(存储属性)而不是 bookmarkType(计算属性)
- 在显示时使用计算属性 bookmarkType.title 来展示友好的类型名称
在 ContentView 中:
- 使用 @State 管理选中的类型
- 使用 Picker 让用户选择不同的收藏类型
- 将选中的类型传递给 BookmarkItemResults 视图
这种解决方案虽然有效,但仍然是一种变通方法,它引入了额外的转换逻辑和样板代码。希望在 iOS 19 或未来的更新中,Apple 能够增强 SwiftData 的功能,使谓词查询支持直接使用枚举类型。