在 SwiftData 的数项改进中,用纯代码声明数据模型无疑给 Core Data 开发者留下了深刻印象。本文将深化探讨 SwiftData 是怎么经过代码创立数据模型的,运用了哪些新的言语特性,并展示了怎么经过声明代码来创立 PersistentModel 实例。

原文宣布在我的博客wwww.fatbobman.com 。 由于技术文章需要不断的迭代,当时耗费了不少的精力在不同的平台之间来保持文章的更新。故从 2024 年起,新的文章将只发布在我的博客上。

三个现实

了解下述三个现实对于更好地掌握和理解 SwiftData 的建模原理以及为什么 SwiftData 会选用本文介绍的这些办法十分有帮助。

SwiftData 是建立在 Core Data 之上的结构

尽管苹果很少强调 SwiftData 与 Core Data 之间的联系,但 SwiftData 结构建立在 Core Data 根底之上这一点仍是无可否认的现实。根据 Core Data 为 SwiftData 带来了几点优点:

  • 数据库文件格局兼容,现有数据能够直接用新结构操作
  • 继承了 Core Data 已有的稳定性验证,大幅减少潜在问题。

尽管 SwiftData 是以 Core Data 为根底的,但这并不意味着,在运用 SwiftData 进行开发时,仍需选用与 Core Data 一样的编程原则。由于 SwiftData 结合了众多 Swift 言语的最新特性,因此,在许多场合下,开发者需要用全新的思维来重新设计数据处理逻辑。

在 SwiftDataKit:让你在 SwiftData 中运用 Core Data 的高级功用 一文中,我介绍了怎么调用 SwiftData 元素背后对应的 Core Data 目标的技巧。

SwiftData 与 Swift 言语严密关联,是 Swift 言语的先导者

近年来,苹果推出了多个以 Swift 为前缀的结构,例如 SwiftUI、Swift Charts、SwiftData 等。这种命名方式表现了这些结构与 Swift 言语的严密结合。为了完结这些结构,苹果还积极推进 Swift 言语的发展,提出新的提案,并在结构中预先应用了尚未完全确认的特性。这些结构广泛选用了 Swift 的新功用,例如结构结构器(Result Builder)、特点包装器(Property Wrapper)、宏(Macro)和初始化拜访器(Init Accessors)等,使其成为了新言语特性的前驱和试验场。

遗憾的是,些结构现在尚不存在跨平台和开源的可能。首要是由于它们依靠了苹果生态中的专有 API。这阻止了运用这些优秀结构在其他平台上推广 Swift 言语的时机。

总的来说,SwiftData 等结构与 Swift 言语联系密切,并在选用新特性方面起到了引领效果。学习这些结构的一起也是在掌握 Swift 言语的新特性。

纯代码声明数据模型相对 Core Data 是一项前进但并非革命

尽管 SwiftData 选用的是纯代码声明数据模型的形式,给 Core Data 开发者带来了惊喜,但这在其他结构和言语中早已被应用。相较于 Core Data,它有所前进,但不能算得上是完全的革新。

但是,SwiftData 在完结这个概念上有其独特的创新之处。这首要得益于与 Swift 言语的严密结合。经过创立并运用新出现的言语特性,SwiftData 以更简洁高效并契合现代编程思维的方式完结了声明式建模。

模型代码解析

在本节中,咱们将对 SwiftData 的模型代码进行分析,这些代码是以 Xcode 供给的 SwiftData 项目模板中的模型为根底,让咱们揭开它神秘的面纱

@Model
final class Item {
    var timestamp: Date = Date.now // 添加了默许值
    init(timestamp: Date) {
        self.timestamp = timestamp
    }
}

宏( Macro)的效果

假如不考虑宏标志 @Model,上面的代码与咱们界说一个标准的 Swift 类完全一样。而 SwiftData 经过 @Model 宏,根据咱们供给的简略表述,将其扩展为一个具备完好描绘的数据模型。

在 Xcode 中打开宏,咱们将能够看到经过宏扩展后的完好代码(@_PersistedProperty 能够打开两次)。

揭秘 SwiftData 的数据建模原理

打开后完好的代码如下:

