SwiftData in WWDC 2024:革命仍在继续、稳定还需时日

发表于

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

SwiftData 自从去年首次亮相,便成为了开发者群体备受关注的焦点框架。随着 WWDC 2024 的来临,业界普遍期待 SwiftData 在功能、性能和稳定性等方面将有突破性进展。本文将评述 SwiftData 最新版本的表现,并分享我在首次体验新版时内心经历的一系列复杂情绪:震惊、喜悦、沮丧以及困惑。

数据管理框架的革命仍在继续

在 WWDC 2023 上,SwiftData 带着其作为苹果生态未来十余年最重要的数据管理框架的重任,露面了。自首个版本发布以来,这一 Core Data 的“继任者”凭借其对现代编程理念的深刻理解及潜在的巨大能力,给所有人留下了深刻而美好的印象。

称其为 Core Data 的“继任者”主要有两个理由:首先,其将长期担负起 Core Data 在苹果生态中的角色;其次,在首个版本中,SwiftData 与 Core Data 高度关联,开发者可在 SwiftData 中找到许多与 Core Data 对应的组件。基于这种关联,我还开发了 SwiftDataKit 库,允许开发者直接访问 SwiftData 组件的底层 Core Data 实现。

在文章 新框架、新思维:解析 Observation 和 SwiftData 框架 中,我曾对 SwiftData 的设计理念表示高度赞扬,尤其是其在数据建模和并发操作方面的创新,这无疑是对 Core Data 的一场革命。

原以为 SwiftData 团队会在首个版本的变革后,将重点转向稳定性的改善和功能的增强。然而,在 WWDC 2024 上,SwiftData 的更新彻底颠覆了我的预期——它竟然重写了底层数据存储逻辑,打破了先前与 Core Data 的紧密耦合,进行了广泛的抽象和分割。这个操作属实震惊到我了。

WWDC 2024 上的 SwiftData 已经演变成一个充分利用 Swift 语言新特性、具备高效数据建模能力、安全的并发操作机制、简洁的谓词表述,并兼容各类底层数据存储类型的现代数据管理框架,与 SwiftUI 的协作也更加无缝。

从这个版本开始,SwiftData 已经不能再算是是建立在 Core Data 之上。我们只能说,SwiftData 支持的默认存储格式与 Core Data 保持一致。甚至从下个版本开始,底层处理与数据库数据之间的操作也可能不再依赖 Core Data 的代码。

在当前版本中,使用 -com.apple.CoreData.SQLDebug 依然可以观察到 Core Data 的操作信息。

尽管 SwiftUI 在五年内推出了六个版本,但它从未经历过如此深刻的底层改变。相比之下,SwiftData 在仅第二年就敢于实施这样大规模的变革,这种勇气确实令人钦佩,但这样的大刀阔斧改革也让我对新版本的稳定性有所担忧。

这次对数据存储逻辑的调整虽然从长远来看是极为必要的,但考虑到没有在首个版本中就进行,实在又些遗憾。

由于存储逻辑的调整,SwiftDataKit 不再适用于更新后的 SwiftData。

WWDC 2024 带来的新功能

尽管乍看之下此次更新引入的新功能不多,其实现方式和潜在的巨大影响力却带来了许多惊喜。

自定义数据存储

在本次的更新中,SwiftData 实现了重大变革,现在允许开发者通过符合 DataStoreDataStoreConfiguration 协议的自定义实现来定义底层存储格式。这一功能使得开发者在保持相同的表现层代码的同时,能够在底层采用文件、各类数据库或网络数据库等多种数据存储方式。

在使用 ModelConfiguration(即 DataStoreConfiguration)构建 ModelContainer 时,系统默认使用支持 Core Data 存储格式的 DefaultStoreDataStore)实现。

这一革命性的改动虽然对多数 SwiftData 用户的日常使用影响不大——代码的变更几乎感觉不到,但这次的改变可能会在一段时间内引发稳定性问题。

欲了解更多关于此功能的细节,请观看 Create a custom data store with SwiftData,我将在未来的文章中对此功能进行更深入的分析。

数据变化历史跟踪 ( SwiftData History )

SwiftData 的初版未包含类似 Core Data 的 持久化历史跟踪 功能,同时,willSavedidSave 通知的缺失限制了开发者在应用外自动监测数据变化的能力。幸运的是,这一缺陷在最新更新中得到了修正。

