大多数浏览器和
Developer App 均支持流媒体播放。
-
利用 SwiftData 创建自定数据存储
将 SwiftData 富有表现力的声明式建模 API 与你自己的持久化后端完美整合。了解如何构建自定数据存储,并探索如何逐步将持久化功能添加到你的 App 中。 为了充分利用好本次讲座,我们建议你先看一下 WWDC23 中的“认识 SwiftData”和“使用 SwiftData 为你的架构建模”。
章节
- 0:00 - Introduction
- 1:21 - Overview
- 4:50 - Meet DataStore
- 7:42 - Example store
资源
相关视频
WWDC24
-
下载
大家好 我叫 Luvena 很高兴能和大家聊聊 SwiftData 中的自定数据存储 它是一种将 SwiftData 与你自己的 持久化后端结合使用的方法 自定数据存储是 SwiftData 的一项新功能 让你能够使用自己选择的 任何文档、文件格式 或持久化后端 它们能与你现有的所有 SwiftData 代码完美配合
在这里 在 SampleTrips App 的实现中 我只需将 ModelConfiguration 替换为 JSONStoreConfiguration 便可更改存储类型 我将在本视频后面的 内容中实现这个示例 只需替换这一处 ModelContainer 现在就知道 要使用其他存储类型 我无需更改 SampleTrips App 中的 任何模型或视图代码
在本视频中 我将首先介绍 存储在 SwiftData 中所起的作用 以及它如何与 ModelContext 和 ModelContainer 进行交互 然后 我将探讨如何使用 新的 DataStore 协议来构建它们 最后 我将通过展示一个 使用 JSON 文件实现持久化的示例 来介绍实现自定 数据存储的基础知识 概括来讲 存储负责获取 和保存支持 PersistentModel 所需的所有数据 为了探讨自定数据存储 在 SwiftData 中的工作方式 我将分析它们如何为我的 App SampleTrips 带来持久化功能 SampleTrips 在 SwiftData 和 SwiftUI 的强大协同作用基础上构建 一个典型的 App 由三个重要组成部分构成 SwiftUI 提供用户界面 这通常是指视图 例如列表或标签 其中显示来自 ModelContext 的模型数据 ModelContext 使用 ModelContainer 中的存储读写数据 在本视频中 我将专门重点介绍 存储在 SwiftData 中所起的作用 SampleTrips 使用 ModelContext 为视图提供助力并显示行程 ModelContext 为视图中的 每个行程 实例化 PersistentModel 每个行程都有一个 相应的 persistentIdentifier 来唯一标识模型 ModelContext 会追踪 我所做的更改 以便根据需要将这些更改 保存到存储中 例如 如果我决定取消 前往 Los Angeles 的行程 同时添加一个前往 Tokyo 的新行程 ModelContext 就会 跟踪这些更改 当新的 Tokyo 模型 插入到 ModelContext 时 它会被一个临时的 persistentIdentifier 来标识 当 ModelContext 保存时 它会告诉存储 删除 Los Angeles 行程 并插入新的 Tokyo 行程 然后存储将为 Tokyo 模型分配一个 永久 persistentIdentifier Trip-5 并将它映射到 之前的临时标识符 Trip-t1 这一过程 称为“重新映射”
然后 存储会使用 更新后的 Tokyo 行程 persistentIdentifier 来响应 ModelContext
ModelContext 完成 状态更新后 UI 就可以更新 渲染行程的视图 持久化更改是一个例证 说明了 ModelContext 和存储 如何协同工作来为 SwiftData 中的 PersistentModel 提供支持 它们使用一组请求和响应进行通信 这些请求和响应定义 获取或保存等操作 存储的作用是 实现模型值的 持久化方法 这一通信利用模型的可发送、 可编码表示形式 称为 DataStoreSnapshot
在 SampleTrips App 中 视图使用 PersistentModel 与 ModelContext 进行通信 但是 当 ModelContext 需要 与存储进行通信时 它会创建一个快照来保存 模型的当前状态 快照是一个可发送、 可编码的容器 其中包含 在这个时间点模型中的值 与 PersistentModel 一样 每个快照都由 persistentIdentifier 来标识
然后 存储会使用这些快照 将值应用到储存空间 反之亦然 当 ModelContext 从存储中 读取数据时 存储会根据上下文 请求获取的 PersistentModel 创建一组快照
然后 ModelContext 会为每个快照 创建 PersistentModel 用于在视图、查询或上下文 执行的其他操作中使用 存储在 SwiftData 中 起着至关重要的作用 它使 ModelContext 能够在任何 格式的储存空间中读写模型数据 让我带大家了解一下新的 DataStore 协议以及它是如何实现这一点的
存储有三个关键组成部分: 描述存储的配置、 与 ModelContext 通信 来传输模型值的快照 以及 ModelContainer 可以管理的存储实现 这些组成部分 分别遵从三种不同的协议: DataStoreConfiguration、 DataStoreSnapshot 和 DataStore SwiftData 中的默认存储 以自己的方式实现了这些类型: ModelConfiguration、 DefaultSnapshot 和 DefaultStore DefaultStore 支持 SwiftData 的 所有丰富功能 如迁移、 历史记录跟踪和 CloudKit 同步 它封装了平台在性能 和可扩展性方面的最佳做法 这使它成为 PersistentModel 的 最佳默认选择
DataStore 协议定义了 为了使 ModelContext 可以使用存储 SwiftData 需要具备的 所有功能 包括保存、获取和缓存 其他协议定义了 可选的数据存储功能 例如用于描述 对存储所做的所有更改的 新 History 协议
ModelContext 使用 来自 DataStore 协议的 请求和响应 与存储进行通信 例如 当从存储中 获取数据时 ModelContext 会向存储 发送 DataStoreFetchRequest 其中包含描述 存储应检索哪些数据的 FetchDescriptor
存储检索到 模型值后 会为每个模型创建一个快照 并将它们返回 DataStoreFetchResult 中
然后 ModelContext 会为每个快照 创建一个 PersistentModel
在 ModelContext 中 更改模型 并调用 save 时 过程类似 ModelContext 会创建一个 DataStoreSaveChangesRequest 其中包含所有 已修改模型的快照 并将这一请求发送到存储
然后 存储会将快照 应用到储存空间并创建 DataStoreSaveChangesResult 再发送回 ModelContext 在结果中 存储会建立映射 以提供 Trip-t1 等 新插入模型的 重新映射标识符 这会告诉 ModelContext 将所插入 行程的 persistentIdentifier 更新为 Trip-5
最后 ModelContext 会处理 来自存储的保存结果 并更新它的状态 将新的永久 persistentIdentifier 分配给所插入的行程
现在我已经介绍了 数据存储的运行机制 接下来我想探讨一下 实现数据存储的过程是怎样的 我将实现一个存储 它使用 JSON 文件 来持久化 SampleTrips 应用程序中的模型 在开始之前 我想说明两点 这个存储是一个“归档存储” 这意味着在读取或写入时 会载入整个文件 此外 我将使用 Foundation 提供的 JSON 编码器 并将数据存储为 文件中的快照数组
创建存储的第一步是 声明符合 DataStoreConfiguration 和 DataStore 协议的 Configuration 类型 和 Store 类型
这些类型使用 关联类型相互引用 在 Configuration 中 我将 Store 类型设置为 JSONStore 在 Store 中 我将 Configuration 设置为 JSONStoreConfiguration
此外 JSONStore 还声明了 用于与 ModelContext 进行通信的 Snapshot 类型 在这里 我使用 DefaultSnapshot 因为我不需要自定 模型数据的 编码或解码 现在 我可以开始实现 为了使 DataStore 可供 ModelContext 使用 所需的两个方法:fetch 和 save 当 ModelContext 发送 DataStoreFetchRequest 时 我需要载入 存储中的数据 并实例化 DataStoreFetchResult 由于 DefaultSnapshot 是可编码的 因此我可以使用 JSONDecoder 从 configuration 提供的 fileURL 中载入 存储的数据 然后 我将实例化并返回 包含文件快照的 DataStoreFetchResult 目前 这一实现不会处理 FetchDescriptor 中的 predicate 或 sort 比较运算符 转换 predicate 或 sort 比较 运算符可能是一个复杂的过程 我可以使用 ModelContext 来完成这项工作
为此 我将在请求 包含 predicate 或 sort 描述符时 引发‘preferInMemoryFilter’ 和‘preferInMemorySort’错误 这非常适合我的情况 因为这是一个可以载入到 内存中的小型数据集 现在 我实现了完全可以 正常运行的 fetch 它能支持查询和排序 实现了 fetch 后 我就可以实现 save 以将快照写入 JSON 文件 在实现 save 时 我要考虑并处理 3 种类型的更改: 插入、更新和删除 在开始处理 save 请求中的 传入快照之前 我首先要读取 文件的当前内容 这将用我定义的一个单独的方法 来处理 这个方法名为 read 我会将所有快照整理到 一个将 persistentIdentifier 作为键的字典中 这是最后将写入磁盘的 新 JSON 文件的 有效副本
然后 我会在 save 请求中处理 所插入模型的快照 这涉及到为每个插入的快照 分配和重新映射标识符 让我来详细分析一下 这个问题 回想一下 当模型被插入存储中时 每个模型都包含一个未与 任何存储关联的临时标识符 在这里 对于插入的每个快照 我将新建一个永久 persistentIdentifier 然后 我将创建一个使用新 persistentIdentifier 的快照副本 这个新 persistentIdentifier 会 被映射到 remappedIdentifiers 字典中的临时标识符 以便稍后在保存结果中 返回给 ModelContext 最后 我会将插入的快照添加到 最初从文件 载入的快照中
处理完插入的快照后 我会用保存 请求中的快照 替换文件中的快照 从而处理更新 最后 我会在从文件 载入的快照中 将删除的快照移除 现在 我在 snapshotsByIdentifier 字典中 拥有了一组完整的更新数据 我想将这些数据写回文件
我会使用 JSONEncoder 将快照的这个有效副本 写回磁盘上的 单个 JSON 文件中 最后 我会返回一个 包含保存结果的 DataStoreSaveChangesResult DataStoreSaveChangesResult 包括 重新映射的 persistentIdentifier 它们用于更新上下文
现在我拥有了一个 完整的自定数据存储 可以在 SampleTrips 中采用它 在 App 定义中 我只需将 ModelConfiguration 替换为 JSONStoreConfiguration 便可更改存储类型 只需替换这一处 ModelContainer 现在就知道 要使用其他存储类型 我无需更改 SampleTrips App 中的 任何模型或视图代码
有了数据存储 SwiftData 可以在任何格式的 储存空间或 持久化后端中读写数据
这样 你就可以将 SwiftUI 和 PersistentModel 的强大功能 与你所需的任何文档、数据库 或云储存空间结合使用 而 ModelContext 通过为你提供 筛选和排序功能 帮助降低简单存储 实现的复杂性
在 SwiftData 中采用自定存储 非常简单 只需更改 DataStoreConfiguration 即可 而且通过新的 DataStore 协议 你可以实现 对任何持久化后端的支持 这为 SwiftData 开启了新的可能性 务必观看 “SwiftData 的新功能” 了解编制索引和唯一性约束等 其他新功能 不要错过“使用 SwiftData 历史记录 API 跟踪模型更改” 全面了解如何检查 存储的历史记录
感谢大家的参与! 我迫不及待想要看看 你们的构建成果
-
-
8:15 - Implement a JSON store
// Implement a JSON store @available(swift 5.9) @available(macOS 15, iOS 18, tvOS 18, watchOS 11, visionOS 2, *) final class JSONStoreConfiguration: DataStoreConfiguration { typealias StoreType = JSONStore var name: String var schema: Schema? var fileURL: URL init(name: String, schema: Schema? = nil, fileURL: URL) { self.name = name self.schema = schema self.fileURL = fileURL } static func == (lhs: JSONStoreConfiguration, rhs: JSONStoreConfiguration) -> Bool { return lhs.name == rhs.name } func hash(into hasher: inout Hasher) { hasher.combine(name) } } @available(swift 5.9) @available(macOS 15, iOS 18, tvOS 18, watchOS 11, visionOS 2, *) final class JSONStore: DataStore { typealias Configuration = JSONStoreConfiguration typealias Snapshot = DefaultSnapshot var configuration: JSONStoreConfiguration var name: String var schema: Schema var identifier: String init(_ configuration: JSONStoreConfiguration, migrationPlan: (any SchemaMigrationPlan.Type)?) throws { self.configuration = configuration self.name = configuration.name self.schema = configuration.schema! self.identifier = configuration.fileURL.lastPathComponent } func save(_ request: DataStoreSaveChangesRequest<DefaultSnapshot>) throws -> DataStoreSaveChangesResult<DefaultSnapshot> { var remappedIdentifiers = [PersistentIdentifier: PersistentIdentifier]() var serializedTrips = try self.read() for snapshot in request.inserted { let permanentIdentifier = try PersistentIdentifier.identifier(for: identifier, entityName: snapshot.persistentIdentifier.entityName, primaryKey: UUID()) let permanentSnapshot = snapshot.copy(persistentIdentifier: permanentIdentifier) serializedTrips[permanentIdentifier] = permanentSnapshot remappedIdentifiers[snapshot.persistentIdentifier] = permanentIdentifier } for snapshot in request.updated { serializedTrips[snapshot.persistentIdentifier] = snapshot } for snapshot in request.deleted { serializedTrips[snapshot.persistentIdentifier] = nil } try self.write(serializedTrips) return DataStoreSaveChangesResult<DefaultSnapshot>(for: self.identifier, remappedPersistentIdentifiers: remappedIdentifiers, deletedIdentifiers: request.deleted.map({ $0.persistentIdentifier })) } func fetch<T>(_ request: DataStoreFetchRequest<T>) throws -> DataStoreFetchResult<T, DefaultSnapshot> where T : PersistentModel { if request.descriptor.predicate != nil { throw DataStoreError.preferInMemoryFilter } else if request.descriptor.sortBy.count > 0 { throw DataStoreError.preferInMemorySort } let objs = try self.read() let snapshots = objs.values.map({ $0 }) return DataStoreFetchResult(descriptor: request.descriptor, fetchedSnapshots: snapshots, relatedSnapshots: objs) } func read() throws -> [PersistentIdentifier: DefaultSnapshot] { if FileManager.default.fileExists(atPath: configuration.fileURL.path(percentEncoded: false)) { let decoder = JSONDecoder() decoder.dateDecodingStrategy = .iso8601 let trips = try decoder.decode([DefaultSnapshot].self, from: try Data(contentsOf: configuration.fileURL)) var result = [PersistentIdentifier: DefaultSnapshot]() trips.forEach { s in result[s.persistentIdentifier] = s } return result } else { return [:] } } func write(_ trips: [PersistentIdentifier: DefaultSnapshot]) throws { let encoder = JSONEncoder() encoder.dateEncodingStrategy = .iso8601 encoder.outputFormatting = [.prettyPrinted, .sortedKeys] let jsonData = try encoder.encode(trips.values.map({ $0 })) try jsonData.write(to: configuration.fileURL) } }
-
-
正在查找特定内容?在上方输入一个主题,就能直接跳转到相应的精彩内容。
提交你查询的内容时出现错误。请检查互联网连接,然后再试一次。