大多数浏览器和
Developer App 均支持流媒体播放。
-
深入了解 SwiftData
了解 SwiftData 的强大功能在 App 中的应用。了解 ModelContext 和 ModelContainer 如何通过协同工作使 App 数据持久化。我们将向你展示如何进行手动跟踪和修改,以及如何通过 FetchDescriptor、SortDescriptor 和 enumerate 来规模化使用 SwiftData。我们建议你提前观看 WWDC23 的“了解 SwiftData”和“使用 SwiftData 对 Schema 建模”这两讲,以便充分汲取本讲知识。
章节
- 0:00 - Intro
- 3:42 - Configuring persistence
- 7:21 - Track and persist changes
- 11:20 - Modeling at scale
- 14:54 - Wrap-up
资源
相关视频
WWDC23
-
下载
♪ ♪
Nick Gillett:大家好 我是 Nick Gillett 我是 Apple SwiftData 团队的 一名工程师 在本次讲座中 我会详细介绍 使用 SwiftData 建构的 App 如何发展 以充分发挥这一内容丰富、 功能强大的新框架的优势 首先 我将介绍如何 在一款 App 中进行持久化配置 接下来 我将介绍 如何利用 ModelContext 跟踪变更并使其持久化 最后 我将介绍 如何充分利用 SwiftData 帮助你在代码中处理对象 我想指出 本次讲座以 “认识 SwiftData” 和“使用 SwiftData 为你的架构建模” 这两讲中介绍的概念和 API 为基础 我强烈建议大家在观看本讲座之前 先去看看上述两场讲座的内容 在本讲座中 我将引用一个 新的示例 App 名为 SampleTrips 这是我们今年构建的一个 App 以展示使用 SwiftData 构建 App 是多么容易 SampleTrips 让我能够 轻松地整理 我何时想要去旅行 以及想要前往何地 利用 SwiftData 也能使一些 标准平台操作变得更易于实现 比如撤销操作 和在用户切换 App 时自动保存数据 SwiftData 是在使用 Swift 的 App 中进行数据持久化的一种新方式 它旨在与你已经在代码中使用的类型 (如“类”和“结构”)配合使用 这个概念的核心是模型 由新的宏 @Model 描述 该宏告诉 SwiftData 你希望持久化的类型 这是来自 SampleTrips App 的 Trip 类 它具有几项属性 用来捕获与旅行有关的信息 并引用了 SampleTrips 中 使用的其他对象 我们设计 SwiftData 是为了让你原本经常编写的 不具有持久化的代码和你现在要 编写的具有持久化的代码 之间的差异尽可能小 只需少许更改 我可以告诉 SwiftData 这个 Trip 是我要持久化的模型 并描述了它与 BucketListItem 和 LivingAccommodations 行为的关系 如果可能 SwiftData 会自动 从你编写的代码中 推断出你想要使用的结构 但是 SwiftData 还提供了 一组强大的自定义选项 以帮助你准确描述数据的存储方式 你可以在“使用 SwiftData 为你的架构建模”中 了解有关模型强大功能的所有内容 这些注释将使 Trip 类 在 SwiftData 中 发挥两个重要作用 第一个作用是描述 App 的对象图 也称为架构; 第二个作用是 Trip 类将作为 我可以编写代码的接口 这种双重性 即能够同时发挥这两种作用 使得带有 @Model 宏注释的类 成为使用 SwiftData 的 App 中的联系中心 而且有一个统一的 API 概念来支持每种作用
架构应用到 名为 ModelContainer 的类 以描述数据持久化的方式 ModelContainer 会分析架构 以生成一个能够容纳 模型类实例的数据库 在代码中处理模型类的实例时 这些实例会关联到 一个 ModelContext 后者跟踪和管理 它们在内存中的状态 这种双重性 是 SwiftData 的核心 本部分中 我将详细介绍 该模型的第一个作用 即描述持久性结构 以及它是如何 与 ModelContainer 配合工作的 ModelContainer 用于描述数据如何 在一台设备上存储或持久化 我们可以将 ModelContainer 视为 架构和其持久化之间的桥梁 它可以用于描述对象的存储方式 比如数据是存储在 内存还是磁盘上 以及该存储的操作和演化语义 (如版本控制、迁移和图分离) 相结合 使用架构实例化一个容器很容易 我只需提供我想要使用的类型 SwiftData 将为我解析出 其余的架构 例如 因为 Trip 类 与其他模型类型相关联 ModelContainer 实际上 会推断出这个架构 即使我只传递了一个单一的类型 ModelContainer 拥有 其他几种强大的初始化方法 旨在与你的代码一起发展 以使用一个名为 ModelConfiguration 的类 来实现越来越复杂的配置 ModelConfiguration 描述了架构的持久性 它控制数据存储的位置 例如 将数据存储在内存中以供临时使用 或将数据存储在磁盘上 以供持久化使用 ModelConfiguration 可以使用你选择的特定文件 URL 也可以根据你的 App 的授权自动生成一个 URL 例如组容器授权 配置还可以描述一个持久化文件 应该以只读模式加载 防止对敏感或模板数据进行写入 最后 使用多个 CloudKit 容器的 App 可以将其指定为 ModelConfiguration 的一部分 用于架构 假设我想通过 新的 Person 和 Address 类 向 SampleTrips 添加一些联系信息 首先 声明包含我要使用的 所有类型的完整架构 接下来 为 SampleTrips 数据声明一个配置 其中包含 Trip、BucketListItem 和 LivingAccommodations 模型 它声明了一个 URL 指向用于存储这个 特定对象图数据的文件 以及一个容器标识符 指向在将 SampleTrips 数据同步到 CloudKit 时 使用的 CloudKit 容器 然后 在其自己的配置中 声明新架构的模型 其中包括 Person 和 Address 并使用唯一的文件 URL 和 CloudKit 容器 标识符将这些数据与 Trips 图分开 最后 将架构和配置组合在一起 形成 ModelContainer
借助 ModelConfiguration 的功能 可以轻松描述 App 的持久化需求 无论其多么复杂 除了手动实例化容器之外 SwiftUI App 还可以使用 新的 ModelContainer 修饰符 来创建他们想要使用的容器 ModelContainer 修饰符可以 添加到 App 中的任何视图或场景中 支持从简单至强大的 各类 ModelContainer 在本部分中 我讨论了 如何使用 ModelContainer 将架构与持久化结合起来 随着你构建的功能和对象图的增强 它会随着你的 App 而成长 我还演示了 如何使用 ModelConfiguration 来解锁强大的持久化功能 正如我们在 “了解 SwiftData”中所学到的 模型和 ModelContext 是编写用户界面 或对模型对象进行操作时 最常用的两个概念 在本部分中 我将深入讲解 ModelContext 如何通过 ModelContainer 跟踪更改 并持久保存编辑 当我们在视图或场景代码中 使用 modelContainer 修饰符时 它会以特定的方式 准备 App 的环境 该修饰符将新的 modelContext 键 绑定到容器的 mainContext mainContext 是一个 专门用于在场景和视图中 处理 ModelObjects 的 特殊 MainActor 对齐的 ModelContext 通过使用环境中的 ModelContext 视图代码可以轻松访问 此处的查询所使用的上下文 从而可以执行诸如删除等操作 因此 ModelContext 易于使用和访问 但它们实际上是做什么呢? 我们可以将 ModelContext 视为 App 管理的数据的视图 我们想要处理的数据 会在使用时被获取到 ModelContext 中 在 SampleTrips 中 当即将到来的 旅行视图加载列表的数据时 每个旅行对象 都会被获取到 MainContext 中 如果对旅行进行了编辑 ModelContext 会将该更改记录为快照 当进行其他更改 比如插入新的旅行 或删除现有旅行时 上下文会跟踪和维护 有关这些更改的状态 直到调用“context.save()” 这意味着即使已删除的 旅行在列表中不再可见 它仍然存在于 ModelContext 中 直到调用 save 将删除操作持久化
一旦调用 save 上下文 会将更改持久化到 ModelContainer 并清除其状态 如果你仍在上下文中引用了对象 比如在列表中显示 它们会一直存在于上下文中 直到你完成使用它们为止 此时 它们将被释放 且上下文会被清空 ModelContext 与其绑定的 ModelContainer 协同工作 它跟踪你在视图中获取的对象 然后在执行保存时传递变更 ModelContext 还支持 回滚或重置 以在需要时清除其缓存状态 这使其适合执行撤销 和自动保存等平台功能 在 SwiftUI App 中 modelContainer 修饰符 具有 isUndoEnabled 参数 它将窗口的 undoManager 绑定到容器的 mainContext 这意味着当在 MainContext 中进行更改时 可以使用三指轻扫和摇动等系统手势 无需额外代码即可撤销或恢复更改 ModelContext 会自动 在对模型对象进行更改时 注册撤销和恢复操作 ModelContainer 修饰符 使用环境中的 undoManager 通常由系统提供 作为窗口或窗口组的一部分 因此 像三指滑动 和摇动这样的系统手势 将自动在你的 App 中生效 ModelContext 支持的另一个 标准系统功能是自动保存 当启用自动保存时 ModelContext 将在 App 进入前台或后台 等系统事件发生时进行保存 在 App 使用过程中 MainContext 还会定期进行保存 自动保存在 App 中默认是启用的 如果需要的话 可以使用 modelContainer 修饰符的 isAutosaveEnabled 参数禁用它 手动创建的 ModelContext 默认禁用自动保存 在“认识 SwiftData”中 我们学习了 如何在 App 中 使用 ModelContext 以及它如何 与 SwiftUI 完美配合 但用户界面并不是 App 需要处理模型对象的唯一场所 在本部分中 我将介绍 SwiftData 如何使编写强大、可扩展的 代码变得更加简单和安全 在后台队列上处理数据、 与远程服务器 或其他持久化机制同步 以及批处理等任务 都需要使用模型对象 通常是集合或图形 其中许多任务都将通过 在 ModelContext 上 使用抓取方法 来获取一组要处理的对象 在此示例中 Trip 模型的 FetchDescriptor 告诉 Swift trips 数组 将是 Trip 对象的集合 无需进行强制类型转换 或处理复杂的结果元组
FetchDescriptor 使得使用新的 Predicate 宏 来构建复杂查询变得简单 例如 哪些旅行当中 需要入住某家特定酒店? 或者哪些旅行中 还需要预订一些活动? SwiftData 支持 子查询和连接等复杂查询 都可以使用纯 Swift 来编写 Predicate 使用你创建的模型 而 SwiftData 使用 从这些模型生成的架构 将这些谓词转换为数据库查询 FetchDescriptor 结合了新的 Foundation Predicate 宏的 强大功能和架构 为 Apple 平台上的持久化 首次提供了 经编译器验证的查询功能 FetchDescriptor 和相关的类 如 SortDescriptor 使用泛型来形成结果类型 并告诉编译器可以使用模型的属性 你已经熟悉并喜爱的许多调优选项 如 offset 和 limit 以及用于 faulting 和 prefetching 的参数
所有这些功能在 ModelContext 的 新 enumerate 函数中结合起来 它旨在通过在单个调用点 封装平台最佳实践 帮助使批量遍历和枚举变得高效 无论 FetchDescriptors 的 复杂性如何 无论其简单还是强大 Enumerate 都可以很好地工作 Enumerate 自动实现了 批量遍历和变更保护 等遍历的平台最佳实践 这些可以根据 你的特定用例进行定制 例如 Enumerate 使用的 批处理对象规模默认为 5000 个 但我可以将其扩增为 10000 个 以减少遍历过程中的 I/O 操作 代价是占用更多内存 图像、视频或其他大型数据块 等较大的对象图 可能选择较小的批处理规模 减小批处理规模可以减少内存增加 同时在枚举过程中 增加 I/O 操作 Enumerate 还默认包含变更保护功能 在大型遍历中导致 性能问题的最常见原因之一是 在枚举过程中 被困在上下文中的变更操作 allowEscapingMutations 告诉 enumerate 这是有意的 若未进行设定 那么如果发现执行枚举的 ModelContext 是脏的(即存在未保存的变更) enumerate 将抛出异常 阻止释放已经遍历过的对象 在本讲座中 我们学习了如何使用 架构和 ModelConfiguration 创建强大的持久化配置 我们还学习了使用 ModelContainer 和 ModelContext 可以轻松采用 撤销和恢复等标准系统实践 而且 使用 FetchDescriptor、 Predicate 和 Enumerate 你可以立即在项目中编写 安全、高性能的代码 我迫不及待地想看到你 在未来的年月中 如何拓展这个新框架的边界 感谢观看 祝你编码愉快
-
-
1:45 - Trip model with cascading relationships
@Model final class Trip { var destination: String? var end_date: Date? var name: String? var start_date: Date? @Relationship(.cascade) var bucketListItem: [BucketListItem] = [BucketListItem]() @Relationship(.cascade) var livingAccommodation: LivingAccommodation? }
-
4:21 - Initializing a ModelContainer
// ModelContainer initialized with just Trip let container = try ModelContainer(for: Trip.self) // SwiftData infers related model classes as well let container = try ModelContainer( for: [ Trip.self, BucketListItem.self, LivingAccommodation.self ] )
-
5:41 - Using ModelConfiguration to customize ModelContainer
let fullSchema = Schema([ Trip.self, BucketListItem.self, LivingAccommodations.self, Person.self, Address.self ]) let trips = ModelConfiguration( schema: Schema([ Trip.self, BucketListItem.self, LivingAccommodations.self ]), url: URL(filePath: "/path/to/trip.store"), cloudKitContainerIdentifier: "com.example.trips" ) let people = ModelConfiguration( schema: Schema([Person.self, Address.self]), url: URL(filePath: "/path/to/people.store"), cloudKitContainerIdentifier: "com.example.people" ) let container = try ModelContainer(for: fullSchema, trips, people)
-
6:49 - Creating ModelContainer in SwiftUI
@main struct TripsApp: App { let fullSchema = Schema([ Trip.self, BucketListItem.self, LivingAccommodations.self, Person.self, Address.self ]) let trips = ModelConfiguration( schema: Schema([ Trip.self, BucketListItem.self, LivingAccommodations.self ]), url: URL(filePath: "/path/to/trip.store"), cloudKitContainerIdentifier: "com.example.trips" ) let people = ModelConfiguration( schema: Schema([ Person.self, Address.self ]), url: URL(filePath: "/path/to/people.store"), cloudKitContainerIdentifier: "com.example.people" ) let container = try ModelContainer(for: fullSchema, trips, people) var body: some Scene { WindowGroup { ContentView() } .modelContainer(container) } }
-
7:40 - Using the modelContainer modifier
@main struct TripsApp: App { var body: some Scene { WindowGroup { ContentView() } .modelContainer(for: Trip.self) } }
-
7:50 - Referencing a ModelContext in SwiftUI views
struct ContentView: View { @Query var trips: [Trip] @Environment(\.modelContext) var modelContext var body: some View { NavigationStack (path: $path) { List(selection: $selection) { ForEach(trips) { trip in TripListItem(trip: trip) .swipeActions(edge: .trailing) { Button(role: .destructive) { modelContext.delete(trip) } label: { Label("Delete", systemImage: "trash") } } } .onDelete(perform: deleteTrips(at:)) } } } }
-
9:57 - Enabling undo on a ModelContainer
@main struct TripsApp: App { @Environment(\.undoManager) var undoManager var body: some Scene { WindowGroup { ContentView() } .modelContainer(for: Trip.self, isUndoEnabled: true) } }
-
11:05 - Enabling autosave on a ModelContainer
@main struct TripsApp: App { var body: some Scene { WindowGroup { ContentView() } .modelContainer(for: Trip.self, isAutosaveEnabled: false) } }
-
11:54 - Fetching objects with FetchDescriptor
let context = self.newSwiftContext(from: Trip.self) var trips = try context.fetch(FetchDescriptor<Trip>())
-
12:14 - Fetching objects with #Predicate and FetchDescriptor
let context = self.newSwiftContext(from: Trip.self) let hotelNames = ["First", "Second", "Third"] var predicate = #Predicate<Trip> { trip in trip.livingAccommodations.filter { hotelNames.contains($0.placeName) }.count > 0 } var descriptor = FetchDescriptor(predicate: predicate) var trips = try context.fetch(descriptor)
-
12:27 - Fetching objects with #Predicate and FetchDescriptor
let context = self.newSwiftContext(from: Trip.self) predicate = #Predicate<Trip> { trip in trip.livingAccommodations.filter { $0.hasReservation == false }.count > 0 } descriptor = FetchDescriptor(predicate: predicate) var trips = try context.fetch(descriptor)
-
13:18 - Enumerating objects with FetchDescriptor
context.enumerate(FetchDescriptor<Trip>()) { trip in // Operate on trip }
-
13:36 - Enumerating with FetchDescriptor and SortDescriptor
let predicate = #Predicate<Trip> { trip in trip.bucketListItem.filter { $0.hasReservation == false }.count > 0 } let descriptor = FetchDescriptor(predicate: predicate) descriptor.sortBy = [SortDescriptor(\.start_date)] context.enumerate(descriptor) { trip in // Remind me to make reservations for trip }
-
14:01 - Fine tuning enumerate with batchSize
let predicate = #Predicate<Trip> { trip in trip.bucketListItem.filter { $0.hasReservation == false }.count > 0 } let descriptor = FetchDescriptor(predicate: predicate) descriptor.sortBy = [SortDescriptor(\.start_date)] context.enumerate( descriptor, batchSize: 10000 ) { trip in // Remind me to make reservations for trip }
-
14:28 - Fine tuning enumerate with batchSize and allowEscapingMutations
let predicate = #Predicate<Trip> { trip in trip.bucketListItem.filter { $0.hasReservation == false }.count > 0 } let descriptor = FetchDescriptor(predicate: predicate) descriptor.sortBy = [SortDescriptor(\.start_date)] context.enumerate( descriptor, batchSize: 500, allowEscapingMutations: true ) { trip in // Remind me to make reservations for trip }
-
-
正在查找特定内容?在上方输入一个主题,就能直接跳转到相应的精彩内容。
提交你查询的内容时出现错误。请检查互联网连接,然后再试一次。