public final class Item {
    // 用户界说的耐久化特点
    public var timestamp: Date = Date.now {
        // 结构器拜访器,在结构实例的过程中,为核算特点添加结构才干
        @storageRestrictions(accesses: _$backingData, initializes: _timestamp)
        init(initialValue) {
            _$backingData.setValue(forKey: \.timestamp, to: initialValue)
            _timestamp = _SwiftDataNoType()
        }
        get {
            _$observationRegistrar.access(self, keyPath: \.timestamp)
            return self.getValue(forKey: \.timestamp)
        }
        set {
            _$observationRegistrar.withMutation(of: self, keyPath: \.timestamp) {
                self.setValue(forKey: \.timestamp, to: newValue)
            }
        }
    }
    // timestamp 对应的下划线版别,暂时未发现有实践用途
    @Transient
    private var _timestamp: _SwiftDataNoType = .init()
    // 用户自界说的结构器
    public init(timestamp: Date) {
        self.timestamp = timestamp
    }
    // 一个用来包装对应的保管目标( NSManagedObject )实例的类型,无需耐久化( @Transient )
    @Transient
    private var _$backingData: any SwiftData.BackingData<Item> = Item.createBackingData()
    public var persistentBackingData: any BackingData<Item> {
        get {
            self._$backingData
        }
        set {
            self._$backingData = newValue
        }
    }
    // 为创立 Scheme 供给模型的元数据
    public static var schemaMetadata: [Schema.PropertyMetadata] {
        return [
            SwiftData.Schema.PropertyMetadata(name: "timestamp", keypath: \Item.timestamp, defaultValue: Date.now, metadata: nil),
        ]
    }
    // 从 backingData 结构 PersistentModel
    public init(backingData: any BackingData<Item>) {
        _timestamp = _SwiftDataNoType()
        self.persistentBackingData = backingData
    }
    // Observation 协议要求的调查注册器
    @Transient
    private let _$observationRegistrar: ObservationRegistrar = Observation.ObservationRegistrar()
    // 空类型,用于下划线版别的特点
    struct _SwiftDataNoType {}
}
// 恪守 PersistentModel 协议
extension Item: SwiftData.PersistentModel {}
// 恪守 Observable 协议
extension Item: Observation.Observable {}

下文将详细描绘生成的代码的细节。

模型元数据

在 Core Data 中,开发者能够经过 Xcode 供给的数据模型编辑器生成 XML 格局的 .xcdatamodeld 文件。这个文件保存了用于创立数据模型(NSManagedObjectModel)的描绘信息。

阅览 CoreData 探秘 – 从数据模型构建到保管目标实例 一文,了解更多信息。

SwiftData 则经过 Model 宏,直接将上述描绘信息集成在了声明代码的内部。

public static var schemaMetadata: [Schema.PropertyMetadata] {
    return [
        SwiftData.Schema.PropertyMetadata(name: "timestamp", keypath: \Item.timestamp, defaultValue: Date.now, metadata: nil),
    ]
}

每个契合 PersistentModel 协议的类都有必要供给一个名为 schemaMetadata 的类特点。该特点详细记录了经过解析当时类型的耐久化特点界说而生成的用于创立数据模型的元数据。

其中,name 对应数据模型的 Attribute Name,keypath 为当时类型对应特点的 KeyPath,defaultValue 对应特点在声明中设置的默许值(没有默许值,为 nil ),而 metadata 则包含了其他的信息,例如:联系描绘、删去规则、原始名称等内容。

@Attribute(.unique, originalName: "old_timestamp")
var timestamp: Date = Date.now
static var schemaMetadata: [SwiftData.Schema.PropertyMetadata] {
  return [
    SwiftData.Schema.PropertyMetadata(name: "timestamp", keypath: \Item.timestamp, defaultValue: Date.now, metadata: SwiftData.Schema.Attribute(.unique, originalName: "old_timestamp"))
  ]
}

defaultValue 与开发者在 Xcode 模型编辑器中为 Attribute 创立的默许值功用共同。由于 SwiftData 允许数据模型的特点声明为更为复杂的类型(枚举,契合 Encoded 协议的结构体等),因此,SwiftData 在构建模型时将经过给定的 KeyPath 来映射对应的存储类型,而且每个 PropertyMetadata 并非必定对应 SQLite 中的一个字段(可能会根据类型创立多个字段)。

SwiftData 将直接读取类特点 schemaMetadata 来完结 Schema 甚至 ModelContainer 的创立。

let schema = Schema([Item.self])

开发者能够运用 Core Data 的新 API NSManagedObjectModel.makeManagedObjectModel,经过为 SwiftData 声明的模型代码来生成对应的 NSManagedObjectModel:

let model = NSManagedObjectModel.makeManagedObjectModel(for: [Item.self])

BackingData

每个 PersistentModel 实例的底层都对应了一个保管目标实例( NSManagedObject ),它被包装在 _DefaultBackingData 类型中( 契合 BackingData 协议 )。

@Transient
private var _$backingData: any SwiftData.BackingData<Item> = Item.createBackingData()
public var persistentBackingData: any BackingData<Item> {
    get {
        self._$backingData
    }
    set {
        self._$backingData = newValue
    }
}

