在 SwiftData 模型中使用 Codable 和枚举的注意事项

发表于

为您每周带来有关 Swift 和 SwiftUI 的精选资讯!

相较于 Core Data,SwiftData 在数据模型的构建方式上实现了根本性的革新。它不仅支持纯代码的声明方式,还允许在模型中直接使用符合 Codable 协议的类型及枚举类型,这些都是其显著的新特性。许多开发者都倾向于利用这些新功能,因为它们似乎非常契合 Swift 语言的声明风格。然而,若对这些新功能的实现细节和潜在限制理解不足,开发者可能会在未来遇到不少问题。本文旨在探讨在 SwiftData 模型中使用 Codable 和枚举时需要注意的几个关键点,帮助开发者避免走入误区。

Codable 的持久化策略

在 Core Data 中,当开发者需要在模型中使用 SQLite 不支持的复杂数据类型时,通常会借助 Value Transformer 机制来实现。通过创建一个 NSSecureUnarchiveFromDataTransformer 的子类,可以将数据转换为自定义的底层格式并进行持久化存储。在此过程中,Core Data 会自动调用开发者定义的 Transformer 进行数据的读写转换。然而,这种方法基于 NSObject,并不完全符合 Swift 的编程风格。

与之相对,SwiftData 提供了一种更贴合 Swift 语言特性的解决方案。开发者可以直接将符合 Codable 协议的类型用作模型属性,如下示例所示:

Swift
struct People: Codable {
  var name: String
  var age: Int
}

@Model
final class Todo {
  var title: String
  var people: People
  init(title: String, people: People) {
    self.title = title
    self.people = people
  }
}

在 SwiftData 的默认存储实现中,持久化 people 属性的方式并不是将数据通过如 JSONEncoder 之类的编解码器转换为二进制格式并存储在单一字段中(类似于 Core Data 的 Value Transformer)。相反,SwiftData 会在实体对应的表中为 Codable 数据的各个属性创建独立字段,分别进行存储( 可以理解为转换成了 Core Data 中的 Composite attributes )。

下图展示了上述代码的底层存储格式:

image-20240810182222975

推荐阅读 WWDC 2023 Core Data 有哪些新变化 了解更多关于复合属性的详细信息。

因此,在 SwiftData 的数据模型中使用非基础类型的 Codable 类型时,Codable 协议更多地起到了一种标识作用,指示 SwiftData 应将该类型解析为复合属性,而非通过直接编解码来处理。

使用 Codable 类型属性作为查询谓词

SwiftData 中将 Codable 类型转换为复合属性而不进行完整的编解码,其主要优势之一是能够直接使用 Codable 类型的属性作为查询谓词。以下以 People 类型为例,演示如何在查询中利用这一特性:

Swift
let predicate = #Predicate<Todo>{
  $0.people.name == "fat" && $0.people.age == 18
}

这种处理方式的显著优势在于,它允许开发者利用 Codable 属性进行高效查询,同时避免了将数据结构定义为复杂的关系对象。因此,在 SwiftData 中,这种非整体编解码的策略显著提升了数据查询的灵活性和效率。

推荐在 Codable 中使用基础类型

SwiftData 对 Codable 类型的处理并不涉及传统的编解码过程,因此,并不是所有复杂的 Codable 类型都适合用于 SwiftData 模型。例如,以下自定义类型可能会导致问题:

Swift
struct CodableColor: Codable, Hashable {
  var color: UIColor

  enum CodingKeys: String, CodingKey {
    case red
    case green
    case blue
    case alpha
  }

  init(color: UIColor) {
    self.color = color
  }

  init(from decoder: Decoder) throws {
    let container = try decoder.container(keyedBy: CodingKeys.self)

    let red = try container.decode(CGFloat.self, forKey: .red)
    let green = try container.decode(CGFloat.self, forKey: .green)
    let blue = try container.decode(CGFloat.self, forKey: .blue)
    let alpha = try container.decode(CGFloat.self, forKey: .alpha)

    color = UIColor(red: red, green: green, blue: blue, alpha: alpha)
  }

  func encode(to encoder: Encoder) throws {
    var container = encoder.container(keyedBy: CodingKeys.self)

    var red: CGFloat = 0
    var green: CGFloat = 0
    var blue: CGFloat = 0
    var alpha: CGFloat = 0

    color.getRed(&red, green: &green, blue: &blue, alpha: &alpha)

    try container.encode(red, forKey: .red)
    try container.encode(green, forKey: .green)
    try container.encode(blue, forKey: .blue)
    try container.encode(alpha, forKey: .alpha)
  }
}

在 SwiftData 模型中使用上述类型可能导致错误:

Swift
@Model
final class A {
  var color:CodableColor = CodableColor(color: .red)
  init(color:CodableColor) {
    self.color = color
  }
}

SwiftData/SchemaProperty.swift:381: Fatal error: Unexpected property within Persisted Struct/Enum: UIColor

此类错误表明,并非所有符合 Codable 协议的复杂类型都适用于 SwiftData 模型。尽管某些复杂的符合 Codable 协议的类型可以在模型中使用( 能正常编译 ),但实际使用中可能会遇到不一致的行为和异常( 不少开发者有这方面的反馈,没有明显出现异常的规律 )。

