概览
在最新的 WWDC 2024 中,苹果对多个系统框架都做了重量级的功能升级。这怎么能够少了 SwiftData 这位“后起之秀”呢?
万象更新的 iOS 18 为 SwiftData 增加了全新的唯一性、自定义数据仓库、富表达式以及字段索引等超赞功能。
在本篇博文中,您将学到如下内容:
- 概览
- 1. 什么是 SwiftData?
- 2. 新的 #Unique 宏
- 3. 历史数据操作记录
- 4. 自定义数据仓库(Data Stores)
- 5. Xcode 预览 Traits
- 6. 自定义额外数据 Queries
- 7. #Expression 表达式宏
- 8. #Index 宏
- 总结
闲言少叙,让我们马上一起跃入 SwiftData 焕然一新的世界中吧!
Let‘s go!!!😉
1. 什么是 SwiftData?
在去年苹果乘着 iOS 17 的东风祭出了全新纯 Swifty 范儿的数据库 SwiftData。
利用 SwiftData 我们仅用寥寥几行描述性代码就可以易如反掌的构建出一个“羽翼丰满”的数据库支持应用。
SwiftData 内置了海量的功能特性,我们可以用它们来搭建本地或者 iCloud 支持的复杂数据库 App。
如下代码所示,我们借助 @Model 宏将平淡无奇的 Trip、BucketListItem 以及 LivingAccommodation 类转换为了持久存储背后“撑腰”的 SwiftData 类型:
// Trip Models decorated with @Model
import Foundation
import SwiftData
@Model
class Trip {
var name: String
var destination: String
var startDate: Date
var endDate: Date
var bucketList: [BucketListItem] = [BucketListItem]()
var livingAccommodation: LivingAccommodation?
}
@Model
class BucketListItem {...}
@Model
class LivingAccommodation {...}
有了上面的类型定义,我们就可以自然而然的将数据融入到 SwiftUI 视图中啦:
// Trip App using modelContainer Scene modifier
import SwiftUI
import SwiftData
@main
struct TripsApp: App {
var body: some Scene {
WindowGroup {
ContentView
}
.modelContainer(for: Trip.self)
}
}
除此之外,借助于 Swift 5.9 中新的宏(Macro)机制我们可以恣意设置和调整 SwiftData 类型中各个属性和关系。比如,我们可以使用 @Transient 修饰符让类中对应的属性不占据数据库的字段空间而是仅存于内存中。
2. 新的 #Unique 宏
从 iOS 18 开始,苹果为 SwiftData 增加了新的 #Unique 宏用来表示类型中属性的唯一性,并且当新插入的对象与已有对象发生冲突时(Collisions)将新增操作改为数据更新操作:
在下面的代码中,我们利用 #Unique 宏将 Trip 类中 name、startDate 和 endDate 三个属性的值组合为判断 Trip 在数据库中存在唯一性的判断标准:
// Add unique constraints to avoid duplication
import SwiftData
@Model
class Trip {
#Unique<Trip>([\.name, \.startDate, \.endDate])
var name: String
var destination: String
var startDate: Date
var endDate: Date
var bucketList: [BucketListItem] = [BucketListItem]()
var livingAccommodation: LivingAccommodation?
}
这样一来,借助 #Unique 宏我们即可省去大量附加判断代码,在数据库层面完成托管对象唯一性的检查。
3. 历史数据操作记录
有了上面的 #Unique 宏,我们还可以进一步为 SwiftData 类型的实例属性添加历史(History)记录支持。
如下代码所示,我们利用 @Attribute 宏在 #Unique 对应的三个属性上开启了 .preserveValueOnDeletion 特性:
// Add .preserveValueOnDeletion to capture unique columns
import SwiftData
@Model
class Trip {
#Unique<Trip>([\.name, \.startDate, \.endDate])
@Attribute(.preserveValueOnDeletion)
var name: String
var destination: String
@Attribute(.preserveValueOnDeletion)
var startDate: Date
@Attribute(.preserveValueOnDeletion)
var endDate: Date
var bucketList: [BucketListItem] = [BucketListItem]()
var livingAccommodation: LivingAccommodation?
}
这样一来我们就将上面三个属性变为了墓碑(tombstone)属性,使他们可以在 Trip 托管对象被删除时在历史记录中得以显现,以便让用户可以直观查看到数据库的“变迁史”。
4. 自定义数据仓库(Data Stores)
在之前的 SwiftData 中我们可以在 SwiftUI 里用 .modelContainer 修改器为特定托管类型创建对应的模型容器:
// Trip App using modelContainer Scene modifier
import SwiftUI
import SwiftData
@main
struct TripsApp: App {
var body: some Scene {
WindowGroup {
ContentView()
}
.modelContainer(for: Trip.self)
}
}
我们也可以根据需求创建“心有所想”的复杂模型容器,比如在下面代码中我们创建的 Model Container 容器让所有 Trip 托管对象仅存活于内存中、并开启了自动保存和 Undo 功能:
// Customize a model container in the app
import SwiftUI
import SwiftData
@main
struct TripsApp: App {
var body: some Scene {
WindowGroup {
ContentView()
}
.modelContainer(for: Trip.self,
inMemory: true,
isAutosaveEnabled: true,
isUndoEnabled: true)
}
}
不仅如此,我们还有更加无拘无束创建模型容器的自由:
// Add a model container to the app
import SwiftUI
import SwiftData
@main
struct TripsApp: App {
var container: ModelContainer = {
do {
let configuration = ModelConfiguration(schema: Schema([Trip.self]), url: fileURL)
return try ModelContainer(for: Trip.self, configurations: configuration)
}
catch { ... }
}()
var body: some Scene {
WindowGroup {
ContentView()
}
.modelContainer(container)
}
}
好消息来了!从 iOS 18 开始我们可以更进一步创建自定义数据仓库 Data Stores,这样数据仓库会拥有更加健壮、更加自由的持久后端支持。这是第一次我们可以在 SwiftData 中创建自己的数据存储仓库:
// Use your own custom data store
import SwiftUI
import SwiftData
@main
struct TripsApp: App {
var container: ModelContainer = {
do {
let configuration = JSONStoreConfiguration(schema: Schema([Trip.self]), url: jsonFileURL)
return try ModelContainer(for: Trip.self, configurations: configuration)
}
catch { ... }
}()
var body: some Scene {
WindowGroup {
ContentView()
}
.modelContainer(container)
}
}
想要了解 Data Stores 定制的更多内容,大家可以进一步观赏下图中的 WWDC24 视频课:
5. Xcode 预览 Traits
大家知道,SwiftData 与 SwiftUI 搭配可谓是“双剑合璧天下无敌”,而 Xcode 预览(Preview)又对 SwiftUI 界面调试有着“匪夷所思”的优秀支持。
在 iOS 18 中,我们可以让 SwiftData 数据模型在 Xcode 的预览中继续大放异彩。这可以通过预览的 Trait 机制来完成:
// Make preview data using traits
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())
}
在上面的代码中,我们让 SampleData 遵循 PreviewModifier 协议并知趣的实现其中两个方法。
这样一来,在 Xcode 预览中使用 SampleData 提供的 SwiftData 数据源就变成“探囊取物”一般的简单啦:
// Use sample data in a preview
import SwiftUI
import SwiftData
struct ContentView: View {
@Query
var trips: [Trip]
var body: some View {
...
}
}
#Preview(traits: .sampleData) {
ContentView()
}
6. 自定义额外数据 Queries
在 Xcode 预览的调试中,对于某些视图来说其本身并不包含 Query 属性而是需要从外部传入它们。
从 iOS 18 开始,SwiftData 也为其提供了更“银杏化”的支持。现在我们可以直接在预览 #Preview 附着的闭包中插入所需的 Query 状态了,这是通过 @Previewable 宏来实现的:
// Create a preview query using @Previewable
import SwiftUI
import SwiftData
#Preview(traits: .sampleData) {
@Previewable @Query var trips: [Trip]
BucketListItemView(trip: trips.first)
}
有了新的 @Previewable 宏,为特定视图创建 Query 数据集变得史无前例的简单了。
7. #Expression 表达式宏
从 iOS 17 开始,苹果推出了新的 #Predicate 宏让我们可以自由自在的构建数据查询相关的断言(Predicate):
// Create a Predicate to find a Trip based on Search Text
let predicate = #Predicate<Trip> {
searchText.isEmpty ? true : $0.name.localizedStandardContains(searchText)
}
除了简单的条件以外,我们还可以在 #Predicate 闭包中合成复杂的多条件查询语句:
// Create a Compound Predicate to find a Trip based on Search Text
let predicate = #Predicate<Trip> {
searchText.isEmpty ? true :
$0.name.localizedStandardContains(searchText) ||
$0.destination.localizedStandardContains(searchText)
}
如今,在 iOS 18+ 中我们的自由度进一步得到与时俱进的增强。现在我们可以使用全新的 #Expression 宏来让断言的表达能力更加“突飞猛进”:
// Build a predicate to find Trips with BucketListItems that are not in the plan
let unplannedItemsExpression = #Expression<[BucketListItem], Int> { items in
items.filter {
!$0.isInPlan
}.count
}
let today = Date.now
let tripsWithUnplannedItems = #Predicate<Trip>{ trip
// 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 宏创建了 unplannedItemsExpression 表达式,并将其应用在了 tripsWithUnplannedItems 断言中从而创建出有史以来最复杂、最富表现力的复合查询条件。
8. #Index 宏
大家都知道,在数据库的查询等操作中为特定字段增加索引可以极大的增加查表速度。
从 iOS 18 开始,数据库索引机制终于显式加入到了 SwiftData 中,这是通过 #Index 宏实现的。
关于 Apple 数据库相关索引机制的进一步介绍,感兴趣的小伙伴们可以移步如下链接继续观赏精彩的内容:
- SwiftUI 后台刷新多个 Section 导致 global index in collection view 与实际不匹配问题的解决
- 探究 CoreData 使用索引(Index)机制加速查表究竟如何实现?
这是一个令我们喜不自胜的 SwiftData 新功能!
使用全新的 #Index 宏我们可以为数据类型中经常访问的特定属性增加查表索引,以便增加查询效率。
如下代码所示,我们不仅可以在单个属性上增加索引,还可以将索引应用到多个属性组合上去。
// Add Index for commonly used KeyPaths or combination of KeyPaths
import SwiftData
@Model
class Trip {
#Unique<Trip>([\.name, \.startDate, \.endDate
#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
}
为托管类型增加索引,不仅有利于查表速度,对于海量数据的筛选和排序也会大有裨益,棒棒哒!💯
总结
在本篇博文中,我们介绍了 iOS 18 中 SwiftData 框架的“重装升级”。其中我感觉 #Expression 和 #Index 宏对小伙伴的实际帮助更为突出,大家怎么认为呢?欢迎讨论哦。
感谢观赏,再会!😎