createBackingData 是 PersistentModel 协议供给的一个类办法,它经过获取现已加载的数据模型信息,创立一个契合 BackingData 协议的实例,比方:_DefaultBackingData<Item>

在调用 createBackingData 时,SwiftData 不能仅依靠当时类供给的 schemaMetadata 创立实例。换句话说,只有在创立了 ModelContainer 实例后,createBackingData 才干正确地构建 PersistentModel 实例。这一点与 Core Data 不同,Core Data 能够仅经过 NSEntityDescription 信息(无需加载 NSManagedObjectModel)创立实例。

下面是 SwiftDataKit 中用于从 BackingData 中获取对应 NSManagedObject 实例的代码:

public extension BackingData {
    // Computed property to access the NSManagedObject
    var managedObject: NSManagedObject? {
        guard let object = getMirrorChildValue(of: self, childName: "_managedObject") as? NSManagedObject else {
            return nil
        }
        return object
    }
}
func getMirrorChildValue(of object: Any, childName: String) -> Any? {
    guard let child = Mirror(reflecting: object).children.first(where: { $0.label == childName }) else {
        return nil
    }
    return child.value
}

经过下面的代码,能够看到:

private var _$backingData: any SwiftData.BackingData<Item> = Item.createBackingData()

SwiftData 调用 createBackingData 来创立 backingData 的实例时,不需要 ModelContext( NSMangedObjectContext )的存在。其内部应该运用了如下的构建保管目标的方式:

let item = Item(entity: Item.entity(), insertInto: nil)

这点也解说了,为什么在 SwiftData 中,咱们创立一个 PersistentModel 实例后,有必要显式的将其注册( insert )到某个 ModelContext 上面。

let item = Item(timestamp:Date.now)
modelContext.insert(item) // must insert into some modelContext

由于 backingData( _DafaultBackingData )没有揭露的结构办法,咱们无法经过保管目标实例来构建该数据。PersistentModel 中的另一个结构办法是为 SwiftData 内部将保管目标转换为 PersistentModel 供给的。

public init(backingData: any BackingData<Item>) {
    _timestamp = _SwiftDataNoType()
    self.persistentBackingData = backingData
}

Init Accessors

经过调查完好的打开代码,timestamp 被宏代码转换成了一个具备结构器的核算特点。

public var timestamp: Date = Date.now {
    @storageRestrictions(accesses: _$backingData, initializes: _timestamp)
    init(initialValue) {
        _$backingData.setValue(forKey: \.timestamp, to: initialValue)
        _timestamp = _SwiftDataNoType()
    }
    get {
        _$observationRegistrar.access(self, keyPath: \.timestamp)
        return self.getValue(forKey: \.timestamp)
    }
    set {
        _$observationRegistrar.withMutation(of: self, keyPath: \.timestamp) {
            self.setValue(forKey: \.timestamp, to: newValue)
        }
    }
}

那么,SwiftData 在构建 PersistentModel 实例时,是怎么为其构建当时值的呢?先看一下下面的代码:

public init(timestamp: Date) {
    self.timestamp = timestamp
}
let item = Item(timestamp: Date.distantPast)

在 SwiftData 运用 createBackingData 创立 Item 实例时,首先会创立一个 timestamp 默许值为 Date.now 的 NSManagedObject 实例(经过 schemaMetadata 传递给 Schema,并包装在 backingData 中)。然后,经过初始化拜访器(Init Accessors)为 timestamp 设置新的值(来自结构办法参数,Date.distantPast)。

初始化拜访器 (Init Accessors) 是 Swift 5.9 中新添加的功用。它将核算特点归入初始化分析(definite initialization analysis)。这样,在初始化办法中能够直接对核算特点赋值,它会转化成对应的存储特点的初始化值。

这段代码的意义是:

@storageRestrictions(accesses: _$backingData, initializes: _timestamp)
init(initialValue) {
    _$backingData.setValue(forKey: \.timestamp, to: initialValue)
    _timestamp = _SwiftDataNoType()
}
  • accesses: _$backingData 表明在 init 中会拜访 _$backingData 这个存储特点。这意味着在调用本 init 拜访器初始化 timestamp 之前,有必要先初始化 _$backingData
  • initializes: _timestamp 表明这个 init 拜访器会初始化 _timestamp 这个存储特点。
  • initialValue:对应传入结构办法参数的初始化值,本例中为 Date.distantPast

Init Accessors 作为 Swift 言语的新功用,相较特点包装器( Property Wrapper ),供给了更一致、精密、清晰和灵活的初始化模型。SwiftData 运用这一功用,在结构阶段对耐久化特点进行显式赋值,减轻了开发者的工作量,也让模型代码的声明更契合 Swift 言语的逻辑。