因此,建议在 SwiftData 模型中优先使用由基础类型构成的简单 Codable 类型,这将有助于避免兼容性问题,并确保模型的稳定性和可维护性。

调整 Codable 类型属性对轻量迁移的影响

虽然使用 Codable 提供了诸多便利,但是我们必须注意到其一些显著的限制和潜在弊端。

由于 Codable 类型的非完全编解码特性,对其属性进行增减或重命名会干扰 SwiftData 的轻量级数据迁移机制。特别是当应用启用 SwiftData 的内置云同步功能时,这类修改可能不会符合云端同步的规则,导致同步失败。

因此,在使用同步功能时,开发者需要谨慎处理 Codable 类型的使用。除非可以确保这些类型属性未来不需修改,否则建议避免在这些场合使用 Codable 类型,以保证数据结构的稳定性和同步的可靠性。

在模型中使用 Codable 数组

当在 SwiftData 模型中使用 Codable 数组时,其数据存储方式与单个 Codable 对象有所不同。

Swift
struct People: Codable {
  var name: String
  var age: Int
}

@Model
final class Todo {
  var title: String
  var peoples: [People]? = []
  init(title: String, peoples: [People]) {
    self.title = title
    self.peoples = peoples
  }
}

在这种情况下,底层存储并不是将数组项转换为复合属性,而是采用了更为直接的方法:将 [People] 数组编码成二进制数据进行保存。

image-20240810190606077

这表示在数组数据形式下,即使对 Codable 类型进行一些调整,例如在 People 结构中添加新属性,也不会影响模型的云同步兼容性。如下所示,给 People 增加一个 description 属性并不会改变其作为 BLOB 类型的存储方式:

Swift
struct People: Codable {
  var name: String
  var age: Int
  var description: String?
}

因此,数组中的 Codable 类型变更在 SwiftData 中处理得较为灵活,不会对数据结构的云同步功能造成负面影响。

虽然编解码的数组能够保证数据的顺序性,但其读写性能和灵活性通常不如对多关系的实现方式。因此,选择是否采用这种模式应根据具体的应用需求来决定。

模型中枚举类型的持久化方式

在 SwiftData 中,枚举类型的持久化相较于普通的复合 Codable 类型来说,其底层数据格式要复杂得多。

考虑以下代码示例:

Swift
enum MyType: Codable {
  case one, two, three
}

@Model
final class NewModel {
  var type: MyType
  init(type: MyType) {
    self.type = type
  }
}

该代码片段对应的底层存储展示如下:

image-20240810192504325

如果我们将 MyTyperawValue 类型设置为 Int,底层的存储结构会相应改变:

Swift
enum MyType: Int, Codable {
  case one, two, three
}

image-20240810192730034

若设置为 String 类型的 rawValue,底层存储结构将再次改变:

Swift
enum MyType: String, Codable { // rawValue: String
  case one, two, three
}

image-20240810192855737

这些示例表明,虽然枚举项本身没有变化,仅仅是修改了 rawValue 的类型,也会导致模型的底层存储结构发生显著变化。因此,开发者在设计枚举时应充分考虑到未来的需求,以避免在后续开发中需要对枚举类型做出调整。

枚举类型不能直接作为查询谓词

尽管将枚举类型直接用作模型属性极为便利,但截至 iOS 18,SwiftData 仍不支持使用枚举类型作为查询谓词。例如,以下代码将无法正常执行:

Swift
let predicate = #Predicate<NewModel>{
  $0.type == .one
}

因此,除非确信枚举类型将来不会用作查询条件,否则不推荐将其直接用于持久化存储。

在需要对枚举类型进行查询的场景中,建议采用类似 Core Data 的传统方法,即存储枚举的 rawValue 并使用它作为查询条件:

Swift
@Model
public final class NewModel {
  public var type: MyType {
    get { MyType(rawValue: typeRaw) ?? .one }
    set { typeRaw = newValue.rawValue }
  }

  private var typeRaw: MyType.RawValue = MyType.one.rawValue
  public init(type: MyType) {
    self.type = type
  }

  static func typeFilter(type: MyType) -> Predicate<NewModel> {
    let rawValue = type.rawValue
    return #Predicate<NewModel> {
      $0.typeRaw == rawValue
    }
  }
}

@Query(filter: NewModel.typeFilter(type: .two)) var models: [NewModel]

这种方法允许开发者利用枚举的易用性,同时确保查询功能的可用性和效率。

总结与展望

尽管存在一些限制,SwiftData 在模型中直接支持 Codable 和枚举类型无疑是一个显著的进步,极大地增强了数据模型的表达力和灵活性。期待在未来的版本更新中,SwiftData 能够引入枚举类型作为有效的查询条件,这将进一步提升其实用性。

此外,针对 Codable 类型的处理,希望 SwiftData 能够提供更灵活的编解码选项,允许开发者根据具体需求选择适合的数据处理策略。特别是在面对需要轻量迁移的场景时,能够自定义处理方式将极大地提高数据模型的适应性和稳定性。

每周一晚,与全球开发者同步,掌握 Swift & SwiftUI 最新动向
可随时退订,干净无 spam