Highlight
SwiftData 新增
#Unique宏支持复合唯一约束,新增#Index宏加速查询性能,并支持自定义数据存储实现。
核心内容
SwiftData 去年随 iOS 17 发布,旨在用 Swift 的现代语言特性(如宏)简化数据持久化。开发者只需用 @Model 装饰类,就能自动获得持久化能力,再通过 @Query 在 SwiftUI 视图中获取数据。
今年解决了一个实际问题:数据重复。当用户同步数据、批量导入或网络重试时,容易产生重复的模型实例。以前开发者需要手动去重,或依赖某个单一唯一标识符。但很多业务场景下,唯一性是多个字段的组合——比如旅行应用中,同名行程只要时间不同就应该允许存在,只有”名称+开始日期+结束日期”三者都相同时才算真正重复。
SwiftData 现在引入了 #Unique 宏,可以声明多个属性的复合唯一约束。当插入的模型与现有模型在这些唯一字段上冲突时,SwiftData 会自动执行 upsert(更新而非插入),从根本上避免重复。
另一个性能相关的问题是查询效率。当数据量增大时,对特定字段频繁排序和过滤会变慢。#Index 宏允许开发者声明数据库索引,类似书籍的目录,能显著加速这些操作。此外,SwiftData 现在支持完全自定义的数据存储,不再局限于 SQLite 底层,可以用 JSON、其他格式甚至完全自定义的持久化方案。
详细内容
复合唯一约束
#Unique 宏接受一组 KeyPath,声明这些属性组合必须唯一(03:08):
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<Trip>声明 Trip 模型的唯一约束\.name, \.startDate, \.endDate三个属性组合必须唯一- 当插入的模型与现有模型在这三个字段上相同时,SwiftData 会更新现有模型而非插入新模型
这些唯一字段也代表了模型的身份。为了让 SwiftData 的 History API 在模型被删除后仍能识别这些身份,可以添加 @Attribute(.preserveValueOnDeletion)(03:36):
@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?
}
@Attribute(.preserveValueOnDeletion)确保这些字段的值在模型删除后仍保留在历史记录中- History API 可以跟踪模型的插入、更新、删除,这些保留的值作为”墓碑”信息帮助处理变更
自定义数据存储
iOS 18 允许完全自定义数据存储(05:59):
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)
}
}
JSONStoreConfiguration替代默认的ModelConfiguration,使用自定义数据存储- 自定义存储可以渐进式实现功能,快速起步
- 仍可使用
@Model、@Query等 SwiftData API
预览数据支持
SwiftUI 预览现在可以用 PreviewModifier 创建示例数据(06:58):
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())
}
PreviewModifier定义预览的共享上下文isStoredInMemoryOnly: true确保预览不写磁盘Trip.makeSampleTrips填充示例数据
使用时(08:15):
#Preview(traits: .sampleData) {
ContentView()
}
对于依赖模型参数的视图,可以用 @Previewable 宏在预览中创建查询(08:50):
#Preview(traits: .sampleData) {
@Previewable @Query var trips: [Trip]
BucketListItemView(trip: trips.first)
}
@Previewable允许在预览声明中创建@Query- 返回的数组可直接传递给视图
复杂查询与表达式
#Predicate 可以构建复合过滤条件(09:55):
let predicate = #Predicate<Trip> {
searchText.isEmpty ? true :
$0.name.localizedStandardContains(searchText) ||
$0.destination.localizedStandardContains(searchText)
}
iOS 18 新增 #Expression 宏,支持非布尔值的复杂计算(10:46):
let unplannedItemsExpression = #Expression<[BucketListItem], Int> { items in
items.filter {
!$0.isInPlan
}.count
}
let today = Date.now
let tripsWithUnplannedItems = #Predicate<Trip>{ trip
(trip.startDate ..< trip.endDate).contains(today) &&
unplannedItemsExpression.evaluate(trip.bucketList) > 0
}
#Expression<[BucketListItem], Int>定义一个输入为数组、输出为整数的表达式- 表达式计算数组中满足条件的元素数量
evaluate(trip.bucketList)在谓词中调用表达式- 这使得谓词可以表达”数组中至少有一个元素满足条件”这类复杂逻辑
索引加速查询
#Index 宏为常用字段创建索引(12:41):
@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?
}
#Index<Trip>为指定 KeyPath 创建索引[\.name],[\.startDate],[\.endDate]为单个字段创建索引[\.name, \.startDate, \.endDate]为三个字段组合创建复合索引- 索引用于加速这些字段上的排序和过滤操作
- 对大数据集效果显著
核心启发
-
为业务身份添加唯一约束:如果你有一个自然的主键(如用户 ID、订单号),用
#Unique声明它。如果是复合键(如用户+时间),也一并声明。这样数据同步、批量导入时不会产生重复记录,代码也不用写去重逻辑。 -
为高频查询字段添加索引:检查应用中的
@Query和FetchDescriptor,看哪些字段经常用于排序或过滤。这些字段应该用#Index标记。数据量小时差异不明显,但数据增长后索引能带来数倍性能提升。 -
用 PreviewModifier 统一预览数据:不要在每个视图的
#Preview里手动创建模型容器和示例数据。定义一个PreviewModifier,让所有需要 SwiftData 数据的预览复用同一个数据源。这样预览更一致,修改示例数据时也只需要改一处。
关联 Session
- Track model changes with SwiftData history — 学习 SwiftData History API,跟踪模型插入、更新和删除
- Create a custom data store with SwiftData — 深入了解如何为 SwiftData 构建自定义数据存储
- Meet SwiftData — SwiftData 基础入门,学习如何建模和持久化应用数据
- Build complex queries with SwiftData — 深入学习 SwiftData 查询、谓词和关系
评论
GitHub Issues · utterances