大多数浏览器和
Developer App 均支持流媒体播放。
-
CloudKit 中的新功能
CloudKit 为您的 app 提供安全、方便且可靠的云数据库,它也正在不断完善。探索如何在 async/await 功能和便利 API 添加支持下理顺您的线程。我们还将展示如何通过分享整个数据记录区来鼓励 app 使用者之间进行协作,探索如何采用加密值等 CloudKit 功能,并帮助保护 app 内的敏感数据。为了充分了解本节内容,我们建议您熟悉 CloudKit 及其容器操作,并基本了解记录和数据类型。
资源
相关视频
WWDC23
WWDC22
Tech Talks
WWDC21
-
下载
尼哈尔:大家好 欢迎来到 《CloudKit的新功能》 我的名字是尼哈尔夏尔马 我是CloudKit团队的一名工程师 我的同事倩稍后会加入我 首先 我们将重点介绍 利用Swift并行性 对CloudKit API进行的一些更改 然后 倩将带我们了解 如何在记录上使用加密字段
最后 我们将深入研究一项新功能 可让你轻松共享记录区域
首先 我们有CloudKit 和Swift并行性 作为背景介绍 CloudKit是一个框架 可让你的app 访问iCloud上的数据库 这在API中公开为CKContainer 你可以通过它访问多个CKDatabase
每个容器都有一个公共数据库 所有用户都可以 在其中读取和写入记录 如果设备有一个 已登录的iCloud账户 那么你的app还可以访问 包含该用户数据的私人数据库 如果你的app支持共享 那么共享给 当前iCloud用户的数据 将在共享的CKDatabase中 对你的app可用 在针对CloudKit编写代码时 API有两个方面 首先 CKContainer和CKDatabase上 可用的函数 此API对CloudKit的新用户很有用 旨在提供较低的进入门槛 该框架不会为你提供 所有可用的配置 而是选择一种默认行为 该行为最适合用户 交互的UI app 接下来是操作API 它被公开为一组 NSOperation子类 这个API提供了许多 作为CKContainer 或CKDatabase函数不可用的功能
这包括在到服务器的单次往返中 发送和接收一批项目 通过从服务器增量 获取大型结果集来分页 从过去的某个时间点 向服务器请求数据库 和记录区域更改 最后 将不同的操作组合在一起 这允许将它们作为一个单元进行记录 并允许你通知系统 你的操作跨工作量大小 许多开发人员在编写 生产质量的代码时 最终会使用此API 利用新的Swift并行性功能 CloudKit进行了多项改进 首先 我将介绍如何 在使用CloudKit API的同时 使用新的Swift async/await功能 然后 我将讨论有助于澄清 每项回调和每操作回调 之间区别的新API 以及CloudKit 如何利用Swift.Result类型 来澄清参数对这些回调的作用 最后 我将介绍我们 对容器和数据库函数功能 所做的改进 这些改进有助于带来一些 以前只能通过操作API 获得的功能和可配置性
CloudKit API 为容器和数据库上的函数 引入async变体 你可以使用async函数 来改进处理并行性的代码 它有助于使错误处理更加自然 并简化代码中的可视化控制流程 有关async函数的更多详细信息 请参阅《在Swift中 认识async/await》课程 我们来看个例子
此代码片段取自 PrivateDatabase代码示范 这是Apple最近发布到GitHub的 几个特定于CloudKit的代码示范之一 你可以使用它们 这个特定的函数想要从服务器中 删除一条记录 并在完成后通知调用者
请注意 这里有许多 可选的条件和条件展开 当你第一次尝试理解这个函数时 控制流并不是很明显
现在 让我们将其与 使用CloudKit的async函数的代码 进行比较 在这里 可选和展开 已经被消除 控制流是线性的 更容易遵循
我很高兴地说 我们在GitHub存储库上的 每个代码示范都有更新 这些更新示范了如何 通过类似的重构代码 来使用Swift并行性
让我们来谈谈每个项目的回调 例如 这里有一个 CKFetchRecordsOperation 向服务器发送四个CKRecord.ID 以理想地获得四个CKRecord内容 此操作可以采用三种不同方式
在第一种情况下 操作成功 没有错误 你的记录已成功 从服务器获取
第二种可能 是你遇到了操作范围内的错误 这是导致整个操作失败的错误 例如 设备可能缺少网络连接 在这种情况下 整个操作将失败 并显示错误代码 networkUnavailable
这是第三种可能 在这种情况下 你的操作 已成功往返于服务器 服务器已成功返回 三个请求的CKRecords 同时出现一个错误 表示第四个请求的记录不存在 在这个例子中 每个项目的错误 是unknownItem 它被绑定到 每个操作的错误 partialFailure中 那么 这在代码中是如何处理的呢? 在顶部 CKFetchRecordsOperation 声明了它的 perRecordCompletionBlock 和每个操作完成块 并在底部的每个操作完成块 示范实现
请注意这两个回调之间的重叠 从之前的缺失记录示范来看 该代码预计会出现两次每项错误 一次是每项回调中的 顶级unknownItem错误 另一次是每操作回调中的 partialFailure错误 类似地 对于成功获取的记录 它也期望每个项目 在两个地方获得成功 首先 作为每个项目回调的顶级参数 再一次 包装在操作回调成功结果的 recordsByRecordID字典中
通过利用Swift.Result类型 CloudKit替换了 这两个回调使API更加清晰
请注意新的基于结果的回调中 块参数的顶层分离 perRecordResultBlock有一个ID 标识 CloudKit正在回调的项目 它有每个项目的结果 结果现在是强类型的 因此你知道你得到的 要么是成功获取的CKRecord内容 要么是每个项目的错误
同样的 操作范围的完成块 也已更新为操作范围的结果块 该结果块不再重复每个项目结果块 已报告的任何成功或失败
所以 CloudKit现在正式分清了 他们的顾虑 一个块专门用于每个项目的报告 另一个专门用于每个操作的报告
回到我们缺少的记录示范 期望是三次对每项 结果块的调用 成功获取CKRecord内容 一次对每个项目结果块的调用 带有一个unknownItem错误 一次对每操作结果块的调用 没有错误 因为操作整体上是成功的 CloudKit的一项新改进 是在各处显示单独的每项 和每操作回调 以前 只有突出显示的操作 具有显示每项错误的每项回调
我很高兴地宣布所有CKOperations 现在都公开了每个项目的回调 这些回调会在适当的时候 传回每个项目的错误 现在 让我们来看看我们对容器 和数据库API所做的一些增强和扩展 增强的形式是CKContainer 和CKDatabase上的新函数
这些新函数一起 使大量CKOperation API 可用作CKContainer 和CKDatabase上的函数 重要的是 这不是操作API的 一对一映射 相反 我们利用默认参数 和Swift.Result类型 来制作一个易于使用 功能强大且可与async/await 配合使用的API 也就是说 每个新函数都会公开两次 一次是使用completionHandler 另一次是作为async函数 有了这个增强的API 容器和数据库上的函数 现在支持OperationAPI中的 一些功能 例如批处理多个项目 对大型数据集进行分页以及获取更改 你还可以将函数调用组合在一起 进行日志记录 并通知系统 有关组合工作负载的大小
现在还可以配置函数调用 例如设置超时 那么 这是怎么做到的呢?
这里 同样 是我们之前看到的 GitHubPrivateDatabase代码示范 使用async函数删除记录
让我们看看如何更新此代码 来利用项目批处理 我将更改此函数的行为 以自动地删除两条记录 通过利用数据库上 增强的函数API
注意关注点的分离 突出显示的区域在功能范围内执行 它们启动函数并捕获 抛出的任何函数范围的错误
如果函数成功完成 这个突出显示的区域将检查 每个项目的成功或失败 我们在GitHub上的代码示范存储库中 提供了涵盖所有这些功能的类似示范 本次课程的注释 有包含到这些存储库的连结 我们希望它们对你有所帮助 接着 我想把它交给倩 她会带我们了解加密字段 谢谢尼哈尔 我是倩 我将谈论 CloudKit中的一项新功能 可以非常容易地 保护用户的数据隐私 为此 我将首先概述CloudKit如何 保护你的用户数据 然后我将介绍新的数据加密功能 最后介绍用户账户加密的 一些先决条件 在Apple 隐私是我们所有产品的 核心价值观之一 作为支持许多Apple app 和服务的框架 CloudKit一直在不断创新隐私技术 为存储和与CloudKit同步的 数据提供保护 首先 让我回顾一下CloudKit 是如何保护你的用户数据 CloudKit的方法 包括两种主要的数据保护方法 基于账户的保护和加密保护
使用CloudKit存储的任何数据 默认情况下 都受到基于账户的身份验证保护 这包括你的CloudKit支持的app 和所有Apple CloudKit支持的app 在存储和检索时 CloudKit使用安全令牌 来强制只有授权用户 才能访问他们的数据 而不是Apple或任何第三方
提醒一下 基于账户的保护 只覆盖私有和共享数据库中的数据 在这些数据库中 数据属于 或共享给特定的iCloud账户 访问共享数据需要身份验证 但是 在公共数据库中 所有用户都可以访问数据 因此 在默认情况下 基于账户的数据保护并不适用
现在 我们来看看 另一种数据保护技术 加密保护 CloudKit为存储在Apple app 和服务中的敏感数据 以及以CKAsset形式存储的 所有用户数据提供加密保护 这些数据在发送到 CloudKit服务器进行存储前 会在本地进行预处理和加密 并在检索时在本地解密
此加密功能使用存储在 iCloud钥匙圈中的密钥材料 该密钥材料属于设备上 登入的iCloud账户 同时兼容 CloudKit的共享功能 确保只有CKShare上的用户 才能解密相关的加密字段
加密保护在基于账户的 保护之上增加了另一层 因为即使未经授权的一方以某种方式 绕过授权 他们也无法解密检索到的数据
应该对用户敏感或私有的数据 使用加密保护 Apple的许多CloudKit 支持的app都利用了此功能 例如照片和笔记
到目前为止 CloudKit对用户非资产数据的 保护默认提供基于账户的保护 CloudKit现在提供加密保护 除了让你免于所有密钥派生、管理 和加密/解密过程 这将帮助你构建具有比以往任何时候 都更强大的隐私承诺的 CloudKit支持的app
让我们看看新的API 怎么帮助你做到这一点 你可以在CKRecords上的新属性 encryptedValues中 添加任何密钥值以进行加密 还可以在相同的属性中 获取解密后的原始值
我将介绍encryptedValuesAPI 如何使你能够通过 CloudKit服务器同步加密数据 这里有两台设备 和一台CloudKit服务器 如果设置了 encryptedValues密钥值对 CloudKit会自动 将本地 CKModifyRecordsOperation中的 记录值加密到服务器 在另一台设备上 从服务器 检索记录后 可以调用相同的API CloudKit将自动打开密钥值对
你只需要最少的代码 就可以实现这个过程 在第一台设备上 使用encryptedValues API 可以设置记录上的密钥值对 在本例中 密钥是 “encryptedStringField” 值是一个字符串对象 之后 你可以调用 CKModifyRecordsOperation 将新记录保存到服务器里
在第二个设备上 你可以调用 CKFetchRecordsOperation 来检索加密记录 并且通过使用相同的 encryptedValues属性 取回字符串 就是这样 一个简单的属性为你处理所有加密 和解密过程 并且你可以加密几乎所有的 CKRecord值类型 除了CKReference 因为它们需要对服务器可见 请注意 由于CKAssets字段 如前面提到的 在默认情况下已经采用了加密 它们不能设置为encryptedValue
你可以通过进入 CloudKit数据库模式 来可视化加密字段 就像常规字段一样 有一个CloudKit控制台课程 《认识CloudKit控制台》 向你演示了 对控制台所做的其他更改 你可以随时查看 在控制台中 所有加密字段 都将显示在记录值 数据类型的下拉列表中
它们将有前缀“加密” 例如 “加密双倍”、“加密时间戳” 以帮助你将它们与未加密的区别开来 你还可以直接通过CloudKit控制台 管理加密字段 无需任何代码更改 例如 你可以将一个新的加密字段 添加到开发数据库架构中的 新纪录类型中
接下来涉及 加密操作的账户的先决条件 与私有和共享数据库中的 其他操作一样 它们需要一个有效的登录账户 你需要在初始化逻辑中 通过使用completionHandler 和CKContaineraccountStatus 来检查当前账户的状态
提醒一下 私有和共享数据库中的操作 状态必须是“可用”
任何其他状态都将导致 “CKErrorNotAuthenticated” 包括 今年引入的新状态临时不可用 以表示 账户已登入但尚未准备好 你可以在“设置”app中指导用户 验证他们的凭据
如果你的用户账户 不是在“可用”状态 你应该监听 CKAccountChanged通知 该通知在账户更改时发布 以便在状态准备就绪时收到通知
这就是你该知道的关于 使用CloudKit 加密数据的全部内容 它将保护你用户的数据 并节省你实施自己的自定义 解决方案的所有时间和精力 现在 让我出色的同事尼哈尔 来谈谈区域共享 谢谢 倩 我们来聊聊CloudKit分享
CloudKit是你安全 注重隐私的iCloud云端数据库 可帮助你在所有用户的设备上 存储和同步用户数据 iOS 10和macOS Sierra 引入了CloudKit共享 这是一种与其他iCloud用户 安全共享数据的方式 在深入探讨共享方面的新功能之前 让我们仔细看看 CloudKit共享是如何运作的
提醒一下 CloudKit共享是通过创建 CKShare对象来启动的 该对象将共享的数据 与共享相关的详细信息分开来 例如与谁共享数据 这些共享参与者拥有什么权限等等 在后台 CloudKit还为参与者建立了 对共享数据的加密访问 并要求对所有请求进行 基于账户的身份验证
现在 你可以通过两种主要方式 在app中添加共享支持 你可以使用系统提供的 UI进行共享管理 快速上手 在iOS上通过 UICloudSharingController提供 在macOS上通过NSSharingService 提供 或者 你还可以构建自己的自定义UI 让用户使用这些框架操作 与共享设置进行交互
就像我之前提到的 CKShares 将共享的内容与共享的对象分开 今天 我们将专注于这个等式的 前半部分 更具体地说 看看几种不同的数据建模方式 以及它们如何影响你 利用CloudKit共享API的方式
让我们从一个利用现有CloudKit 共享功能的示范开始 iCloud Drive文件夹共享 建立在CloudKit之上 让我们看看如何在自己的app中 构建类似的东西 因此 这里的数据模型 代表一个文件系统层次结构 因此你将从“文件”和“文件夹” 类型的记录开始 并且你希望让用户 能够轻松地共享任何文件夹记录 以及包含在其中的所有记录 文件或文件夹
在CloudKit中表示这种层次关系 并利用它进行共享的方法 是使用CKRecord.parent 从子记录到父记录的引用
这使得CloudKit 将结果层次结构视为单个可共享单元 因此你需要继续在此处添加这些引用 这非常重要 也是CloudKit中父引用的特殊之处 请注意 如果你不打算支持共享 就不需要使用父引用 你自己架构中的任何普通 CKReference字段就足够了
通过该设置 现在只需初始化 CKShare即可支持文件夹共享 并将文件夹记录 作为CKShare的根记录
使用文件夹作为根记录意味着 CloudKit将自动共享 所有记录 这些记录是最终指向 该文件夹记录 基于父引用的层次结构的一部分 这也意味着以后 从该层次结构中添加或删除的记录 将分别自动共享或取消共享 那么这个简单的文件夹共享模型 是如何在代码中设置的呢?
继续我们的示范 这里有两个文件记录 以及文件夹记录 要在 私有数据库的自定义区域中共享
首先 在指向文件夹记录的两个 文件记录上设置父引用 然后保存文件记录 请注意 最好尽早 保存父引用 以便在共享文件夹时最小化 需要修改的记录数量
然后 通过以我们的文件夹初始化 作为根记录的CKShare 并将CKShare与文件夹记录 一起保存到私有数据库中 从而共享所有三个记录 请注意 由于父引用先前 已保存到服务器 所以在共享时 只需要修改根文件夹记录
就这样 你的app现在正在共享文件夹记录 及其下面的记录 CloudKit可以支持同一区域内的 多个CKShare 只要它们的记录层次结构不重叠
现在 假设我们不是分层的文件夹 共享模型 而是在你的区域中 有代表一些不同类型的记录 并且它们之间没有逻辑层次结构
换句话说 该区域被视为 记录桶 你希望快速 开始共享其中的所有记录
理想情况下 你只需能够将整个记录区域标记为 “共享” 而无需操作其中的任何记录
现在 通过区域共享 你就可以做到这一点 让我们用代码来建立它
你需要做的就是使用新的 CKShare初始化程序 该初始化程序接受 私有数据库中现有区域的 记录区域ID 一旦保存了这个新的区域范围 共享记录 服务器上该区域中存在的所有记录 都会自动共享 共享新纪录 或取消共享记录只需从该区域中 添加或删除这些记录即可 通过删除区域范围的共享记录 可以随时取消共享整个记录区域 让我们更深入地研究一下 这些新的区域共享记录
为了方便起见 区域范围的共享记录 始终具有一个众所周知的记录名称 CKRecordNameZoneWideShare 它可以与区域ID一起使用 以创建完整的共享记录ID
使用区域共享的区域 不需要在该区域中的 记录之间设置任何父引用
请注意 由于区域共享 只允许每个区域有一个共享记录 因此这种共享方式不能 与同一区域内的分层共享共存 因此 你可以在一个区域中 有一个或多个分层共享 或者只有单个区域范围的共享记录
你可以在任何非默认记录区域中 保存区域范围的共享 这些区域 也被标记为一个新的区域功能 CKRecordZoneCapability ZoneWideSharing
CKShare记录创建之后的 所有现有CloudKit 共享机制都保持不变 并且完全支持 区域范围的共享 只有一个例外 由于在使用区域共享时 不再有任何根记录 CKShareMetadata上的相关属性 如hierarchicalRootRecordID 和rootRecord 在接受区域共享时将为零
同样的 当使用 CKFetchShareMetadataOperation 来引导一个自定义共享接受流程时 当获取区域范围共享的 元数据时 系统会忽略 “shouldFetchRootRecord” 和“rootRecordDesiredKeys”
因此 现在有两种 CloudKit共享可用 具体取决于你的数据模型 如果你app的架构在逻辑上 形成层次结构 层次结构树作为可共享单元 是有意义的 那么继续使用CKRecord父引用 来表示 这些层次结构 然后共享它们的根记录 在Apple 我们为备忘录、提醒事项 和iCloud Drive文件夹共享 采用类似的方式
对于所有其他情况 现在即可高效地共享 整个记录区域 现在只需创建 单个区域范围的共享记录 并充分利用CloudKit共享 在Apple 我们已经利用了区域共享 来实现了几个功能 例如HomeKit安全视频共享 和HomePod多用户功能
所以今天我们探讨了 如何使用Swift中的async/await 以一种新的方式 开始编写CloudKit代码 包括对每项进度 和错误报告API的增强
我们讨论了如何利用记录中的 加密字段获取敏感用户数据 充分利用Apple对用户隐私的承诺 而无需自行加密
我们还了解了一种在数据模型 没有分区共享时 可以更快地开始 使用CloudKit共享的方法
developer.apple.com上 有一些关于这些功能的很棒的新文档 你可以在网站上找到更多信息 你可以去看看 在《探索CloudKit》集合中 也有许多相关的课程 供你查看 包括来自Core Data的课程 介绍了构建在CloudKit之上的 共享功能 谢谢你 希望你有个很棒的WWDC [欢乐的音乐]
-
-
3:34 - CloudKit: Existing convenience API
// Sample code using existing Convenience API /// Delete the last person record. /// - Parameter completionHandler: An optional handler to process completion `success` or `failure`. func deleteLastPerson(completionHandler: ((Result<Void, Error>) -> Void)? = nil) { database.delete(withRecordID: lastPersonRecordId) { recordId, error in if let recordId = recordId { os_log("Record with ID \(recordId.recordName) was deleted.") } if let error = error { self.reportError(error) // If there is a completion handler, pass along the error here. completionHandler?(.failure(error)) } else { // If there is a completion handler, like during tests, call it back now. completionHandler?(.success(())) } } }
-
4:04 - CloudKit: Async convenience API
// Sample code updated to CloudKit Async API /// Delete the last person record. func deleteLastPerson() async throws { do { let recordId = try await database.deleteRecord(with: lastPersonRecordId) os_log("Record with ID \(recordId.recordName) was deleted.") } catch { self.reportError(error) throw error } }
-
5:39 - CloudKit: Existing completion blocks
// Error reporting in CKFetchRecordsOperation extension CKFetchRecordsOperation { var perRecordCompletionBlock: ((CKRecord?, CKRecord.ID?, Error?) -> Void)? var fetchRecordsCompletionBlock: (([CKRecord.ID : CKRecord]?, Error?) -> Void)? } fetchRecordsOp.perRecordCompletionBlock = { record, recordID, error in // error is CKError.unknownItem. } fetchRecordsOp.fetchRecordsCompletionBlock = { recordsByRecordID, operationError in // operationError is CKError.partialFailure. // operationError.partialErrorsByItemID[missingRecordID] is CKError.unknownItem. }
-
6:35 - CloudKit: Result type completion blocks
// Error reporting in CKFetchRecordsOperation extension CKFetchRecordsOperation { var perRecordResultBlock: ((CKRecord.ID, Result<CKRecord, Error>) -> Void)? var fetchRecordsResultBlock: ((Result<Void, Error>) -> Void)? } fetchRecordsOp.perRecordResultBlock = { recordID, result in // result is .failure(CKError.unknownItem) or .success(record). } fetchRecordsOp.fetchRecordsResultBlock = { result in // result is .success. }
-
9:14 - CloudKit: Delete single item
// Single item delete func deleteLastPerson() async throws { do { let recordId = try await database.deleteRecord(with: lastPersonRecordId) os_log("Record with ID \(recordId.recordName) was deleted.") } catch { self.reportError(error) throw error } }
-
9:37 - CloudKit: Delete batch
// Batched modifications func deleteLastPeople() async throws { do { let recordIds = [lastPersonRecordId, penultimatePersonRecordId] let (_, deleteResults) = try await database.modifyRecords(deleting: recordIds) for (recordId, deleteResult) in deleteResults { switch deleteResult { case .failure(let error): self.reportError(error, itemId: recordId) case .success: os_log("Record with ID \(recordId.recordName) was deleted.") } } } catch let operationError { self.reportError(operationError) throw operationError } }
-
13:43 - CloudKit: Encrypted values
extension CKRecord { @NSCopying open var encryptedValues: CKRecordKeyValueSetting { get } }
-
14:29 - CloudKit: Using encrypted values
// Device 1: Encrypt data before calling CKModifyRecordsOperation. myRecord.encryptedValues["encryptedStringField"] = "Sensitive value" // Device 2: Decrypt data after calling CKFetchRecordsOperation. let decryptedString = myRecord.encryptedValues["encryptedStringField"] as? String
-
16:35 - CloudKit: Account status
open func accountStatus(completionHandler: @escaping (CKAccountStatus, Error?) -> Void)
-
16:46 - CloudKit: CKAccountStatus
public enum CKAccountStatus : Int { case couldNotDetermine case available case restricted case noAccount case temporarilyUnavailable }
-
21:10 - CloudKit: Setup a record hierarchy
// Share a record hierarchy let zone = CKRecordZone(zoneName: "MyZone") // Save zone... let fileRecordA = CKRecord(recordType: "File", recordID: CKRecord.ID(zoneID: zone.zoneID)) let fileRecordB = CKRecord(recordType: "File", recordID: CKRecord.ID(zoneID: zone.zoneID)) let folderRecord = CKRecord(recordType: "Folder", recordID: CKRecord.ID(zoneID: zone.zoneID)) fileRecordA.setParent(folderRecord) fileRecordB.setParent(folderRecord) // Save records...
-
21:41 - CloudKit: Record Hierarchy, Share
// Share a record hierarchy let share = CKShare(rootRecord: folderRecord) do { let (saveResults, _) = try await database.modifyRecords(saving: [folderRecord, share]) for (recordID, saveResult) in saveResults { // Handle per-record result. } } catch let operationError { // Handle operation error. }
-
22:51 - CloudKit: Share a Record Zone
// Share a record zone let zone = CKRecordZone(zoneName: "MyZone") // Save zone... let share = CKShare(recordZoneID: zone.zoneID) do { let (saveResults, _) = try await database.modifyRecords(saving: [share]) for (recordID, saveResult) in saveResults { // Handle per-record result. } } catch let operationError { // Handle operation error. }
-
-
正在查找特定内容?在上方输入一个主题,就能直接跳转到相应的精彩内容。
提交你查询的内容时出现错误。请检查互联网连接,然后再试一次。