SwiftData枚举字段Predicate失效

2,023 阅读2分钟

背景

这几天在使用swiftdata通过枚举属性过滤数据场景下,碰到一个很奇怪的问题,初看啥没啥问题,代码如下:

@Model
class Book: Equatable, Identifiable {
  let id = UUID()
  var bookType: BookType = BookType.STORY
  var createTime: Date
  
  init(bookType: BookType, createTime: Date) {
    self.bookType = bookType
    self.createTime = createTime
  }
}

extension Book {
  enum BookType: String, CaseIterable, Codable, Identifiable {
    var id: Self {
      return self;
    }

    case STORY = "story"
    case SCIENCE = "science"
  }
}

struct OneView: View {
  @Query private books: [Book]
  
  init(_ bookType: Book.BookType) {
    let condition = type
    let predicate = #Predicate<Book> {
        $0.bookType == condition
    }
    self._books = Query(
        filter: predicate,
        sort: \Book.createTime,
        order: .reverse,
        animation: .default
    )
  }
}

结果出现一个错误:unsupportedPredicate;而自然而然想要通过枚举的.rawValue属性来去尝试:

struct OneView: View {
  @Query private books: [Book]
  
  init(_ bookType: Book.BookType) {
    let condition = type.rawValue
    let predicate = #Predicate<Book> {
        $0.bookType.rawValue == condition
    }
    self._books = Query(
        filter: predicate,
        sort: \Book.createTime,
        order: .reverse,
        animation: .default
    )
  }
}

嗯,没有上面那个错误了,但是换了个错误:Can't find the property \Book.BookType.rawValue

查了下相关的资料,然后我找到了苹果开发者论坛里面一模一样的一个问题:

由于苹果并未开放SwiftUI的源代码,接下来将会结合一些资料以及宏展开等来探究这个问题的根因。如果有同学对这一块不感兴趣,我先直接列出对应的临时解决方案:

  1. 在model定义内新增加一个_bookType,然后增加bookType非持久化字段,以利用_bookType来获取和设置bookType这个枚举值
@Model
class Book {
  let id = UUID()
  @Transient var bookType: BookType {
    get { Book.BookType(rawValue: _bookType) }
    set { _bookType = newValue.rawValue }
  }
  var _bookType: String
  var createTime: Date
}

2. 在View页面使用_bookType字段来进行条件过滤:

struct OneView: View {
  @Query private books: [Book]
  
  init(_ bookType: Book.BookType) {
    let condition = type.rawRype
    let predicate = #Predicate<Book> {
        $0._bookType == condition
    }
    self._books = Query(
        filter: predicate,
        sort: \Book.createTime,
        order: .reverse,
        animation: .default
    )
  }
}

通过上述的变更后,根据枚举属性过滤数据就没有问题了

原因

在有了上述这个解决方案后,我们也该探究一下为什么会出现这个问题。

由于SwiftUI是闭源代码,无法从源码层面直接分析其查询的逻辑,但是我们可以通过宏展开来做一定的判断。(PS:@Query无法做宏展开,因此,这里将会从#Predicate来间接判断其处理过程)

无法使用枚举等值比较的原因

let predicate = #Predicate<Book> {
    $0.bookType == condition
}

上述代码报错Unsupported Predicate,这个其实也没啥好分析的了,就是枚举比较在Predicate内无法被支持

无法使用枚举.rawValue做比较的原因

let predicate = #Predicate<Book> {
    $0.bookType.rawValue == condition
}

上述代码报错

原因分析

我们将#Predicate宏展开后的代码如下:

Foundation.Predicate<Book>({
    PredicateExpressions.build_Equal(
        lhs: PredicateExpressions.build_KeyPath(
            root: PredicateExpressions.build_KeyPath(
                root: PredicateExpressions.build_Arg($0),
                keyPath: .bookType
            ),
            keyPath: .rawValue
        ),
        rhs: PredicateExpressions.build_Arg(condition)
    )
})

可以看到,宏展开后,predicate表达式基本分为两个部分:lhs和rhs;即:左表达式和有表达式。

左边构建查询key路径,右边构建实参。

从左边构建的查询key路径也可看出,我们的$0.bookType.rawValue被分成了两个部分:一个是root,即:.bookType,另一个是keyPath,即:.rawValue。

而book的model是没有这个.rawValue的keyPath的,这个可以将book的@model做一个宏展开,里面的schemaMetadata属性上,会注册这个model所有的属性即路径:

static var schemaMetadata: [SwiftData.Schema.PropertyMetadata] {
  return [
    SwiftData.Schema.PropertyMetadata(name: "id", keypath: \Book.id, defaultValue: UUID(), metadata: nil),
    SwiftData.Schema.PropertyMetadata(name: "bookType", keypath: \Book.bookType, defaultValue: BookType.STORY, metadata: nil),
    SwiftData.Schema.PropertyMetadata(name: "createTime", keypath: \Book.createTime, defaultValue: nil, metadata: nil)
  ]
}

显而易见,这也是为什么会报错Can't find the property \Book.BookType.rawValue的原因了。

小结

目前看来swiftui以及swiftdata在功能完备性上还是有一些小瑕疵的,后续有空的时候,将会进一步尝试下:更改schemaMetadata,注册rawKey到keypath是否可行。