大多数浏览器和
Developer App 均支持流媒体播放。
-
SwiftData 的新功能
借助 SwiftData,你可以通过富有表现力的声明式 API 为你的 App 添加持久化功能。了解 SwiftData 的改进功能,包括复合唯一性约束、使用 #Index 实现更快的查询、在 Xcode 预览中进行查询,以及丰富的谓词表达式。和我们一起探索如何使用所有这些功能来表达更丰富的模型,并提升 App 的性能。要了解如何在 SwiftData 中构建自定数据存储或使用历史记录 API,请观看“使用 SwiftData 创建自定数据存储”和“使用 SwiftData 历史记录 API 跟踪模型更改”。
章节
- 0:00 - Introduction
- 0:57 - Adopt SwiftData
- 2:11 - Customize the schema
- 2:43 - #Unique macro
- 3:37 - History API
- 4:29 - Tailor a model container
- 5:39 - Custom data stores
- 6:41 - Xcode previews
- 9:20 - Customize queries
- 10:18 - #Expression macro
- 11:56 - #Index macro
资源
相关视频
WWDC24
WWDC23
-
下载
嗨 我叫 Rishi Verma 是 SwiftData 团队 的一名工程师 很高兴今天能与大家分享 SwiftData 的所有新功能
iOS 17 推出了 SwiftData 框架 让你可以在所有 Apple 平台上 用 Swift 针对 App 的数据 进行建模并实现持久化 通过它 你能够快速、高效 又安全地编写代码 它运用的是 现代 Swift 语言功能 比如宏 在这个视频中 我们首先快速回顾一下 SwiftData 框架 然后深入了解新功能 我会先介绍如何使用新的架构宏 来避免重复模型 然后介绍 设置和配置模型容器的新方法
最后 深入了解 如何用复杂的筛选条件来优化查询 并利用新的宏 来提升性能
首先 让我们 快速了解一下 SwiftData SwiftData 这种框架 让你可以轻松构建 App 的模型层 并在 App 的每次启动中 始终保持这个模型层
这个框架不仅可以 提供持久性 还可针对架构进行建模和迁移 而且支持图表管理、 CloudKit 同步等等 为了说明在 App 中采用 SwiftData 有多么容易 我会展示我和团队 正在开发的一款 App — Trips! “Trips”是一款 用 SwiftUI 编写的 App 用来追踪记录 对于度假的各种想法
要将 SwiftData 与这个 App 中的模型搭配使用 我只需要导入框架 用 @Model 宏来修饰每个模型 这就是 SwiftData 的强大之处!
在 App 的定义中 WindowGroup 上的修饰符 modelContainer 会展现 Trip 模型的 整体视图层次结构
这样一来 我的视图就可以移除静态数据 使用 @Query 来填充视图 这会从模型容器中获取 Trip 模型 并返回“Trips”数组 就是这样 现在 App 会保持我创建的所有行程 并完美地结合到 我的 SwiftUI 视图中 第一步是添加 @Model 宏 而这仅仅是 自定义架构的开始
@Model 宏功能强大 可以快速启动持久性体验 只需用宏简单地修饰一下 所有持久化类 Trip 类以及相关模型的 存储属性就会被持久保持
你还可以更进一步 使用 @Attribute 和 @Relationship 宏进行架构自定义 还能将存储的属性标记为 @Transient 以避免将这个数据持久化
今年 我们有一个新的架构宏 可以对持久性模型 构造出复合约束
你可以使用全新的 #Unique 宏 向 SwiftData 说明 哪些模型属性的组合 必须始终 在模型数据中保持唯一 当两个模型实例 共享相同的唯一值时 SwiftData 将在与现有模型冲突时 执行更新插入
例如 在“Trips”App 中 我可以使用 #Unique 宏来确保 每个行程相对于它们的名称、 startDate 和 endDate 都是独一无二的 这样 只有当行程开始日期 或结束日期不同时 我的 App 中才会出现多个 名称相同的行程 这样一来 避免数据重复就非常简单 因为 SwiftData 有更多的信息来推断 哪些模型 实际上是重复的 并对数据执行更新
正是因为这些 #Unique 属性可以 帮助确保 @Model 不重复 它们也代表了 这个模型的身份 你还可以使用 @Attribute 宏 结合 preserveValueOnDeletion 来修饰这些属性 这样可以确保这些标识值 在使用 SwiftData 中的 历史记录 API 时可用
SwiftData 历史记录 为 App 提供了一种方法 可以了解哪些模型 在一段时间内经历了 插入、更新或删除操作 模型删除后 被标记为保留的值 会作为“墓碑值” 保留在历史记录信息中 为 App 提供处理这些更改时 所需的信息 它还能与旨在为这项功能提供支持的 自定数据存储无缝协作 要进一步了解 请观看视频 “使用 SwiftData 历史记录 API 跟踪模型更改” 通过定制模型容器 你可以为 App 调整数据存储位置 以及数据在整个 App 中的使用方式
modelContainer 修饰符为开始使用 SwiftData 提供了一种最简单的方式 只需提供 要保持的模型类型 SwiftData 会为你设置一个容器 modelContainer 修饰符 还允许你自定 容器的一些属性
比如 它可以将数据保存在内存中 而不是磁盘上 它可以启用或停用自动存储功能 它还可以启用或停用 撤销-重做支持
要进一步 自定义 modelContainer 比如更改它在磁盘上的存储位置 则可以单独构建你自己的 modelContainer 实例 下面我以“Trip”App 为例演示一下 我不使用 modelContainer 修饰符来构造容器 而是使用名为 container 的属性 创建自己的容器
在这个属性的闭包中 我要为模型创建配置 并传递架构 在这里 我还会自定义 数据在磁盘上的 URL 然后 我会将这个配置 传递给 ModelContainer 构造器 然后再将它返回
在 iOS 18 中 SwiftData 让你能够 通过完全自定数据存储 进一步自定义 modelContainer 默认的数据存储提供了 强大的持久化后端 支持 SwiftData 的所有功能 而现在 你可以 创建自己的数据存储 使用自己的实现方法 在容器中持久保存数据
例如 在“Trips”App 中 我已经实现了自己的自定文稿格式 由 JSON 文件组成 要在 App 中使用它 我只需要置换出 自定数据存储中 提供的一个模型配置 在这个示例中 也就是 JSONStoreConfiguration
自定数据存储让你能够 使用熟悉的 SwiftData API 比如 @Model 和 @Query 宏 无论数据需要 以何种格式持久保存 它还针对数据存储提供了一种方法 可以循序渐进地采用各种功能 便于大家快速上手 要进一步了解 请观看视频 “利用 SwiftData 创建自定数据存储”
你还可以创建自定容器 来搭配 Xcode 预览使用 在使用 SwiftUI 开发 App 时 预览是一个好搭档 它能够很好地 与 SwiftData 搭配使用
我想为“Trips”App 中的每个视图 创建出色的预览 首先 我要使用预览特征
为此 我将创建 一个名为 SampleData 的新结构体 让它符合 PreviewModifier 其中准备好 我需要填写的两个函数 一个函数用于 为预览设置共享情境 另一个用于 将共享情境应用到视图 对于我的“Trips”预览 我将提供一个 ModelContainer 作为 sampleData 的共享情境 由于预览不需要 将任何内容存储到磁盘 我将创建一个仅在内存中储存数据的 ModelConfiguration 并设置 ModelContainer
然后我会调用 我之前创建的方法 它创建了 各种样本行程 然后将它们存储到模型容器中 由于行程现在通过 名称和日期来保证唯一性 所以这段代码不需要删除任何重复 数据 SwiftData 会为我搞定这些!
最后 我会返回这个容器 接下来 我需要实现一个方法 将 modelContainer 添加到使用这个 sampleData 的视图中 要进行这个操作 我只需使用 modelContainer 修饰符 来应用这个容器
最后 我将向 PreviewTrait 添加扩展 便于我轻松地访问 这个 sampleData 这个操作会创建一个名为 sampleData() 的新的静态属性 而这个 SampleData() 结构 将作为修饰符来应用
现在 当我为任何 SwiftUI 视图声明预览时 我都可以将 .sampleData 与 traits 参数搭配使用 这样做会创建 一个内存中模型容器 载入 sampleData 并修改我的预览 以便在 SwiftUI 视图中使用它
拥有大量可用的样本数据 让我可以使用 SwiftData 查询 轻松处理 App 的任何视图 但有些 App 视图 可能不包含查询 因为它们依赖于 传递给它们的模型 现在 你也能使用 @Previewable 宏 为这些视图制作出色的预览
比如 在“Trips”中 BucketListItemView 将 一个行程用作参数 在我的 sampleData 中 现在 bucketListItemView 已有模型容器 其中包含了一些 sampleData 但它还没有查询这些数据
现在你可以使用 @Previewable 宏 直接在预览声明中创建查询 这提供了一个可以传递给 BucketListItemView 的行程数组 从而使用 sampleData 创建预览
最后 我们来说说如何为 SwiftData 创建丰富且经过优化的查询 查询可以用一组模型 来驱动 SwiftUI 视图 这些模型可以轻松进行排序和筛选 并且它会自动响应 对 ModelContainer 所做的更改 #Predicate 有助于筛选 并可以在数据查询期间进行评估 而不是使用 大型的内存数据集 我们来看几个 筛选“Trips”的方法 如果我在“Trips”App 中 添加一个搜索栏 searchText 就可用于构建谓词 从而筛选查询 甚至是提取
构建谓词非常简单 我根据用户提供的 searchText 查看行程的名称是否包含这个文本 但这个文本可能 不仅应用于行程的名称
所以我要 构建复合谓词 以便同时查看行程中的 destination 属性 这就是构建复合谓词 所需的全部内容 但我也可以让谓词的用途 远不止于此
iOS 18 带来的新功能是可以使用 Foundation 的新 #Expression 宏 来轻松构建复杂谓词
表达式允许引用 不生成 true 或 false 的值 也就是允许引用任意类型的值
表达式可以用于 表示复杂的评估 使用模型的属性 并在谓词中组合 来进一步定制查询结果
我想在“Trips”App 中创建查询 用来获取任何正在进行的行程 这些行程还有 一些景点没看 这些都是在行程中 由 BucketListItems 建模的 可以看到 isInPlan 属性 仍然是 false 我要先构建谓词
在谓词中 我指定行程应该是正在进行中的 所以当前日期 位于开始和结束的范围之内
但我还需要至少指定 这个行程中的一个 BucketListItem 将 isInPlan 属性设置为 false 仅凭谓词 无法表达这一点 因为缺少相应属性 来计算计划外的 BucketListItem 数量 为此 我可以构造一个表达式 将这个逻辑构建到谓词中
这个表达式将计算 我尚未计划的 愿望清单项目的数量 它会使用愿望清单项目的数组 并返回 符合筛选条件的项目数
现在 我可以用所提供的 行程的 bucketList 项目 作为谓词的一部分 来计算这个表达式的值 然后 我的谓词可以检查 表达式的值 是否大于零 表达式让谓词宏 变成一款富有表现力的强大工具 助你轻松编写查询 高效地获取 App 所需的数据 但还有另一种方法 能够提高这些查询的性能 那就是使用一款 全新的架构宏 #Index 全新的 #Index 宏 增加了在模型中 创建单个索引或复合索引的能力 就像书的目录一样 索引能表示出 SwiftData 生成并存储 在容器中的额外元数据 这种元数据 让针对指定键路径的查询 更快、更高效
要获得这些益处 你需要声明 SwiftData 应该为哪些属性创建索引 考虑那些 在查询的排序和筛选中 最常出现的属性
在“Trips”App 中 行程查询经常是按照名称、 开始日期以及结束日期这些属性 来进行筛选和排序 为了加快这些查询的速度 我可以添加 #Index 宏 并为名称、开始日期和结束日期 指定关键路径 以及这三者的复合索引 对于大型数据集 比如 各种各样的度假打算 这可以显著加快 筛选和排序操作 有了谓词宏 在 SwiftUI 中使用查询变得更轻松 表达式也带来了更强大的功能 有了 #Index 宏 你可以让它们在 App 中 实现更出色的性能
运用 SwiftData 的强大功能 来构建你的 App 模型层 考虑向架构添加 #Unique 约束条件 可以更容易 避免出现重复模型 添加全新的 #Index 宏来加速查询
使用全新的历史记录 API 来跟踪 App 模型的变化 借助自定数据存储 你现在可以通过自己的文稿格式 或持久化后端 来充分利用 SwiftData 的强大功能
谢谢!很荣幸与大家分享这些内容 我们期待看到大家 创造出无限精彩!
-
-
1:32 - SampleTrips models decorated with @Model
// 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 {...}
-
1:43 - SampleTrips using modelContainer scene modifier
// Trip App using modelContainer Scene modifier import SwiftUI import SwiftData @main struct TripsApp: App { var body: some Scene { WindowGroup { ContentView } .modelContainer(for: Trip.self) } }
-
1:53 - SampleTrips using @Query
// Trip App using @Query import SwiftUI import SwiftData struct ContentView: View { @Query var trips: [Trip] var body: some View { NavigationSplitView { List(selection: $selection) { ForEach(trips) { trip in TripListItem(trip: trip) } } } } }
-
2:16 - SampleTrips models decorated with @Model
// 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 {...}
-
3:08 - Add unique constraints to avoid duplication
// 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? }
-
3:36 - Add .preserveValueOnDeletion to capture unique columns
// 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? }
-
4:35 - SampleTrips using modelContainer scene modifier
// Trip App using modelContainer Scene modifier import SwiftUI import SwiftData @main struct TripsApp: App { var body: some Scene { WindowGroup { ContentView() } .modelContainer(for: Trip.self) } }
-
4:52 - Customize a model container in the app
// 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) } }
-
5:13 - Add a model container to the app
// 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) } }
-
5:59 - Use your own custom data store
// 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) } }
-
6:58 - Make preview data using traits
// 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()) }
-
8:15 - Use sample data in a preview
// 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() }
-
8:50 - Create a preview query using @Previewable
// Create a preview query using @Previewable import SwiftUI import SwiftData #Preview(traits: .sampleData) { @Previewable @Query var trips: [Trip] BucketListItemView(trip: trips.first) }
-
9:55 - Create a predicate to find a Trip based on search text
// Create a Predicate to find a Trip based on Search Text let predicate = #Predicate<Trip> { searchText.isEmpty ? true : $0.name.localizedStandardContains(searchText) }
-
10:06 - Create a Compound Predicate to find a Trip based on Search Text
// 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) }
-
10:46 - Build a predicate to find Trips with BucketListItems that are not in the plan
// 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 }
-
12:41 - Add Index for commonly used KeyPaths or combination of KeyPaths
// 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 }
-
-
正在查找特定内容?在上方输入一个主题,就能直接跳转到相应的精彩内容。
提交你查询的内容时出现错误。请检查互联网连接,然后再试一次。