DataStore 协议中新增了数据变化历史的访问接口,modelContext 亦提供了相应的调用 API。虽然这些新的 API 和历史数据格式都更加现代化,更符合 Swift 语言的风格,它们的操作逻辑和处理方式仍与 Core Data 的持久化历史跟踪非常相似。

欲了解更多关于此功能的细节,请观看 Track model changes with SwiftData history

数据的批量删除

DataStore 协议现在包括了数据批量操作的功能,当前主要支持批量删除。SwiftData 的 DefaultStore 已经实现了这一 API,使得开发者在调用 delete<T>(model: T.Type, where predicate: Predicate<T>?, includeSubclasses: Bool)deleteAllData 时,可以利用底层的批量操作逻辑( 大概率 )。

新的标注

  • #Unique 宏:此宏用于定义唯一性约束。与 Attribute 宏中的 unique 选项相比,#Unique 宏增加了支持跨多个属性的复合约束。由于 SwiftData 的默认存储实现仍依赖于 SQLite 的约束机制,使用此宏的数据模型将不适用于 CloudKit 的同步规则
Swift
@Model
final class Person {
    // Declare any unique constraints as part of the model definition.
    #Unique<Person>([\.id], [\.givenName, \.familyName])

    var id: UUID
    var givenName: String
    var familyName: String


    init(id: UUID, givenName: String, familyName: String) {
        self.id = id
        self.givenName = givenName
        self.familyName = familyName
    }
}
  • #Index 宏:此宏允许为单个或多个属性创建索引,从而提升检索效率。对于那些经常用于搜索和排序的属性,索引能显著加快查询速度。然而,索引的使用也会占用更多存储空间并可能影响写入性能。因此,为那些经常用作查询条件或排序依据,并且涉及大量数据的属性创建索引,才可能带来实际的效益。
Swift
@Model 
class Trip {
    #Index<Trip>([\.name], [\.startDate], [\.endDate], [\.name, \.startDate, \.endDate])

    var name: String
    var destination: String
    var startDate: Date
    var endDate: Date
    
    var bucketList: [BucketListItem] = [BucketListItem
    var livingAccommodation: LivingAccommodation
}
  • preserveValueOnDeletion 选项:通过 Attribute 宏中添加 preserveValueOnDeletion 选项,即使数据被删除,该属性的内容仍将保留在数据变化历史的删除记录中。这使得开发者可以根据历史记录中的具体内容进行相应的后续处理。
Swift
@Model 
class Trip {
    @Attribute(.preserveValueOnDeletion)
    var startDate: Date

    @Attribute(.preserveValueOnDeletion)
    var endDate: Date
}

更友好的预览环境设置

随着 WWDC 2024 的更新,预览包含 SwiftData 数据的视图现在更加方便安全。

  • PreviewTrait:SwiftUI 的最新更新引入了 PreviewModifier 协议,该协议允许开发者轻松自定义 PreviewTrait,从而为预览构建一个安全完整的环境上下文。以下代码展示了如何实现一个 PreviewModifier,该实现为预览视图创建 modelContainer,生成演示数据,并注入上下文实例。
Swift
struct SampleData: PreviewModifier {
    static func makeSharedContext() throws -> ModelContainer {
        let config = ModelConfiguration(isStoredInMemoryOnly: true)
        let container = try ModelContainer(for: Trip.self, configurations: config)
        Trip.makeSampleTrips(in: container)
        return container
    }
    
    func body(content: Content, context: ModelContainer) -> some View {
        content.modelContainer(context)
    }
}

extension PreviewTrait where T == Preview.ViewTraits {
    @MainActor static var sampleData: Self = .modifier(SampleData())
}
  • @Previewable 宏:此宏极大简化了开发者构建预览包装视图的流程。下面的示例代码演示了如何自动为预览视图创建一个包装视图,并在该视图中通过 @Query 获取相关数据。modelContext 和演示数据均由之前定义的 PreviewTrait 提供。
Swift
struct TripDetail: View {
    let trip: Trip?
    var body: some View {
        ...
    }
}

#Preview(traits: .sampleData) {
    @Previewable @Query var trips: [Trip]
    TripDetail(trip: trips.first)
}

构建复杂谓词变得更加简单

