在 SwiftData 中使用枚举进行谓词查询

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

在 SwiftData 中使用枚举进行谓词查询

2025 年 3 月 10 日 • 5 分钟阅读

在 Swift 开发中,枚举是组织相关状态或类型数据的理想选择。例如,在一个书影音收藏应用中,我们可能会使用枚举来表示收藏项的类型(书籍、电影、音乐)。

当我尝试在 SwiftData 中直接使用枚举进行数据过滤时,会遇到以下问题:

  1. 如果在内存中对检索到的所有数据进行过滤,会导致主线程卡顿,影响用户体验。这个问题会随着数据量的增加越发明显。
  2. 尝试在 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,而不是直接存储枚举类型。这种方法需要在模型中创建两个变量:

  1. 一个存储属性,用于存储 rawValue 的值(使用基本类型如 Int、String)
  2. 一个计算属性,用于获取和设置枚举值。
  3. 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 的功能,使谓词查询支持直接使用枚举类型。