作者:雪人少校(William),iOS 开发者,现就职于字节跳动音乐团队。
Session: https://developer.apple.com/videos/play/wwdc2020/10036/
文中涉及的demo在这里[1]
整个《Widgets 边看边写》系列中的其他文章:《Widgets 边看边写》第一部分:冒险开始了《Widgets 边看边写》第二部分:Timelines 的基本使用
开始之前,我们先介绍下面三个对象(在本系列之前的 session 中已经提到过,但因为它们相对抽象,且能体现 Widget 的设计思想,为了方便理解,我们用简单的语言再描述一下)
-
Timeline:时间线,是一个关于更新策略的封装,里面包装了若干TimelineEntry
-
TimelineEntry:时间线策略的控制点,主要用来控制,更新的具体时间点(会结合Timeline的其他属性)
-
TimelineProvider:提供Timeline的那个对象,系统通过它拿到Widget的Timeline
TimelineEntry 是最本质的那个对象,用来控制Widget更新时机
按苹果一贯控制且克制的风格,关键事情要拿在自己手里,在整个 iOS 系统中进行平衡——对于 Widget 来说,“关键的事情”是电量 & 流量的开销
这种机制确保了一个 Widget 不能随意刷新自己的内容,而是要通过有计划,有规律的方式来更新
特别注意:
TimelineEntry里面的Date不是一个精确的时间,只是一种给系统的建议,系统根据自身情况来决定是否需要更新,极端情况下,比如低电,甚至不能保证更新
本篇讲动态配置,如果没有看之前部分,可以把demo下载下来直接从第三部分开始
本篇 Widget:Leadbard Widget —— 一个将角色按 health 排列的 Widget,同时点击每个角色头像能够唤起主app,并进入该角色的详情页面,先整体看一下:
主App | Widget |
| |
URL Session
Widget可以通过网络请求来获取数据 —— TimelineProvider 是通过回调工作的,这样会让网络处理比较简单
直接看代码(入口 CharacterDetail.loadLeaderboardData)
主旨是:
- 1 这里其实就是一个普通的 URLSession,利用 dataTask 的方式请求这个 URL,然后在回调里处理返回的数据
- 2 这里把 fauxResponse(其实就是个字符串,没啥神秘的)写入了一个在临时目录的文件 “userData.json”
Q:但这是什么操作?!写个文件,然后请求文件 URL,然后在回调里处理文件数据?!
A:这里没问题,且说明了一件事:Widget 里可以调用 URLSession 系列的 API,间接证明了 Widget 是有网络能力的
说到这里,我们仔细想一下,为啥要在 demo 里证明 Widget 有哪些能力?
了解过 Extension 开发的同学应该明白,苹果有好多 extension 类型:
ShareExtension | 点击“信息” |
| |
时间上先有虎嗅 App 进程,然后才有“信息”界面,我们合理推论一下
-
-
-
从安全角度考虑,利用进程间的页表隔离,彻底隔离两个进程的数据(符合苹果一贯的谨慎)
无论哪种,都说明 extension 的进程和他的属主 app 的进程并不一定是同一个
之所以不一定,看 Extension 的类型,反例是 NotificationExtension
回到主题,Widget 也是 Extension,它有自己的运行时环境,且在 iPhone/iPad 这样的用电池的设备上,在能用的 API 集合上必然是受限的!
Demo 刚刚说明了,URLSession 的 API 是可以放心使用的
理解了这些,后面很多内容就比较 make sense 了!
Background Session
用过后台下载特性的同学都知道,background session 在完成的时候,可选后台唤起主 app,从 AppDelegate 的这两个回调
不理解 Background Session 的同学去看看这里,这里我们不展开
对于 Widget 来说,Widget(进程)同样有能力处理 Background Session 回调
在没有 AppDelegate 的情况下,Widget 要怎么做呢?
答案是:在 StaticConfiguration 中,有一个配置 “onBackgroundURLSessionEvents”,可以注册一些回调,可以用来代替 AppDelegate 的角色
比如,注册一个处理回调
Widget唤起宿主App
想要做到这样的效果,应该怎么做?
该 SwiftUI Link API 出场了,从 AllCharactersView 讲起(我们 Leaderboard Widget 的主要视图)
这里嵌入了一个 Link(角色的url)
Q:就是 Deeplink 而已,有什么特殊的?A:是 Deeplink,但不是通过 URLScheme 做的
感兴趣的读者可以去工程里求证
看这里(注意这是主 App 的视图,不是 Widget)
链接匹配则上面的 NavationLink 会通过 SwiftUI 的数据绑定,显示对应的界面
关于数据绑定的细节,可以参考这篇[2]
Widget Bundle
目前,我们在Widget Gallery里还只能看到一个Widget
就是被 @main 标识的那个Widget
但一个 Widget Bundle 可以容纳多个 Widget,@main 可以移动到 bundle 这里
⚠️:从这里开始,我们看的是 Demo 里 Game Status Final 目录下的工程
现在能看到多个 Widget 了
动态配置 Widget
此 Target 主要是给 Widget 提供配置项目,下面详细说
Widget 可以有一些静态配置,在 part2 里介绍过
视频有点抽象,补一张图,“编辑小组件”就是这里所讨论的配置
一个配置可以通过 XCode 增加一个 IntentDefinition 来定义
不要被 SiriKit 带偏了,我们后面讨论的东西和 Siri 没有直接关系
这里之所以会出现 SiriKit 的身影,是因为现代版本的 iOS 里,Springboard 和 SiriKit 是分不开的,所以 Springboard 上的配置很多都和 SiriKit 相关
那这个配置到底干嘛的?
定义点了编辑之后,用户能看的项目
讲动态之前,先看静态配置:
注意几个细节
-
-
-
静态配置一般是通过Enum来表达,那故名思议,这当然是静态配置
-
再看Enum
一系列固定值,符合预期
再看看动态配置
这和刚刚的静态配置看起来一样的?
静态配置 | 动态配置 |
| |
发现了吗?配置项的类型不同
这里在视频中没有详细说,但有必要了解清楚,那按苹果工具的思路,我们尝试探索一下:点这里
然后对新加的参数调整一下类型
>
答案了出现了!
为了逻辑上的连贯,在这里稍微展开介绍一下视频里没提到的知识点:
XCode 对于 intentdefinition 文件是有自动代码生成的,我们点定义看一下(注意上方文件导航)
没有工程路径!说明这是一个生成的类
Show in Finder 看实际路径
一路追上去,是 DerivedData
对于动态配置,这里是关键
然后我们看一下 Hero 属性,看起来是一个形式化定义,是不是又生成代码了?
是的,读者有兴趣可以点Hero自行查看
让我们回到主线,打开 IntentHandler.swift 文件看一眼:
把两个集合合并好后,给系统回调
但系统怎么知道是 IntentHandler.swift 文件?
iOS 开发里代码逻辑的上找不出关联的东西,找 Info.plist
看到了吧?
remoteCharacters 的内容在最后
到这里,视频就完了,但为了理解上的完整,我们再说一下如果用户真的选择了某个角色会发生什么:
我们开篇就说了这个类——提供 Timeline 的那个对象
Widget 配置中有它
Provider 有个回调
public func timeline(for configuration: DynamicCharacterSelectionIntent, with context: Context, completion: @escaping (Timeline) -> Void) {
let selectedCharacter = character(for: configuration)
let endDate = selectedCharacter.fullHealthDate
let oneMinute: TimeInterval = 60
var currentDate = Date()
var entries: [SimpleEntry] = []
while currentDate < endDate {
let relevance = TimelineEntryRelevance(score: Float(selectedCharacter.healthLevel))
let entry = SimpleEntry(date: currentDate, relevance: relevance, character: selectedCharacter)
currentDate += oneMinute
entries.append(entry)
}
let timeline = Timeline(entries: entries, policy: .atEnd)
completion(timeline)
}
注意第 2 行
func character(for configuration: DynamicCharacterSelectionIntent) -> CharacterDetail {
let name = configuration.hero?.identifier
return CharacterDetail.characterFromName(name: name)
}
会去拿 hero.identifier,然后和自己定义的角色匹配,匹配到了就会更新界面!
所以,当用户选择了 Widget 的具体配置项目之后,会触发这里的刷新
func timeline(for configuration: DynamicCharacterSelectionIntent, with context: Context, completion: @escaping (Timeline) -> Void)
总结
终于讲完了,为了理解上的完整性,本文做了很多扩展性的说明,感谢各位读者的耐心阅读;这是 Widget 系列的最后一篇,WidgetKit 的概念很棒,也非常好用!希望大家能真正读懂原理,创造更多优秀的 Widget!
推荐阅读
✨ Apple Widget:下一个顶级流量入口?
为 Widgets 构建 SwiftUI 视图
《Widgets 边看边写》第一部分:冒险开始了
《Widgets 边看边写》第二部分:Timelines 的基本使用
关注我们
我们是「老司机技术周报」,每周会发布一份关于 iOS 的周报,也会定期分享一些和 iOS 相关的技术。欢迎关注。
支持作者
这篇文章的内容来自于 《WWDC20 内参》。在这里给大家推荐一下这个专栏,专栏目前已经创作了 99 篇文章,只需要 29.9 元。点击【阅读原文】,就可以购买继续阅读 ~
WWDC 内参 系列是由老司机周报、知识小集合以及 SwiftGG 几个技术组织发起的。已经做了几年了,口碑一直不错。 主要是针对每年的 WWDC 的内容,做一次精选,并号召一群一线互联网的 iOS 开发者,结合自己的实际开发经验、苹果文档和视频内容做二次创作。
参考资料
[1]演示 Demo: https://developer.apple.com/documentation/widgetkit/building_widgets_using_widgetkit_and_swiftui
[2]数据绑定: https://xiaozhuanlan.com/topic/0528764139