在 WWDC 2024 中,Foundation 的谓词系统引入了多项新功能,不仅增加了新的表达式方法,最为显著的改进是新增了 #Expression 宏,这大大简化了谓词表达式的构建过程。在之前的版本中,开发者仅在使用 #Predicate 宏时才能体验到构建的便捷性。现在,通过 #Expression 宏,即使是构建独立的表达式也能实现自然流畅的表达方式。

#Expression 宏使得开发者可以通过多个独立的表达式来分别定义谓词,这不仅使构建复杂谓词更为清晰,还增强了表达式的可复用性。

与谓词只能返回布尔值不同,表达式可以返回任意类型。因此,在声明表达式时,开发者需要明确指定输入和输出类型。

Swift
let unplannedItemsExpression = #Expression<[BucketListItem], Int> { items in
    items.filter {
        !$0.isInPlan
    }.count
}

let today = Date.now
let tripsWithUnplannedItems = #Predicate<Trip>{ trip in
    // The current date falls within the trip
    (trip.startDate ..< trip.endDate).contains(today) &&

    // The trip has at least one BucketListItem
    // where 'isInPlan' is false
    unplannedItemsExpression.evaluate(trip.bucketList) > 0
}

尽管 #Expression 宏是一个极具价值的工具,但增强的表达功能并不意味着 SwiftData 的 DefaultStore 已经能够正确地将谓词转换为对应的 SQL 指令。关于当前版本是否解决了无法正确转换包含 可选和 to-many 的谓词问题,我尚未进行更详尽的测试。如果您有相关信息,请通过 X 通知我,或在本文评论区留言。

欲了解如何在 SwiftData 中动态的构建谓词,请阅读 如何为 SwiftData 动态的构建复杂的谓词

上个版本的不足是否得到了解决

在文章 写在 WWDC 2024 之前:SwiftData 的未来潜力与现实挑战 中,我列出了 SwiftData 首个版本中缺失的关键功能和主要问题。经过初步测试,至少以下问题和需求依旧未得到解决:

  • 网络同步仍只支持私有数据库。
  • 使用 @ModalActor 在非主上下文进行数据操作时,视图仍无法正确响应数据变化( 甚至相较于上个版本表现更差 )。
  • 在处理多关系时,多端插入数据的性能问题依旧存在。
  • didSavewillSave 的通知功能仍然无法使用。

鉴于首个版本在稳定性方面的表现不佳,我计划过段时间再进行更深入的测试。

坦白说,从目前的问题解决情况来看,我的心情相当沮丧。

现在适合在项目中使用 SwiftData 吗?

这两天,我收到了许多开发者关于现阶段是否可以在新项目中采用 SwiftData 的询问。对此,我在当下有些迷茫。

就功能而言,尽管仍有所欠缺,更新后的 SwiftData 已能满足大多数应用场景。然而,关于其稳定性,我目前并无足够信心。特别是考虑到此次更新中底层结构的重大调整,其对稳定性的短期影响尚难以预测。

因此,我建议还未使用 SwiftData 的开发者在未来一至两个月内暂避免在实际项目中部署 SwiftData。等待一段时间,直到其稳定性得到进一步验证后再考虑采用。当然,在此期间,深入了解 SwiftData 的文档和文章,学习其全新的设计理念仍然非常重要。

希望SwiftData 能尽快证明其在实际应用中的可靠性。

SwiftData,苹果对 Swift 社区的再一贡献?

我想很多读者在此刻心情或许有些沮丧,如此优秀的框架,为何不将优化稳定性放在首位?

撰写本文过程中,这个疑问一直挥之不去:为什么苹果会在框架推出仅一年后就进行如此重大的调整?这样做的目的何在?这将如何影响未来的发展和前景?

通过深入学习新的 API,我发现当前的 SwiftData 已经在很大程度上从苹果生态体系中解耦。换句话说,它已经初步具备成为一个跨平台开源 Swift 框架的基础。如果 SwiftData 能进一步提供一个与平台无关的默认存储实现,我们很可能在未来几年中在非苹果生态系统中看到其被应用的场景,成为提升 Swift 语言跨平台影响力的重要手段。

这或许就是 SwiftData 继续进行大规模变革的原因。当它最终开源时,将标志着又一场新的革命的开始!我们期待这一天能够早日到来。

与全球开发者一同,每周探索 Swift 世界的精彩内容

如果文章对你有所帮助,可以请我喝杯