与 Observation 结构融合

与 NSManagedObject 运用 Combine 结构供给的 Publisher 与 SwiftUI 的视图绑定不同,SwiftData 的 PersistentModel 选用了新的 Observation 结构。

请阅览 深度解读 Observation —— SwiftUI 性能提升的新途径 ,了解更多有关 Observation 结构的信息。

为了满意 Observation 结构的需求,SwiftData 为模型代码添加了以下内容:

extension Item: Observation.Observable {}
public final class Item {
    // 用户界说的耐久化特点
    public var timestamp: Date = .now {
        ....
        get {
            _$observationRegistrar.access(self, keyPath: \.timestamp)
            return self.getValue(forKey: \.timestamp)
        }
        set {
            _$observationRegistrar.withMutation(of: self, keyPath: \.timestamp) {
                self.setValue(forKey: \.timestamp, to: newValue)
            }
        }
    }
    ....
    // Observation 协议要求的调查注册器
    @Transient
    private let _$observationRegistrar: ObservationRegistrar = Observation.ObservationRegistrar()
}

经过在耐久化特点的 get 和 set 办法中运用 _$observationRegistrar 来注册和告诉调查者,完结了以特点为粒度的调查机制。这样做能够大幅减少由于无关特点变动而导致的视图无效更新。

从上面的注册办法中能够得知,开发者有必要显式调用耐久化特点的 set 办法,才干让调查者获取到数据变化的告诉(调用 withObservationTracking 的 onChange 闭包)。

Get 和 Set 办法

PersistentModel 协议界说了一些 get 和 set 办法,并供给了默许完结。例如:

public func getValue<Value, OtherModel>(forKey: KeyPath<Self, Value>) -> Value where Value : Decodable, Value : RelationshipCollection, OtherModel == Value.PersistentElement
public func getTransformableValue<Value>(forKey: KeyPath<Self, Value>) -> Value
public func setValue<Value>(forKey: KeyPath<Self, Value>, to newValue: Value) where Value : Encodable
public func setValue<Value>(forKey: KeyPath<Self, Value>, to newValue: Value) where Value : PersistentModel

经过这些办法,开发者能够读取或写入某个耐久化特点。请注意,运用上述的 set 办法(例如:setValue)给特点设置新的值将会绕过 Observation 结构,特点订阅者将无法得到特点发生变化的告诉(视图不会主动改写)。相同,假如用 SwiftDataKit 直接改写 PersistentModel 底层对应的 NSManagedObject 实例的耐久化特点,也不会发生告诉。

item.setValue(forKey: \.timestamp, to: date) // 不告诉 timestamp 的订阅者
item.timestamp = date // 告诉 timestamp 的订阅者

BackingData 协议还供给了 get 和 set 办法的界说和默许完结。BackingData 供给的 setValue 办法只能修正 PersistentModel 对应的底层 NSManagedObject 特点,与经过 SwiftDataKit 修正保管目标实例的效果相似。直接运用该办法将导致底层 NSManagedObject 的数据与表层 PersistentModel 数据不共同。

除了供给与 NSManagedObject 的 get 和 set 办法相似的功用外,PersistentModel 协议供给的 get 和 set 办法还要履行其他操作,例如将 PersistentModel 的一个特点对应到 NSManagedObject 的多个特点(当特点为复杂类型时),以及线程调度(保证线程安全)等使命。

其他

除了上述的内容外,PersistentModel 协议还声明晰其他几个特点:

  • hasChanges:表明是否发生了改动,与 NSManagedObject 的同名特点功用相似。
  • isDeleted:表明是否已添加到 ModelContext 的删去列表,与 NSManagedObject 的同名特点功用相似。
  • modelContext:当时 PersistentModel 所注册的 ModelContext,在未经过 insert 进行注册前,该值为 nil

与 NSManagedObject 比较,SwiftData 现在仅暴露了有限的 API。跟着 SwiftData 的不断发展,可能会供给更多功用供开发者运用。

总结

本文经过详细分析一段 SwiftData 简略模型的代码,深化解析了其完结原理,包括模型构建、PersistentModel 实例生成以及特点调查告诉机制等。分析的过程也是娴熟运用一个结构的重要途径。

在代码解析的过程中,咱们不仅加深了对 SwiftData 结构的知道,也对许多 Swift 言语的新特性有了更直观的了解,可谓一举两得。

订阅我的电子周报 Fatbobman’s Swift Weekly,你将每周及时获取有关 Swift、SwiftUI、CoreData 和 SwiftData 的最新文章和资讯。

原文宣布在我的博客wwww.fatbobman.com

欢迎订阅我的大众号:【肘子的Swift记事本】