大多数浏览器和
Developer App 均支持流媒体播放。
-
保持复杂功能的及时更新
时间至关重要:了解 Apple Watch 的复杂功能如何全天候提供相关信息,并帮助人们在需要时获取所需信息。 了解如何利用 app 运行机会,如何合并 API,比如后台 app 刷新和 URLSession 等,以及如何在合适的时间推送通知。
资源
相关视频
WWDC21
WWDC20
-
下载
(你好 WWDC 2020)
你好 欢迎来到 WWDC (《保持复杂功能的及时更新》) 你好 我叫 Mike Lamb 是 Apple Watch 团队的软件工程师 我今天来这里是想谈谈 保持复杂功能及时更新的最佳方法 将会涉及很多内容 现在就开始吧 复杂功能是 Apple Watch 的 固有用户体验 随着 watchOS 7 发布了表盘共享 SwiftUI、复杂功能和多种复杂功能 API 复杂功能成为今年 Watch app 更为瞩目的焦点 复杂功能是快速在表盘上 提供及时和相关信息的好方法 保持复杂功能的及时更新 是为客户提供良好体验的必要条件 watchOS 可提供复杂功能 app 的 特殊功能 从而打造出色的体验 即使其他 app 被停止运行 并从内存中删除 复杂功能 app 仍能继续运转 如果系统确实需要停止某个复杂功能 app 稍后也会被重新启动来更新其复杂功能
由于复杂功能总是可见的 复杂功能 app 被认为是为隐私而设的 我们今天的目标是讲述那些 app 可以使用的 保持动态复杂功能及时更新的技术 我们从讨论我正在构建的一个 示例 app 开始 来解释这些技术
更新我们复杂功能的绝佳时机 就是在 app 被积极使用的时候 我们会讨论具体如何进行 即使 app 不在前台 watchOS 也提供了机制 使其能够保持复杂功能的及时更新 背景更新让复杂功能感觉像变魔术一样 第一个后台更新机制是后台 App 刷新 后台 App 刷新允许 app 调度后台运行时间 以访问 Watch 上的 API 和数据 App 还可以调度后台 URLSessions 以便在 app 没在活动状态时 从服务器中提取数据 而且数据可以通过复杂功能推送 直接发送到 Watch 上的 app 今天的讲座结束时 我们会讲到具体的示例
重申一下 在这三个背景机制中 app 无需一直处于活动状态 它将在必要时启动 以更新其复杂功能 所有这些机制既可单独使用 也可以组合使用 这取决于 app 的需要 为了说明如何保持复杂功能的及时更新 我一直在创建一个放风筝 app 的示例 我们的目标是在不需要手机的情况下 提供令人信服的体验 因此要构建一个独立的 Watch app 我们将使用 watchOS 7 中可用的新 API 来支持多种处于活动中的复杂功能 要明确的是 今天我们要展示的是 如何使复杂功能保持最新 而不是展示如何设计或构建它们 有关如何进行那些操作的信息 请观看我们的其他讲座 我们希望这款 app 能鼓励人们更加积极 所以第一个复杂功能 将从 HealthKit 中提取有关 当天活动的信息 我们将使用后台 App 刷新来提取数据 当你想放风筝时 跟踪天气情况是很重要的 所以我们需要天气数据 尤其是风 我们将使用后台 URL 会话定期获取 最新缓存位置的天气数据 和朋友一起放风筝非常有趣 所以我们想要提供的复杂功能 能显示朋友们的鼓励 我们将使用复杂功能推送 来提供此复杂功能的更新 我们先谈一谈前台的更新 在用户启动 app 的时候 就是更新复杂功能的好时机 一旦我们有了想要显示的更新 就可使用 ClockKit API 重新加载复杂功能的时间线 一个复杂功能由条目的时间线组成 当某个 app 想要更新复杂功能时 就在复杂功能服务器上为该复杂功能 调用 reloadTimeline
这里我们编写了一个 updateActiveComplications 方法 然后将在整个 app 中使用它
这种方法迭代活跃的复杂功能数组 并要求复杂功能的服务器 重新加载每一个复杂功能 在本例中 我们每次都更新所有的复杂功能 通常情况下 你应该更为严格地筛选 只需更新那些需要更新的复杂功能 每次更新复杂功能时 我们都调用 updateActiveComplications
在 updateActiveComplications 调用 reloadTimeline 后 或在其他需要的时候 复杂功能服务器将调用 app 的 CLKComplicationDataSource 来获取当前的时间线条目
作为回应 app 将会使用模板 和提供程序构建条目
当 app 完成构建条目时 它就会使用提供的完成处理程序 将其传递到复杂功能服务器
代码如下 在 updateActiveComplications 调用 reloadTimeline 之后 CLKComplicationDataSource 将被调用 请求当前时间线条目以更新复杂功能 然后提供处理程序 用于在构建条目后传递回该条目
我们使用适合于复杂功能类型的模板 利用提供程序填充模板以构建时间线条目 创建条目后 就可使用提供的处理程序将其传递回 如你所见 在前台更新复杂功能 是非常简单直接的 我们要求复杂功能服务器重新加载 复杂功能 然后为每个复杂功能提供当前条目 每当用户更改 app 中的选择 或者 app 在前台接收到新数据时 我们都会这样做
不过 我们的 app 通常不在前台 这样的话 我们就可以使用 后台 App 刷新等机制 以获取所需的数据来刷新复杂功能
回到示例 app 我们想从 HealthKit 中 提取活动复杂功能的数据 后台 App 刷新允许我们安排定期更新 即使 app 不在使用中 也能保持复杂功能的更新
我们的 app 可以使用后台 App 刷新 来进行每小时最多四次的复杂功能刷新 不管 app 会在活动表盘上 配置多少复杂功能 这个次数都是固定的 app 接收的实际更新数量取决于 其他进程的运行数量 和电池使用情况等条件
要安排后台 App 刷新 可在 WKExtension 上调用 scheduleBackgroundRefresh 你的 app 可能会在后台启动 因此 考虑在 applicationDid- FinishLaunching 中安排第一个请求
我们已经编写了 一个调度后台 App 刷新的方法 首先 选择调度日期 这是第一个请求 我们会如显示尽快地提出请求 系统会选择合适的时间启动你的 app 这总是在你请求的时间之后 通常在一、两分钟内 但这取决于系统条件 利用用户信息代码字典提供你自己的数据 在本例中 为了说明这是如何实现的 我们将传递请求发出的时间
一旦有了调度日期和可选的用户信息 我们就在 WKExtension 上调用 scheduleBackgroundRefresh
当请求已被调度时 WKExtension 将在主线程上 异步调用完成处理程序 处理可能发生的任何错误
当任务准备好时 我们的 app 将被激活 并调用扩展委托来处理后台任务
在做了一些处理之后 我们将请求复杂功能更新 并安排下一次后台 App 刷新 然后设定任务完成
让我们来看看扩展委托 当 Xcode 生成扩展委托时 它还生成处理后台任务的方法
系统可能有多个任务要完成 我们的扩展委托循环遍历所有任务 并给一个都发送了通知
生成的代码为 app 可以接收的 所有类型的后台任务 提供默认处理程序 我们会在 WKApplication- RefreshBackgroundTask 处理它 并用我们自己的代码替换默认处理程序
在这段代码中 纯粹作为一个示例 我们检索在请求调度时添加的用户信息 我们使用存储在其中的日期 来计算自发出请求后的时间
然后调用 updateActiveComplications 方法 请求复杂功能服务器重新加载 活动复杂功能 然后安排下一次后台刷新 在更新了复杂功能 并安排了另一个请求之后 我们就完成了当前任务 我们输入“假”表示不需要 snapshot 每次复杂功能更新 都会产生一个 snapshot 请求 所以我们不必单独发出请求
一旦 app 完成后台任务后 就可能会被暂停 所以我们必须在设定任务完成之前 做好所有的工作
如果我们想做一些更复杂的事情 比如访问 HealthKit 就要修改策略了
为了避免在扩展委托中放置过多的代码 我添加了一个使用 HealthKit 的 数据提供程序 并添加一个 采用自己完成处理程序的方法
HealthKit 查询可以是异步的 所以我们需要等到刷新完成后 才能更新复杂功能 然后安排下一个请求 并设置任务完成
这样做非常简单 因为 HealthKit 的工作 是由新的 HealthDataProvider 完成的
我们调用 HealthDataProvider 来刷新此数据 它是异步完成的 当它刷新完成数据时 就会调用完成处理程序 告诉我们是否要更新复杂功能 实际上 我们在完成处理程序中 更新复杂了功能 安排下一次刷新并设置成任务完成
回顾一下 后台 App 刷新对于安排 周期性的后台任务是很棒的 你的 app 可以恢复或启动 以处理这些任务 每小时最多四次 这里有一些要牢记的指导方针 一次只有一个请求未完成 如果你需要定期更新 请执行我所展示的操作 并在标记当前任务完成之前 安排下一次更新 不允许进行任何网络活动 你使用 Watch 上可用的大多数 API 但 URLSession 是个例外 如果确实要使用 URLSession 它将失败并出现错误
你的 app 被限制为 最多四秒的活动中央处理器时间 听起来可能不算长 但是四秒钟的持续处理其实是很长的 如果你需要执行更多的处理 可以考虑把它分成更小的块
该 app 最多有 15 秒的总时间 来完成任务 超过 15 秒的一个常见原因是 忽略了标记任务已完成
后台 App 刷新很棒 但是要想在后台通过网络访问数据 那就要使用后台 URLSession 后台 URLSessions 允许你的 app 在没有运行的情况下 调度和接收数据
除了后台 App 刷新之外 还可以使用后台 URLSession 你甚至可以随时更改请求 并插入身份验证挑战 挺酷的
我们会用它来检索计划添加到 app 中的 本地风力复杂功能的天气信息 在大多数情况下 你的 app 每小时 最多可以发出和接收四次请求 实际次数取决于若干因素 包括 Wi-Fi 的可用性 蜂窝网络接收和电池续航时间
你的 app 可以有多个 未完成的后台下载任务 始终要确保在启动 app 时附加到会话 这样你可以接收 URLSession 委托回调 首先 让我们来看看如何调度 后台 URLsessions
在 app 中 我们希望定期获得天气数据 为此我们将使用 URLSession 框架 我们创建了 WeatherDataProvider 来作为 URLSession 的委托 我们的数据提供程序创建一个 后台 URLSession 配置 我们在后台配置上设置 sessionSendsLaunchEvents 以在后台唤醒 app 我们使用配置在下载任务中 创建 URLSession 我们设置任务的最早开始日期 这将是任务的预定日期 然后我们恢复任务以启动它 这是 WeatherDataProvider 第一部分 它是 URLSessionDownloadDelegate 要创建 URLSession 我们需要一个后台配置 我们将该配置标记为非自主决定 并确保 sessionSendsLaunchEvents 设置为“真” 以便在后台启动 app 然后我们使用一个配置来创建 URLSession 这样如果我们将 delegateQueue 设置为“空值” URLSession 的响应将在后台 串行队列上发送 然后还是 WeatherDataProvider 我们添加了一个调度方法 来创建和调度下载任务 我们有多个未完成的请求 但在本例中 你只需要一个请求 因此 只要有一个任务尚未完成 我们就会安排一个新的后台任务 我们使用 Core Location 的 最新缓存位置构建所需的 URL 并为后台会话创建下载任务 我们设置了此任务的最早开始日期 正如前面所做的那样 我们立即发出第一个请求 并将后续请求安排为每 15 分钟一次 我们设置预期发送和接收的字节数 最后 我们恢复已经准备好运行的任务 把它安排好后 下载任务将独立于我们的 app 运行 当它完成时 我们的 app 将在后台恢复 或根据需要启动 以处理请求 下载完成后 WKExtension 将 WKURLSessionRefresh- BackgroundTask 传递到扩展委托
我们使用 WeatherDataProvider 来处理请求
在标记任务已完成之前 URLSession 委托方法会被传递出去 在处理这些调用之前 不要将任务标记为已完成
如果任务成功完成 我们的委托会接收到 didFinishDownloadingTo 委托调用 无论下载是否成功完成 我们的委托都将收到 didCompleteWithOptionalError 当该调用完成时 WeatherDataProvider 会调用 为它提供的完成处理程序 这样我们就可以更新复杂功能 安排新请求 然后设置任务已完成
我们之前安排的下载任务已经完成 现在是处理结果的时候了 我们的会话委托被要求处理 WKURL- SessionRefreshBackgroundTask 会话委托请求 WeatherDataProvider 刷新 并传递在完成后要调用的闭包
刷新完成后 WeatherDataProvider 会调用闭包 在该闭包中 我们安排下一次检索 根据需要更新复杂功能 然后标记任务已完成 在 WeatherDataProvider 中 我们的更新方法 存储其传递的完成处理程序 以便在交付了预期的委托方法后 就可以调用它 由于任务已完成 我们的 WeatherDataProvider 会接收 URLSession 委托方法 downloadTask didFinishDownloadTo 我们请求的数据已经下载到文件中 我们检查一下 然后处理我们收到的 json 天气数据
下载任务完成并处理完数据后 我们的 app 会收到 didCompleteWithOptionalError 我们希望在主队列上调用完成处理程序 因此我们调动到主队列 并调用完成处理程序 如果没有出现错误 我们会告诉它更新复杂功能 然后 我们将完成处理程序设置为“空值” 这样它就不会被多次调用 根据请求 我们的 app 可能会得到中间请求 这样可允许 app 在下载完成之前更新 URL 取消下载任务或回答身份验证挑战
我们简单看一下那些内容 就像我们到目前为止已经看到的 涉及 URL 会话任务的其他情况一样 这些将以 WKURLSessionRefresh- BackgroundTask 的形式出现在 app 中 和以前一样 扩展委托会被要求 处理这些任务 我们将使用 WeatherDataProvider 来处理这些请求 当任务处于活动状态时 WeatherDataProvider 将从 URLSession 子系统 接收到委托调用
WillBeginDelayRequest 允许 app 更新或取消 URL 请求 例如 如果我们发起请求后 已经等很长时间了 我们可以从 Core Location 的 最新缓存位置替换 第一次发出请求时指定的位置
DidReceiveChallenge 允许 app 响应任何可能发生的 身份验证挑战 当所有事件都已交付时 我们的委托将接收 SessionDidFinishEvents 这时候我们将调用完成处理程序 此时你可能想安排一个新任务 但不要这样做 因为当前的任务尚未完全完成 相反 只需将此任务标记为已完成 对于后台 App 刷新 指导方针相同
你的 app 应该避免过于繁琐的处理 并确保将任务设置为 在收到任务后的 15 秒内完成 后台 URLSessions 非常适合 从远程服务器检索数据 可以根据需要安排、修改或取消它们
我们今天要看的最后一个机制是 复杂功能推送 根据用例的不同 推送比拉取服务器获取数据更有效
对于事件驱动的数据尤其如此 我们将使用复杂功能推送为 上一个复杂功能提供数据 其中记录着一起放风筝的朋友的鼓励 服务器每天可以向每个 Watch 发送多达 50 个复杂功能推送 复杂功能推送不需要被定期间隔 如果数据是突发数据 则请求的发送速度 会比我们讨论的其他机制更快 为防止超过每日上限 限制可能是必要的
向 Watch 发送推送的服务器 需要有正确的证书 我们快速了解一下如何设置该证书 首先我们需要一个标识符 里面包括 app 的 bundle ID 并以 dotwatchkit- appdotcomplication 结尾 这很关键 如果 app 标识符的格式不正确 你的推送可能会被 Apple 服务器拒绝 也可能不会在 Watch 上收到
创建了 dotcomplication app 标识符后 使用它来创建 Apple Push Notification service SSL 证书 你的服务器将使用该证书 以验证 Apple 的推送通知服务器
你的 app 还需要远程通知后台模式 以及 WatchKit 扩展中的推送通知功能
借助 Watch app 我们使用 PushNotificationProvider 在 PushKit 中注册它
注册成功后 app 将收到凭据 将这些凭据上传到你的服务器 这样你的服务器就可以和 Watch 通信了 这就是 PushNotificationProvider 它是 PKPushRegistryDelegate 它创建 PKPushRegistry 实例 并为回调提供主队列 它将自己设置为委托
它还将 desiredPushType 设置为 dotcomplication 这与我们在服务器上 创建并安装的 dotcomplication 标识符和证书相匹配
注册完成后 注册表返回凭据 和 didUpdate pushCredentials 调用 我们将这些凭据发送到服务器 以便它们可以与 app 的这个实例 进行通信
一旦 Watch 将其凭据上传到服务器 服务器就会使用这些凭据 把 Watch 的推送发送到 Apple 服务器 Apple 服务器再将推送发送给 Watch 正如我们前面所说的 每个 Watch 每天允许 50 次推送
在发送复杂功能推送时 格式的设置要和在 aps 代码字典中 提供内容可用条目的任何后台推送一样 推送被 Watch 上的 PushKit 接收到后…
就通过 didReceiveIncoming- PushWithPayload 传递到 app 调用之后 完成处理程序处理完推送 就会被提供 回到 PushNotificationProvider 的 实施 当推送可用时 若 app 不在活动状态 则将被恢复或启动
那时 将在我们的委托上调用 didReceiveIncomingPush 有效负载类型 我们在 PushKit 注册时指定的队列 用于进行此调用 在我们的例子中 这是主队列 这里提供了完成处理程序 必须在处理有效负载时调用它 处理有效负载 并调用扩展委托来更新我们的复杂功能 现在我们在更新所有的复杂功能 如果这是一个送货 app 我们只需更新需要的复杂功能 这就是复杂功能的推送 总之 一个 app 的每个实例 每天最多可以收到 50 个推送 当 app 有多个在活动中的复杂功能时 这个数字不会改变 指导方针与其他类型的背景更新方针相同 总结一下我们今天讨论的技术 当 app 处于活动状态时 利用前台的机会 更新你的复杂功能 当 app 的状态为回应输入而改变时 或者当 app 在前台提取服务器数据时 执行此操作更有必要
当 app 从前台转向到后台时 app 可以使用 ProcessInfo 任务 来完成工作
后台 App 刷新对于调度运行时间 非常有帮助 以访问你自己的数据或 使用 HealthKit 这样的 watchOS API app 可以使用后台刷新 来更新其复杂功能 每小时最多四次
可以安排后台 URLSession 任务 从服务器拉取数据 每小时最多四次 请记住 当 app 被激活时 请始终重新连接委托 以便它可以接收可能在等待的更新 推送通知可以从服务器发送到每个 Watch 每天多达 50 次 如果是突发数据 则定期间隔或者更频繁地发送它们 第 51 次及以后的通知将被忽略 因此根据需要在服务器上应用限制 这些机制可单独使用或根据需要组合使用 你可以看到 让复杂功能保持及时最新是多么重要 有多种机制可供你使用 让你的复杂功能保持最新 即便 app 不处于活动状态 也可混合和搭配我们讨论过的技术 从而提供最好的体验 我们今天讨论的所有机制 都可以与独立的 Watch app 一起使用 从而为你的 app 提供最大的灵活性 本周早些时候的 《Watch 表盘共享介绍》视频 更详细地讨论多种新的复杂功能 API 《在 Swiftui 中构建复杂功能》 描述了如何使用 Swiftui 构建优秀的复杂功能 最后 去年的《创建独立的 Watch App》 视频也很棒 把这些内容都了解一下 谢谢 我迫不及待 想看看你使用这些技术所创造的 精彩的复杂功能 祝你 WWDC 参会愉快
-
-
3:32 - updateActiveComplications
class ExtensionDelegate: NSObject, WKExtensionDelegate { func updateActiveComplications() { let complicationServer = CLKComplicationServer.sharedInstance() if let activeComplications = complicationServer.activeComplications { for complication in activeComplications { complicationServer.reloadTimeline(for: complication) } } } }
-
4:26 - getCurrentTimelineEntry
class ComplicationController: NSObject, CLKComplicationDataSource { func getCurrentTimelineEntry(for complication: CLKComplication, withHandler handler: @escaping (CLKComplicationTimelineEntry?) -> Void) { switch (complication.family) { case .modularSmall: let template = CLKComplicationTemplateModularLargeTallBody.init( headerTextProvider: headerTextProvider, bodyTextProvider: bodyTextProvider) entry = CLKComplicationTimelineEntry(date: Date(), complicationTemplate: template) } handler(entry) } }
-
6:06 - scheduleBar
private func scheduleBAR(_ first: Bool) { let now = Date() let scheduledDate = now.addingTimeInterval(first ? 60 : 15*60) let info:NSDictionary = [“submissionDate”:now] let wkExt = WKExtension.shared() wkExt.scheduleBackgroundRefresh(withPreferredDate: scheduledDate, userInfo:info) { (error: Error?) in if (error != nil) { print("background refresh could not be scheduled \(error.debugDescription)") } } }
-
7:08 - handleBAR
class ExtensionDelegate: NSObject, WKExtensionDelegate { func handle(_ backgroundTasks: Set<WKRefreshBackgroundTask>) { for task in backgroundTasks { switch task { case let backgroundTask as WKApplicationRefreshBackgroundTask: if let userInfo:NSDictionary = backgroundTask.userInfo as? NSDictionary { if let then:Date = userInfo["submissionDate"] as! Date { let interval = Date.init().timeIntervalSince(then) print("interval since request was made \(interval)") } } self.updateActiveComplications() self.scheduleBAR(first: false) backgroundTask.setTaskCompletedWithSnapshot(false)
-
8:47 - handleBAR (DataProvider)
class ExtensionDelegate: NSObject, WKExtensionDelegate { var healthDataProvider: HealthDataProvider func handle(_ backgroundTasks: Set<WKRefreshBackgroundTask>) { for task in backgroundTasks { switch task { case let backgroundTask as WKApplicationRefreshBackgroundTask: healthDataProvider.refresh() { (update: Bool) -> Void in if update { self.updateActiveComplications() } self.scheduleBAR(first: false) backgroundTask.setTaskCompletedWithSnapshot(false) }
-
11:35 - Instantiate backgroundURLSession
class WeatherDataProvider : NSObject, URLSessionDownloadDelegate { private lazy var backgroundURLSession: URLSession = { let config = URLSessionConfiguration.background(withIdentifier: “BackgroundWeather") config.isDiscretionary = false config.sessionSendsLaunchEvents = true return URLSession(configuration: config, delegate: self, delegateQueue: nil) }()
-
12:02 - Schedule backgroundURLSessionTask
func schedule(_ first: Bool) { if backgroundTask == nil { if let url = self.currentWeatherURLForLocation(delegate.currentLocationCoordinate) { let bgTask = backgroundURLSession.downloadTask(with: url) bgTask.earliestBeginDate = Date().addingTimeInterval(first ? 60 : 15*60) bgTask.countOfBytesClientExpectsToSend = 200 bgTask.countOfBytesClientExpectsToReceive = 1024 bgTask.resume() backgroundTask = bgTask } } } }
-
13:29 - handle backgroundURLSession
class ExtensionDelegate: NSObject, WKExtensionDelegate { var weatherDataProvider:WeatherDataProvider func handle(_ backgroundTasks: Set<WKRefreshBackgroundTask>) { for task in backgroundTasks { switch task { case let urlSessionTask as WKURLSessionRefreshBackgroundTask: weatherDataProvider.refresh() { (update: Bool) -> Void in weatherDataProvider.schedule(first: false) if update { self.updateActiveComplications() } urlSessionTask.setTaskCompletedWithSnapshot(false) }
-
13:59 - handle backgroundURLSession
class WeatherDataProvider : NSObject, URLSessionDownloadDelegate { var completionHandler : ((_ update: Bool) -> Void)? func refresh(_ completionHandler: @escaping (_ update: Bool) -> Void) { self.completionHandler = completionHandler }
-
14:08 - didFinishDownloadingTo
class WeatherDataProvider : NSObject, URLSessionDownloadDelegate { func urlSession(_ session: URLSession, downloadTask: URLSessionDownloadTask, didFinishDownloadingTo location: URL) { if location.isFileURL { do { let jsonData = try Data(contentsOf: location) if let kiteFlyingWeather = KiteFlyingWeather(jsonData) { // Process weather data here. } } catch let error as NSError { print("could not read data from \(location)") } } }
-
14:23 - didComplete
func urlSession(_ session: URLSession, task: URLSessionTask, didCompleteWithError error: Error?) { print("session didCompleteWithError \(error.debugDescription)”) DispatchQueue.main.async { self.completionHandler?(error == nil) self.completionHandler = nil } } }
-
17:53 - Complication Pushes
class PushNotificationProvider : NSObject, PKPushRegistryDelegate { func startPushKit() -> Void { let pushRegistry = PKPushRegistry(queue: .main) pushRegistry.delegate = self pushRegistry.desiredPushTypes = [.complication] } func pushRegistry(_ registry: PKPushRegistry, didUpdate pushCredentials: PKPushCredentials, for type: PKPushType) { // Send credentials to server } func pushRegistry(_ registry: PKPushRegistry, didReceiveIncomingPushWith payload: PKPushPayload, for type: PKPushType, completion: @escaping () -> Void) { // Process payload delegate.updateActiveComplications() completion() }
-
-
正在查找特定内容?在上方输入一个主题,就能直接跳转到相应的精彩内容。
提交你查询的内容时出现错误。请检查互联网连接,然后再试一次。