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 实现了重大变革,现在允许开发者通过符合 DataStore 和 DataStoreConfiguration 协议的自定义实现来定义底层存储格式。这一功能使得开发者在保持相同的表现层代码的同时,能够在底层采用文件、各类数据库或网络数据库等多种数据存储方式。
在使用 ModelConfiguration
(即 DataStoreConfiguration
)构建 ModelContainer
时,系统默认使用支持 Core Data 存储格式的 DefaultStore
(DataStore
)实现。
这一革命性的改动虽然对多数 SwiftData 用户的日常使用影响不大——代码的变更几乎感觉不到,但这次的改变可能会在一段时间内引发稳定性问题。
欲了解更多关于此功能的细节,请观看 Create a custom data store with SwiftData,我将在未来的文章中对此功能进行更深入的分析。
数据变化历史跟踪 ( SwiftData History )
SwiftData 的初版未包含类似 Core Data 的 持久化历史跟踪 功能,同时,willSave
和 didSave
通知的缺失限制了开发者在应用外自动监测数据变化的能力。幸运的是,这一缺陷在最新更新中得到了修正。
在 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 的同步规则。
@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 宏:此宏允许为单个或多个属性创建索引,从而提升检索效率。对于那些经常用于搜索和排序的属性,索引能显著加快查询速度。然而,索引的使用也会占用更多存储空间并可能影响写入性能。因此,为那些经常用作查询条件或排序依据,并且涉及大量数据的属性创建索引,才可能带来实际的效益。
@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
选项,即使数据被删除,该属性的内容仍将保留在数据变化历史的删除记录中。这使得开发者可以根据历史记录中的具体内容进行相应的后续处理。
@Model
class Trip {
@Attribute(.preserveValueOnDeletion)
var startDate: Date
@Attribute(.preserveValueOnDeletion)
var endDate: Date
}
更友好的预览环境设置
随着 WWDC 2024 的更新,预览包含 SwiftData 数据的视图现在更加方便安全。
- PreviewTrait:SwiftUI 的最新更新引入了 PreviewModifier 协议,该协议允许开发者轻松自定义 PreviewTrait,从而为预览构建一个安全完整的环境上下文。以下代码展示了如何实现一个
PreviewModifier
,该实现为预览视图创建modelContainer
,生成演示数据,并注入上下文实例。
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
提供。
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
宏使得开发者可以通过多个独立的表达式来分别定义谓词,这不仅使构建复杂谓词更为清晰,还增强了表达式的可复用性。
与谓词只能返回布尔值不同,表达式可以返回任意类型。因此,在声明表达式时,开发者需要明确指定输入和输出类型。
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
在非主上下文进行数据操作时,视图仍无法正确响应数据变化( 甚至相较于上个版本表现更差 )。 - 在处理多关系时,多端插入数据的性能问题依旧存在。
didSave
和willSave
的通知功能仍然无法使用。
鉴于首个版本在稳定性方面的表现不佳,我计划过段时间再进行更深入的测试。
坦白说,从目前的问题解决情况来看,我的心情相当沮丧。
现在适合在项目中使用 SwiftData 吗?
这两天,我收到了许多开发者关于现阶段是否可以在新项目中采用 SwiftData 的询问。对此,我在当下有些迷茫。
就功能而言,尽管仍有所欠缺,更新后的 SwiftData 已能满足大多数应用场景。然而,关于其稳定性,我目前并无足够信心。特别是考虑到此次更新中底层结构的重大调整,其对稳定性的短期影响尚难以预测。
因此,我建议还未使用 SwiftData 的开发者在未来一至两个月内暂避免在实际项目中部署 SwiftData。等待一段时间,直到其稳定性得到进一步验证后再考虑采用。当然,在此期间,深入了解 SwiftData 的文档和文章,学习其全新的设计理念仍然非常重要。
希望SwiftData 能尽快证明其在实际应用中的可靠性。
SwiftData,苹果对 Swift 社区的再一贡献?
我想很多读者在此刻心情或许有些沮丧,如此优秀的框架,为何不将优化稳定性放在首位?
撰写本文过程中,这个疑问一直挥之不去:为什么苹果会在框架推出仅一年后就进行如此重大的调整?这样做的目的何在?这将如何影响未来的发展和前景?
通过深入学习新的 API,我发现当前的 SwiftData 已经在很大程度上从苹果生态体系中解耦。换句话说,它已经初步具备成为一个跨平台开源 Swift 框架的基础。如果 SwiftData 能进一步提供一个与平台无关的默认存储实现,我们很可能在未来几年中在非苹果生态系统中看到其被应用的场景,成为提升 Swift 语言跨平台影响力的重要手段。
这或许就是 SwiftData 继续进行大规模变革的原因。当它最终开源时,将标志着又一场新的革命的开始!我们期待这一天能够早日到来。