大多数浏览器和
Developer App 均支持流媒体播放。
-
去而复返:Apple Watch 上的数据传输
Apple Watch 的改进使您获得了更多与 app 往来通讯的方式,并带来了需要考虑的新受众。了解可用于数据通信的策略以及如何选择适合此任务的工具。对比和比较使用 iCloud 钥匙串、Watch Connectivity、Core Data 等技术的益处。
资源
- Downloading files from websites
- Keeping your complications up to date
- Keeping your watchOS content up to date
- Sharing access to keychain items among a collection of apps
- Supporting Associated Domains
- WCSession
相关视频
WWDC21
WWDC20
WWDC19
-
下载
嗨!欢迎来到WWDC 我是安希区考克 是Watch架构团队的工程师 今天很高兴能跟大家谈谈 Apple Watch的数据传输策略
Apple Watch 自推出后 已经变得越来越独立了 Series 3是第一款拥有移动网络的 Apple Watch
WatchOS 6独立的Watch app 让你能够写出不需要依赖于 iOS版本的app 并且可以从客户手表上的 App Store直接购买 WatchOS 7引进家人共享设置后 客户拥有更多独立性 不必非得拥有一支搭配的iPhone 但 这些新功能也带给我们 这些开发者 新的挑战 让我们思考跟Watch app 沟通的方式 幸运的是 我们有很多很棒的选择 今天我们就要谈谈这些选择 以及我们如何挑选对的选择完成工作 我会先介绍跟Watch app 数据沟通相关的工具 接着讨论要如何评估哪一个工具 才是完成任务的正确选择 我们可以把工具大概分成几个类别 iCloud让我们共享所有装置 并会给我们服务器储存空间 使用iCloud同步的 钥匙串和CloudKit的CoreData 我们就能在app里使用这个
假如我们需要在 配对的装置之间传输数据 可以使用Watch Connectivity
要直接跟服务器沟通 可以使用URL对话或Socket 但是首先 我们来谈谈 选择对的工具时 你会问到的问题 在思考要如何从我的 Watch app沟通时 我会思考几件事 这是哪一种数据? 数据现在在哪里 我又希望它传输到什么地方? 这个互动会不会需要 依赖搭配的iOS app? 我想不想支持家人共享设置? 数据需要何时抵达目的地? 数据能不能等待系统 为客户优化表现和电池用量? 数据改变的频率有多高? 根据这些问题的答案 我就可以开始从我的工具中 评估要如何为我的数据传输任务 塑造适合的解决方案
我们来看看 iCloud同步的钥匙串 可以给我们哪些能力
钥匙串为密码、密钥和其他敏感凭证 提供安全的储存空间 有了watchOS 6.2引入的 iCloud钥匙串同步功能 这些钥匙串的项目就能同步到 一个人所有的装置里
在app里进行 iCloud同步有两个好处 运用关联网域的密码自动填入 以及共享钥匙串项目 密码自动填入让你 只需要用很少的程序代码 就能使用钥匙串同步 首先 把关联网域能力加到 目标当中 如果是Watch app 请把能力加到WatchKit扩充套件目标 用你的网域名添加 一个“网络凭证”的条目
把apple-app-site-association文件 加到网络服务器 档案必须通过HTTPS 存取 不能有重新导向 档案为JSON格式 没有档案扩充功能 且应该要放在 服务器的./well-known目录 请到线上文件观看 《支持关联网域》获得完整细节 在TextFields 和SecureFields 加入textContentType 自动填入的选项有 用户名称、邮件地址、密码和新密码 若是新密码 系统会提醒对方进行储存 网站的钥匙串便会新增 或更新一个纪录
自动填入建议 从watchOS 6.2就有了 在watchOS 8新增 编辑文字的体验后 又变得更棒了 想知道更多关于使用 密码自动填入的信息 请到开发者app或网站观看 《自动填入随处可见》 另一个使用钥匙串同步 分享数据的方式 就是在app之间共享钥匙串项目
前面曾经说过 钥匙串是密码、密钥和凭证等 敏感数据的安全储存空间 你也可以把其他小块的共享数据 储存在钥匙串 像是对于开机屏幕的喜好 只要该信息不会经常改变 储存在钥匙串的数据 将在这个人所有的装置里同步 我们来看看要怎么在钥匙串 储存和取回一个OAuth2符记 并跟一组app共享 首先 我们要加入钥匙串共享 或App群组能力 到所有我们希望共享 这些钥匙串项目的app 这是共享项目必要的条件 可确保客户信息的 安全与隐私 防止其他app存取 如果是Watch app 请把能力加到Watch扩充套件目标 在这个例子里 我要加入钥匙串共享能力 并把我的app加到钥匙串群组 我所有要共享钥匙串项目的app 也必须共享这个群组 现在 我们来看看 在钥匙串储存OAuth2符记的程序代码 要储存符记 如果项目存在的话 我们要进行更新 不存在的话则要新增 我创造一个OAuth2Token结构 来容纳符记数据 像是符记字符串、到期和刷新符记 我让这个符记结构配合Codable 使它易于储存和取回 我们创造一个查询字典 这是一组符合现有项目的属性 如果我们已经有为这个 服务器和账户储存一个的话 注意 在这里 可同步的属性 是设定为true 把这个属性纳入我们的查询很重要 表示我们希望项目同步到 客户所有的装置 我们会把符记写成Data 把Data设为属性字典里 钥匙串项目的值 接着 用查询和属性 更新钥匙串的项目
一定都要检查钥匙串API 返回的成果码 我们要先检查 钥匙串有没有说项目没有找到 若是如此 我们要写另一个函数 加到钥匙串 这我们等一下会看看 我们要确定没有错误出现 所以我们要检查结果有没有成功 假如更新函数返回的结果是成功 那么就表示符记有更新到钥匙串
我们现在来看看add函数 要把符记加到钥匙串 我们要设置一个包含所有属性的字典 这包括我们用来找到 既有项目的属性 以及符记数据 接着 我们会叫出钥匙串API 包含这些属性的add函数 然后检查转回码 确定有成功
要从钥匙串取回符记信息 我们要设置查询字典 以便找到我们想要的项目 我们要纳入之前在更新函数中 为找到项目而包含的同一组密钥和值 另外 我们还要纳入一些属性 告诉钥匙串API 我们希不希望项目属性返回 (不希望) 还有我们希不希望项目数据返回 (希望) 钥匙串的CopyMatching函数 使用我们的查询进行搜寻 并在我们提供的参考数据填入item 在存取取回的项目之前 我们要检查转回码 确定项目有找到
一如往常 我们要检查转回码 确定有成功 拿替项目复制好的字典 拿从字典要求得来的符记数据 然后把数据解密为OAuth2Token类型 这下 我们便成功储存、更新 取回OAuth2符记到钥匙串 它会分享给钥匙串 共享群组里的所有app
我还想再跟你分享一个 钥匙串储存空间的函数 就跟你在客户的装置上储存 任何东西的时候一样 不用了就应该移除 我们要用现在 已很熟悉的属性设置查询来进行搜寻 用查询来叫出钥匙串API的删除函数 然后就跟平常一样 检查是否成功 以删除的例子来说 没找到就表示成功 数据不用了以后 要完成清除的动作 使用iCloud钥匙串 同步所提供的钥匙串服务 是app共享不常改变的 小块数据的好方法 且数据将同步到 用户所有的装置 使用关联网域 可轻松添加 密码自动填入功能到app 你也可以直接把值储存 及取回到钥匙串 并使用钥匙串共享 或App群组分享到你其他的app iCloud钥匙串同步 不需要有搭配的iOS app 且支持家人共享设置 根据网络可用性、电池 等系统条件 项目就会 在可以的时候进行同步 要注意 客户可以关闭iCloud钥匙串同步 且不是所有地区都能使用此功能 CloudKit的CoreData 会将你的本地数据库同步到 客户所有其他共享 你app CloudKit容器的装置 CoreData和SwiftUI的整合 可以简化存取和展示 Watch app里数据库之数据的方式 假如你在开发多平台的应用程序 这样做会让你很快 就在Watch上获得太多数据 仔细思考 你的客户真正需要 在Watch上得到的信息是什么
你可以考虑在CoreData模型中 使用多重组态 把原本在储存空间 和电池容量较大的装置上 运作的app的数据 切割成适合 Watch app的片段
CloudKit和CoreData是很强大的工具 CoreData和SwiftUI的整合 让你更容易在app 使用CoreData的功能 提供managedObjectContext 到View的Environment值 并使用FetchRequest这个属性封套 从数据库获得结果 这些结果可以用在SwiftUI Lists 跟其他View CloudKit的CoreData 提供了共享有架构数据的方法 可以同步到一个人所有的装置 并在iCloud备份 不需要有搭配的iPhone app 且支持家人共享设置 CoreData同步会根据 网络可用性和系统条件 出现改变 不要期待实时同步 不过CloudKit确实会 为你的app优化同步表现
想知道更多在app使用CloudKit的 CoreData的信息 请到开发者app 或网站观看《设计 通过CloudKit和CoreData 分享数据的app》 以及《把CoreData并行 带到Swift和SwiftUI》 你或许已很熟悉Watch Connectivity 说不定之前也有用过了 但我想要给你更多细节 还有一些最好的做法 协助你成功
当Watch app和搭配的 iPhone app都在蓝牙 接收范围内或使用相同的 Wi-Fi网络 Watch Connectivity 就能允许你在两个装置之间传输数据 使用这个功能最好的 时机是在客户同时安装了 手机和Watch的app时 用来优化他们的体验 以及在数据只存在于其中 一个装置时 用来进行数据分享
例如 如果有人启动了iPhone app 并下载最新的数据 你可以把数据分享给Watch app 使复杂功能保持最新 让Watch app 下次启动时 启用相同的数据 这样会让客户觉得app更有回应 并减少app必须进行的 数据重复下载 Watch Connectivity功能多样 所以知道有哪些功能以及何时应该 使用各个功能会很有帮助 但是首先 我想先分享几个秘诀 让你在决定 Watch Connectivity 是适合你的任务的工具后能获得成功 既然Watch Connectivity是让 两个装置进行沟通的工具 我们一定会需要知道 一些先决条件 并处理一些错误 你可以做几件事来确保 Watch Connectivity沟通顺畅无碍 越早在app的生命周期 激活WCSession越好 最好是app在app或扩充委派里 完成启动时 这样就能让app尽快 从搭配的app接收到信息 了解可达性 传输数据时 后台沟通 完全不会要求 搭配的app必须可达 但是互动信息 的确有可达性的要求 我们届时会讨论到 了解这些可节省你的时间
所有的WCSession委派函数 都会在非主要的序列队列被叫唤 如果你需要从这些函数进行任何工作 来更新用户界面 请确保你在主要队列进行 现在 我们来谈谈不同的 Watch Connectivity功能 及各个功能该在何时使用 应用程序上下文 是单一的属性列表字典 会传到后台的 搭配app 目标是要在app苏醒时 就可使用 假如你在先前的字典传送之前 更新应用程序上下文 它会被新的值取代
要在拥有新数据时让搭配的app内容 保持最新 或是针对 可能频繁更新的数据 应用程序上下文就很有用 用户信息传递 也会把属性列表字典传到 后台的搭配app 但那跟应用程序上下文有点不同 这不是每次更新时 就被取代的单一字典 每一个用户信息字典传递 都会根据你队列之的顺序 进行队列、传送 你也可以从队列 取消传递 档案传递跟用户信息传递类似 只要有做过一次 另一种就会感觉很熟悉 档案会被排进队列 等着传给搭配的app 在电力和其他条件许可时进行传送 你可以从队列取消传递 档案传递后 会放在接收的app的 档案收件匣中 从对话委派的didReceiveFile回呼 返回时 每一个档案 都会从收件匣里删除 因此请务必在 从这个方法返回之前 移动档案或尽快处理之 关于这点 记得这件事会很有帮助 由于回呼是在 非主要序列队列被叫唤 如果你叫出异步方法 来处理收件匣的档案 你很有可能会遇到问题 因为档案到时候已经不见了 档案传递的时机 会以系统条件为主 当然 较大的档案可能花比较长的时间传递
“TransferCurrentComplication UserInfo” 是用户信息传递功能的特殊例子 会把跟复杂功能 相关的数据传到Watch 只要还有复杂功能 传递的预算 它就会 尽快被传递 先于其他的 用户信息传递 当手机有更新数据时 这样立即的传递就会 为客户把活跃的复杂功能保持最新 你可以检查剩余的预算 如果在没有剩余预算时传递 最新复杂功能信息 它还是会传出去 只是会使用正常的用户信息传递队列
你可以用sendMessage 将数据传到搭配的app 并得到回复 这是在搭配的app 可达的时候进行的互动传讯 无论你传的是字典或数据 请避免太大的信息 我们也建议你为sendMessage的动作 加入replyHandler 简短的回复让你可以确认 搭配的app确实有收到信息 且数据是正确的 在sendMessage加入 replyHandler时 也要确保你有在包含 replyHandler的搭配app里 执行didReceiveMessage 或didReceiveData委派回调函数 否则 传信息时会得到错误
认识sendMessage之后 我们来重温可达性的概念 两个app都必须要可达 以便传送信息 你可以在WCSession 检查isReachable属性 判定搭配的app针对 非后台的实时传讯是否可达 不过 可达是什么意思? 就是两个装置都必须在彼此的 蓝牙范围内或使用同一个Wi-Fi网络 WatchKit扩充套件要可达 一定必须正在前台运作 或在后台运作时有高优先性 像是在执行 长时间运行的后台对话 iOS app就没有前台的要求 假如你从Watch app传信息 到iOS app 而iOS app没有在前台 iOS app会在后台积活 接收信息
这就表示iOS app 从Watch扩充套件可达的次数 比相反的情况多上许多 当客户同时安装了iPhone 和Watch app Watch Connectivity可以提供他们 感觉十分即时很有回应且直觉的体验 由于Watch Connectivity是专门要让 手机和配对的Watch进行沟通的 不要用来支持家人共享设置的app
数据传递需要依赖 通过蓝牙或Wi-Fi 而可用的搭配装置 使用sendMessage进行的实时沟通 要求搭配方要可达 记住 搭配的app 很多时候都不可达 特别是当你试图跟 Watch app沟通的时候 后台传递不会立即传到 可以把它想成寄一封信 你把信投进邮筒 但你无法确定信究竟何时会到
想知道更多Watch Connectivity的 信息 请到开发者app或网站观看 《介绍Watch Connectivity》 现在 我们来谈谈直接 跟服务器沟通的几个方法 对大部分的应用案例来说 最好的选择是URL对话 依据互动和数据类型而定 你可能有办法推迟沟通 或者可能需要马上进行 因此 URL对话有不同的配置 可以允许在后台或前台运行 我们来看看每个选项该什么时候使用 只要可以 就应该使用后台对话 这或许不是我们开发者 会有的直觉反应 我们可能希望放手去做 马上得到或传送数据 但请好好想一想 前台对话必须在 你的app位于前台 或最前端时完成 但除非是最短的那种任务 否则这样的时间并不够 试想 若客户的沟通任务 失败了 他们会有什么样的体验
所以 请体贴你的客户 谨慎评估每一次的沟通任务 问问自己:“我可以 在后台做这件事吗?”
针对任何沟通可能延迟的状况 或大型的数据传输 后台URL对话 才是正确的选择 你也可以发送推送通知到app 通知有新数据可用 启动后台更新 真正的后台传递时间点 会依系统条件而定 我们来示范传送某些数据 到后台服务器 举例来说 假如我有一些应用程序的设定 想要通过网络服务器储存 当客户储存设定时 我可以在Watch上储存 接着传送到后台的服务器
要做到这件事 我写了 一个后台URL对话类别 来应付服务器沟通的任务
URL对话的后台配置 会有独一无二的识别符 之后可以用来找到它 把SendsLaunchEvents属性 设定为true 表示对话的任务 需要处理时 对话应该在后台启动app 注意 如果要传输大量数据 你应该把URL对话配置的 isDiscretionary属性设定为true 让系统把传输排定在装置 最佳的时间 以达到最好的表现 在这个情况下 你也应该让客户知道 在连上Wi-Fi或充电前 下载可能不会发生
准备好传送数据时 我们必须让传输进入队列 以排定后台对话 我们要创造、配置一个URL请求 包含更新的设定内容到服务器
接着 我们要为对话请求写一个任务 在这个简化的例子中 我只有加一个任务到对话 但你也可以加入多个请求到对话 以讲求效率 把earliestBeginDate 设定为晚一点开始下载 注意 系统会根据后台预算 网络和系统条件来决定任务 开始的真正时间 如果在运作中的 表盘上拥有复杂功能 你的app 每小时最多可收到四次后台刷新任务 所以请让任务间隔至少15分钟 防止被系统延迟
我正在把这个对话 暂置在一份进行中的对话清单 之后系统让我知道URL 请求完成时 这就会变得很重要 在task叫出resume会真正启动之 所以叫出这个很重要 最后 我把status设定为queued 以免对话有观察者 当后台请求使用传到 扩充套件委派的后台任务 进行处理后 系统会通知app 为了处理这个任务 我们需要写一个符合 WKExtensionDelegate的类别 并执行处理后台任务的函数 针对后台URL对话刷新任务 我们会试着在 进行中的请求清单找到我们的对话 找到的话 我们要在 对话上叫出一个函数 针对该对话把后台 刷新任务加到列表 这样我们一完成数据的处理 就能让系统知道我们完成了 我等一下就会示范
如果在清单里没有找到对话 就要把任务标为已完成 总是在一结束后就把 后台刷新任务设定为已完成 是非常重要的 要得到后台任务叫唤 我们还得再做一件事 我们必须把扩充套件委派连结到app 要做到这点 我们要用 WKExtensionDelegateAdaptor 属性封套 在扩充套件委派类别里 添加一个属性到app 这下 系统会叫出扩充套件委派 来处理后台任务 在扩充套件委派里 我们叫出这个函数 把后台任务加到 既有的对话 把这个任务加到后台任务列表中 这样只要一处理完 URL数据 就可以把它标为已完成 现在 我们已经绕了一整圈 剩下唯一要做的 就是得到数据 让系统知道我们完成了 请求完成后 URL对话下载委派会被叫出 处理从下载任务的档案收到的数据 你应该要把这个项目移到 app可取得的目录 或尽快处理档案的数据 这个任务完成后 下载好的档案会被删除 我们要把这个对话 从处理中的对话清单移除 因为我们不会再从扩充套件委派得到 更多后台任务了 我们要把状态设定为已完成 以防有任何观察者存在 最后 我们要把后台任务设定为完成 这会让系统知道 我们已经完成后台处理 确保自己做到这点 不只是在当个Watch app的良好公民 还是在防止系统因为超过 后台限制而中止你的app 就是这样! 我们已经把设定传递到后台 也收到所有的更新 请注意 在完整操作时 你会应付错误和鉴别的挑战 但这个就是基本的步骤 当有人在跟你的app互动时 使用前台URL对话 进行快速的服务器沟通 获取最新的健身清单 或每日冥想练习 都是很好的例子 前台URL对话是接收与传送数据 比较耗电的方法 因此会强制2.5分钟逾时 但是在实践上 针对比这个限制 快上许多就能完成的互动 你应该尝试前台对话 URL对话是直接跟服务器 进行一般目的的沟通 最好的方法 这类对话不必依赖搭配的iPhone app 而且可以跟支持家人 共享设置的app一起使用 可能延迟数据传输的时候 以及传输大量数据的时候 要使用后台对话 想知道更多有关URL对话的内容 请到开发者app或网站观看 《复杂功能保持最新状态》 以及《解密后台执行》 除了URL对话 如果你在设计一款串流语音app Socket也是直接跟服务器 沟通的另一个选择 在串流中的语音对话期间 你可以在Watch app 使用HTTP实时串流或Web Socket 关于更多使用Socket的信息 请到开发者app或网站观看 《在watchOS 6上串流语音》 我们谈到了许多内容 现在就总结一下要如何从我们 讨论到的这些选项当中进行选择 针对可同步到一个人 所有装置里的小块敏感数据 请选择iCloud同步的钥匙串 若要在iCloud储存数据库 并跟一个人所有的装置共享 请选择CloudKit的CoreData 若要优化搭配的iPhone 和Watch app的使用体验 或是共享只能在 搭配app的其中一个装置上 取得的数据 就选择Watch Connectivity 要直接跟服务器沟通 请选择URL对话 针对串流语音app 你也可以使用Socket 若要支持使用家人共享设置 或使用移动网络数据传输的客户 请务必选择iCloud同步的钥匙串 CloudKit的CoreData URL对话 或者是Socket 请先考量数据类型 数据来源和目的地 以及你的客户观众 再去选择解决方案 找到适合这项工作的正确工具 别忘了在没有连接除错器的情况下 在装置上测试app 再进行配置 确认app在现实条件下做出的行为 谢谢你观看这支视频 认识我们在Watch app 数据传输方面提供的各种强大工具 我们等不及看看你接下来会设计什么了 [音乐]
-
-
4:20 - Password Autofill
struct LoginView: View { @State private var username = "" @State private var password = "" var body: some View { Form { TextField("User:", text: $username) .textContentType(.username) SecureField("Password", text: $password) .textContentType(.password) Button { processLogin() } label: { Text("Login") } Button(role: .cancel) { cancelLogin() } label: { Label("Cancel", systemImage: "xmark.circle") } } } private func cancelLogin() { // Implement your cancel logic here } private func processLogin() { // Implement your login logic here } }
-
6:25 - Store Item in Keychain
func storeToken(_ token: OAuth2Token, for server: String, account: String) throws { let query: [String: Any] = [ kSecClass as String: kSecClassInternetPassword, kSecAttrServer as String: server, kSecAttrAccount as String: account, kSecAttrSynchronizable as String: true, ] let tokenData = try encodeToken(token) let attributes: [String: Any] = [kSecValueData as String: tokenData] let status = SecItemUpdate(query as CFDictionary, attributes as CFDictionary) guard status != errSecItemNotFound else { try addTokenData(tokenData, for: server, account: account) return } guard status == errSecSuccess else { throw OAuthKeychainError.updateError(status) } }
-
7:59 - Add Item to Keychain
func addTokenData(_ tokenData: Data, for server: String, account: String) throws { let attributes: [String: Any] = [ kSecClass as String: kSecClassInternetPassword, kSecAttrServer as String: server, kSecAttrAccount as String: account, kSecAttrSynchronizable as String: true, kSecValueData as String: tokenData, ] let status = SecItemAdd(attributes as CFDictionary, nil) guard status == errSecSuccess else { throw OAuthKeychainError.addError(status) } }
-
8:25 - Retrieve Item from Keychain
func retrieveToken(for server: String, account: String) throws -> OAuth2Token? { let query: [String: Any] = [ kSecClass as String: kSecClassInternetPassword, kSecAttrServer as String: server, kSecAttrAccount as String: account, kSecAttrSynchronizable as String: true, kSecReturnAttributes as String: false, kSecReturnData as String: true, ] var item: CFTypeRef? let status = SecItemCopyMatching(query as CFDictionary, &item) guard status != errSecItemNotFound else { // No token stored for this server account combination. return nil } guard status == errSecSuccess else { throw OAuthKeychainError.retrievalError(status) } guard let existingItem = item as? [String : Any] else { throw OAuthKeychainError.invalidKeychainItemFormat } guard let tokenData = existingItem[kSecValueData as String] as? Data else { throw OAuthKeychainError.missingTokenDataFromKeychainItem } do { return try JSONDecoder().decode(OAuth2Token.self, from: tokenData) } catch { throw OAuthKeychainError.tokenDecodingError(error.localizedDescription) } }
-
9:39 - Remove Item from Keychain
func removeToken(for server: String, account: String) throws { let query: [String: Any] = [ kSecClass as String: kSecClassInternetPassword, kSecAttrServer as String: server, kSecAttrAccount as String: account, kSecAttrSynchronizable as String: true, ] let status = SecItemDelete(query as CFDictionary) guard status == errSecSuccess || status == errSecItemNotFound else { throw OAuthKeychainError.deleteError(status) } }
-
11:59 - Core Data SwiftUI View
import CoreData import SwiftUI struct CoreDataView: View { @Environment(\.managedObjectContext) private var viewContext @FetchRequest( sortDescriptors: [NSSortDescriptor(keyPath: \Setting.itemKey, ascending: true)], animation: .easeIn) private var settings: FetchedResults<Setting> var body: some View { List { ForEach(settings) { setting in SettingRow(setting) } } } }
-
23:04 - Background URL Session Configuration
class BackgroundURLSession: NSObject, ObservableObject, Identifiable { private let sessionIDPrefix = "com.example.backgroundURLSessionID." enum Status { case notStarted case queued case inProgress(Double) case completed case failed(Error) } private var url: URL /// Data to send with the URL request. /// /// If this is set, the HTTP method for the request will be POST var body: Data? /// Optional content type for the URL request var contentType: String? private(set) var id = UUID() /// The current status of the session @Published var status = Status.notStarted /// The downloaded data (populated when status == .completed) @Published var downloadedURL: URL? private var backgroundTasks = [WKURLSessionRefreshBackgroundTask]() private lazy var urlSession: URLSession = { let config = URLSessionConfiguration.background(withIdentifier: sessionID) // Set isDiscretionary = true if you are sending or receiving large // amounts of data. Let Watch users know that their transfers might // not start until they are connected to Wi-Fi and power. config.isDiscretionary = false config.sessionSendsLaunchEvents = true return URLSession(configuration: config, delegate: self, delegateQueue: nil) }() private var sessionID: String { "\(sessionIDPrefix)\(id.uuidString)" } /// Initialize the session /// - Parameter url: The URL for the Background URL Request init(url: URL) { self.url = url super.init() } }
-
24:22 - Enqueue the background data transfer
// This is a member of the BackgroundURLSession class in the example. // Enqueue the URLRequest to send in the background. func enqueueTransfer() { var request = URLRequest(url: url) request.httpBody = body if body != nil { request.httpMethod = "POST" } if let contentType = contentType { request.setValue(contentType, forHTTPHeaderField: "Content-type") } let task = urlSession.downloadTask(with: request) task.earliestBeginDate = nextTaskStartDate BackgroundURLSessions.sharedInstance().sessions[sessionID] = self task.resume() status = .queued }
-
25:45 - WatchKit Extension Delegate
class ExtensionDelegate: NSObject, WKExtensionDelegate { func applicationDidFinishLaunching() { // For Watch Connectivity, activate your WCSession as early as possible WatchConnectivityModel.shared.activateSession() } func applicationDidBecomeActive() { // Restart any tasks that were paused (or not yet started) while the application was inactive. If the application was previously in the background, optionally refresh the user interface. } func applicationWillResignActive() { // Sent when the application is about to move from active to inactive state. This can occur for certain types of temporary interruptions (such as an incoming phone call or SMS message) or when the user quits the application and it begins the transition to the background state. // Use this method to pause ongoing tasks, disable timers, etc. } func handle(_ backgroundTasks: Set<WKRefreshBackgroundTask>) { // Sent when the system needs to launch the application in the background to process tasks. Tasks arrive in a set, so loop through and process each one. for task in backgroundTasks { // Use a switch statement to check the task type switch task { case let backgroundTask as WKApplicationRefreshBackgroundTask: // Be sure to complete the background task once you’re done. backgroundTask.setTaskCompletedWithSnapshot(false) case let snapshotTask as WKSnapshotRefreshBackgroundTask: // Snapshot tasks have a unique completion call, make sure to set your expiration date snapshotTask.setTaskCompleted(restoredDefaultState: true, estimatedSnapshotExpiration: Date.distantFuture, userInfo: nil) case let connectivityTask as WKWatchConnectivityRefreshBackgroundTask: // Be sure to complete the connectivity task once you’re done. connectivityTask.setTaskCompletedWithSnapshot(false) case let urlSessionTask as WKURLSessionRefreshBackgroundTask: if let session = BackgroundURLSessions.sharedInstance() .sessions[urlSessionTask.sessionIdentifier] { session.addBackgroundRefreshTask(urlSessionTask) } else { // There is no model for this session, just set it complete urlSessionTask.setTaskCompletedWithSnapshot(false) } case let relevantShortcutTask as WKRelevantShortcutRefreshBackgroundTask: // Be sure to complete the relevant-shortcut task once you're done. relevantShortcutTask.setTaskCompletedWithSnapshot(false) case let intentDidRunTask as WKIntentDidRunRefreshBackgroundTask: // Be sure to complete the intent-did-run task once you're done. intentDidRunTask.setTaskCompletedWithSnapshot(false) default: // make sure to complete unhandled task types task.setTaskCompletedWithSnapshot(false) } } } }
-
26:43 - Connect the WatchKit Extension Delegate to the App
@main struct MyWatchApp: App { @WKExtensionDelegateAdaptor(ExtensionDelegate.self) var extensionDelegate @SceneBuilder var body: some Scene { WindowGroup { NavigationView { ContentView() } } } }
-
27:07 - Store the Background Refresh Task it can be completed
// This is a member of the BackgroundURLSession class in the example. // Add the Background Refresh Task to the list so it can be set to completed when the URL task is done. func addBackgroundRefreshTask(_ task: WKURLSessionRefreshBackgroundTask) { backgroundTasks.append(task) }
-
27:31 - Process Downloaded Data
extension BackgroundURLSession : URLSessionDownloadDelegate { private func saveDownloadedData(_ downloadedURL: URL) { // Move or quickly process this file before you return from this function. // The file is in a temporary location and will be deleted. } func urlSession(_ session: URLSession, downloadTask: URLSessionDownloadTask, didFinishDownloadingTo location: URL) { saveDownloadedData(location) // We don't need more updates on this session, so let it go. BackgroundURLSessions.sharedInstance().sessions[sessionID] = nil DispatchQueue.main.async { self.status = .completed } for task in backgroundTasks { task.setTaskCompletedWithSnapshot(false) } } }
-
-
正在查找特定内容?在上方输入一个主题,就能直接跳转到相应的精彩内容。
提交你查询的内容时出现错误。请检查互联网连接,然后再试一次。