当 Widget 遇到智能化
作者: kAzec, iOS 开发者,目前就职于字节跳动
Sessions: https://developer.apple.com/videos/play/wwdc2020/10194/
在阅读本文前,推荐先对新的 Widget 系统有个大致的了解,同时也推荐先熟悉 Apple 在 SiriKit 中引入的 Intents API。
概述
在 WWDC 2020 中,Apple 引入了 Widget (小挂件)这一全新的 App Extension,允许开发者在设备主屏幕、“今天”视图和 macOS 通知中心上显示自定义的小挂件。
该 session 首先介绍了如何通过 Intents API,让我们开发的 Widget 支持让用户进行个性化配置,并介绍了目前支持的配置参数的类型,如何自定义参数类型,并且支持为用户动态生成待选项列表。
之后介绍了 Widget 支持在 Smart Stack 中堆叠展示,同时可以通过开发者的配置让 Widget 智能地被系统展现:
-
通过复用 Intents
的“捐赠(donate)”的概念,让用户在合适的时机看到 TA 需要的内容。 -
通过为特定的 TimelineEntry
指定相关性信息,让用户能及时看到开发者认为和当前用户高度相关的内容。
让我们开始吧~
知识导图
基本概念
为了更好的解释相关概念,这个 session 使用了一个信用卡记账示例应用,这个应用实现了两个 Widget:
-
RecentPurchases
: 显示某个信用卡账户最近的购买记录 -
DueDate
: 显示某个信用卡账户消费额 & 还款日期
Tips:
Apple 并没有放出该 session 中演示的 demo 的源码,但是,另一 Widgets Code-along[1] 系列 session 中,所演示的 Emoji Rangers demo 提供了源码[2],同时也实现了本文所描述的配置能力,可以用作参考。
Widget 的可配置的参数列表是使用 Intents 进行配置的。Intents 是 Apple 在 WWDC 2016 引入的新概念,了解 SiriKit 的开发者对此肯定不会感到陌生(参见 Introducing SiriKit[3] 和 Introduction to Siri Shortcuts[4])。
为了让用户可以自定义 RecentPurchases
Widget 所显示的信用卡账户、对应的消费分类的功能,我们可以定义一个包含了两个参数的 Intent:
-
Card:Widget 所显示消费记录对应的信用卡账户 -
Category:Widget 所显示消费记录的特定分类
基于我们定义的这个 Intent,WidgetKit 会自动为我们生成如下图所示的配置界面,其中 Card 和 Category 参数分别对应一个配置行。
在用户进行 Widget 配置时,WidgetKit 可以通过 Intent Handling Protocol 从我们的 Widget Extension 或 Host App 获取要显示的待选项列表(这一点在后面会详细讲到)。
最后,在获取 Widget Timeline 数据时,WidgetKit 会将用户所自定义的 Intent 实例传入我们的 Widget Extension,我们可以使用该 Intent 实例中的信息来返回个性化的 Timeline 数据。
Widget 支持配置的参数类型
Widget 支持配置从整型、字符串等基础类型到日期、URL 等高级类型的参数,同时支持自定义参数类型 & 自定义枚举类型。
所有支持的类型配置可见下图:
支持自定义输入模式
其中,特定的类型还有近一步的自定义选项来定制输入 UI。例如,Decimal 类型可以选择采用输入框(Number Field)输入或者是滑块(Slider)输入,同时可以定制输入的上下限;Duration 类型可以定制输入值的单位为秒、分或者时;Date Components 可以指定输入日期还是时间,指定日期的格式等等。
支持自定义参数类型
除了系统自带的参数类型以外,也支持自定义参数类型。可以通过 Add Type...
添加自定义的参数类型,通过Add Enum...
添加自定义的枚举类型,Xcode 会自动生成/更新对应的 Swift 类型(在 Xcode 12 beta 中添加自定义类型后,大部分情况下需要重新 build 项目,或者重启 Xcode 才能看到生成的 Swift 类型😅)。
可能有读者会注意到,上图中显示的第二个自定义枚举类型 Dynamic
,支持动态生成所显示的待选项列表,这个我们会在后面详细介绍。
支持输入多个值
大部分类型的参数支持输入多个值,即输入一个数组。同时,支持根据不同的 Widget 大小,限制数组的固定长度。
为 Widget 添加个性化配置能力
下面,基于前述的信用卡实例应用中的 RecentPurchases
Widget,对如何为 Widget 添加个性化配置的能力做逐步、详细的介绍。
确定 Widget 可配置的选项
RecentPurchases
Widget 目前一次只能显示一张卡片的消费记录等信息,所以很自然,我们会想要让这个 Widget 支持自定义要显示的信用卡账号。同时,多个消费记录可能属于不同的类别(category
),那么很自然的,我们可以让用户选择只显示某一个类别的消费记录。
所以,我们希望RecentPurchases
Widget 可以支持自定义信用卡账号(card
)以及消费类别(category
)两个参数。
在 Xcode 中自定义 Intent 类型
首先,我们在工程中的一个 Intent 定义文件中 (如项目中没有已有文件,可以通过 Choose File > New > File 选择 SiriKit Intent Definition File 添加)中,定义一个新的 Intent,叫做 ViewRecentPurchasesIntent
。
需要注意以下几点:
Intent 的 Category 选择为 View(即用于展示/配置 UI)
选中 Intent is eligible for widgets
取消选中 Siri can ask value for run(除非该 Intent 也用于 Siri Shortcuts)
在定义完新的 Intent 类型后,Xcode 会自动生成对应的 Swift 类型文件(以及相对应的 IntentHandling
协议,见下),我们在 Widget Extension & Host App 均可以使用这个 Intent 类型。(Xcode 12 beta 中添加新的 Intent 类型后可能需要重新 Build target,或者重启 Xcode 才能看到/使用新的类型)
实现 IntentHandling
协议
在示例应用中的 card
参数,用户添加了那些信用卡账号只有运行时才知道,对于那些需要在运行时确定有哪些待选项的参数,我们可以选中 Options are provided dynamically 选项,并实现对应的 IntentHandling
协议。
一般我们通过创建一个 Intent Extension target 来处理和系统 Intents 相关的交互。
关于什么是 Intent Handling,如何提供某个 Intent 的 Handler 实现可以参考 SiriKit Programming Guide[5] 中的 Siri Intents 部分内容。
以之前定义的 ViewRecentPurchasesIntent
为例,Xcode 会自动生成一个 ViewRecentPurchasesIntentHandling
协议。通过指定一个 Handler 类,并实现下面两个方法:
-
provideCardOptionsCollection(for:with:)
在用户点击 Widget 中 Card 配置项的时候,WidgetKit 会展示上图右侧中的列表 UI,其中的数据由这个方法异步返回。 -
defaultCard(for:)
我们可以通过实现该方法,在用户首次添加我们的 Widget 时,对于该 Widget 的某一个可配置项返回一个默认的参数值。例如在图示的实现中,我们返回了用户的主要信用卡(Primary Card)。
Tips:
通过使用 INObjectCollection(sections:)
构造器,传入INObjectSection
数组,可以分区展示待选项列表。自定义 Intent 类型继承自 INObject
,通过重载/设置displayString
、subtitleString
等属性,可以定制自定义类型在待选项列表中的显示内容。Intent Handling 协议中定义的 defaultXXX(for:)
方法被标记为optional
,但是依然推荐实现,因为一个好的默认视图对我们的 Widget 来说是十分重要的。
你可能注意到了待选项列表上方的搜索框。默认情况下,搜索框会对我们所返回的全部内容进行搜索过滤。但是,当待选数据较多,或者说待选数据取决于用户具体输入时,我们可以打开 Intent handler provides search results as the user types 选项,实现对待选项列表的实时更新。
在打开该选项后,Xcode 会为生成的 IntentHandling
协议的 provideCardOptionsCollection(for:with:)
方法添加一个 searchTerm
参数:
当用户在搜索框中输入字符时,WidgetKit
会调用该方法对待选项列表进行更新。首次显示待选项列表时,该参数值为 nil。
切换至 Intent-based API
现在我们定义了用于配置 RecentPurchases
Widget 的 Intent 类型,同时实现了对应的 IntentHandling
协议。下面我们可以将 RecentPurchases
Widget 切换至 Intent-based API 用以展示用户自定义的内容。
-
从
StaticConfiguration
切换至IntentConfiguration
,并传入所配置的 Intent 类型(示例中为ViewRecentPurchasesIntent.self
)。 -
从
TimelineProvider
切换至IntentTimelineProvider
,并更新相关的方法(snapshot(for:with:completion:)
、timeline(for:with:completion:)
),添加intent
参数,修改实现,使用intent
参数中的配置信息,返回个性化的TimelineEntry
数据。
自定义 Widget 配置界面
-
通过对
WidgetConfiguration
添加下面两个modifier
,自定义配置界面标题、描述文案。 -
通过在 Widget Extension target 的 Build Settings 页面中,配置 Widget 的 Global Accent Color Name 和 Widget Background Color Name,自定义配置界面的强调色和背景色。对应的颜色资源需要添加在 target 中的 Assets Catalog 中。
控制配置项的显示条件
你可以控制某一个配置项,只在另一个配置项含有任何/特定值时展示。如下图,日历 App 的 Up Next Widget,仅在 Mirror Calendar App 选项没有被选中时,才会显示 Calendars 配置项。
在 Intent 定义文件中,将某一个参数 A,设置为另一个参数 B 的 Parent Parameter,这样,参数 B 的显示与否就取决于参数 A 的值。
例如,在下图中,calendar
参数仅在 mirrorCalendarApp
参数的值为 false
时展示:
让 Widget 智能地被系统展现
在 iOS 14 中,随着 Widget 一并引入的还有 Smart Stack,即 Widget 智能堆栈。用户可以将多个 Widget 堆叠显示,通过上下滑动切换正在显示的 Widget。
为了让堆栈能够在合适的时机展示合适的 Widget,Apple 引入了一套类似 Siri Suggestions 的,基于 Intents donation 以及 Relevance 内容相关性的竞标机制。
Widget 智能三要素
-
Timely:Widget 应该在合适的时机,展示用户感兴趣的内容。 -
Glanceable:Widget 展示的内容应该是简洁、直观、一目了然的。 -
Obvious value:Widget 应该展示对用户来说最有价值/相关性高的内容。
一个优秀的 Widget 应该是一目了然的,并会在合适的时机,提供用户最感兴趣的内容。
例如,对于一个天气 App 来说,某个用户可能习惯在早上 8:00 左右打开天气 App 查看当日天气,那么我们希望用户在每天早上 8:00 打开手机时,能够在主屏幕上直接看到天气 App 的 Widget。
此外,我们也希望在有雷雨天气时,主动在 Smart Stack 中显示天气 App 的 Widget,让用户对恶劣天气做好准备。
为此,WidgetKit 提供了两种机制来实现上述目标。
基于用户行为(Custom Intent donations)
在 iOS 12 中,Apple 引入了 Siri Shortcuts & Custom intent donations(参见 Introduction to Siri Shortcuts[6])。当用户在宿主 App 内进行某一操作时,App 可以主动 donate
一个 Intent 实例告知系统用户进行了此操作,从而让系统了解用户的行为规律。这一信息过去被用于在 Spotlight 中预测用户可能进行的操作,而在 iOS 14 中,同样的信息也可以让系统预测 Smart Stack 中某一 Widget 适合的展示时机。
对于 Widget 来说,利用这一机制的前提是实现了可配置化的能力。Widget 的可配置化是基于自定义 Intent 类型实现的,那么同样的 Intent 也可以被 donate
给系统。
Demo
以前述的 RecentPurchases
Widget 为例,我们希望系统能掌握用户查看特定信用卡消费记录的规律,并在合适的时机展示对应卡片的 Widget,下面进行逐步介绍:
-
将 Widget 对应的 Intent 标记为 Intent is eligible for Siri Suggestions。 -
在 Suggestions 部分添加 Supported Combinations,我们只关注用户所查看的信用卡账户,所以添加一个只有 card
参数的 combination。 -
在宿主 App 内显示对应的信用卡消费记录时,创建并 donate
一个ViewRecentPurchasesIntent
。
Under the hood
让系统理解用户在 App 内的行为的关键在于配置合适的 Supported Combinations,直译过来是**“支持的参数组合”**。通过配置一个或多个参数组合,我们可以告诉系统用户在 App 内的行为的哪些特征(特征由对应的 Intent Parameter 决定)是值得关注的。
例如在上面的 Demo 实现中,我们制定了包含一个参数 card
的参数组合,那么系统会提取所有包含同一 card
参数的行为数据,归纳总结出用户在特定的时间,查看某一信用卡账户信息的行为规律。而在匹配最合适的 Widget 时,相应的会去查找所有配置展示了对应卡片的 Widget,不论该 Widget 的另一个参数 category
的取值。
例如上图的例子中,系统最后匹配到了两个 Widget:Acme Card - Grocery 和 Aceme Card - Travel。
而如果我们在参数组合中添加 category,则在匹配 Widget 时,会同时考虑两个参数,最后匹配到 Acme Card - Grocery:
App 主动提供相关性信息(Providing Relevance info)
在之前的学习中我们已经了解了如何使用 TimelineProvider/IntentTimelineProvider
来提供不同时间点的 Widget 渲染所需的数据。
Timeline
数据由一个个 TimelineEntry
组成,一个 TimelineEntry
除了包含 Widget 对应显示的时刻信息、 Widget 渲染的内容数据外,还可以包含一个 TimelineEntryRelevance
对象,用来表示这个 entry 的相关性。
TimelineEntryRelevance
信息包含两个 score
和 duration
两个属性:
score
A value that indicates the relevance of an entry compared to other entries in the past.
score
值的高低,反应了在对应的时间点,Widget 所展示的内容和用户的相关程度(或者说用户可能感兴趣的程度、对用户的重要程度)。
例如以前述的 RecentPurchases
挂件为例,我们将 score
值设置为要展示的消费记录的金额,那么相应的,金额越大的消费记录越有可能被系统展示给用户。
需要注意的是,score
值是一个相对值,它不会用来和别的 App 所提供的值进行比较,只会用于和过去该 App 所提供过的所有值进行比较。
当 score
值小于或等于0时,系统认为对应时刻的 Widget 内容对用户来说是完全不重要的(比如显示为空占位图视图时),所以不会主动展示该 Widget 。
duration
The length of time following the entry's date that the widget has the relevance score set.
简而言之,duration
值代表了一个 TimelineEntry
的用户相关性所持续的时长。这个持续过程可以跨越多个 entries
,直到下一个指定了 non-nil relevance
值的 TimelineEntry
把它覆盖,又或者是指定的持续时间结束。
例如,下图所示体育比赛信息挂件中,在 6:30 指定了一个长达三小时的相关性信息,由于后续(6:40、7:02、9:30)的 TimelineEntry
并没有提供新的 relevance
信息,那么后续的挂件展示依然会继承之前所指定的相关性数据。
当 duration
的值为 0 时,WidgetKit 认为该 entry 对应内容的相关性会持续到下一个提供了 relevance
信息的 TimelineEntry
被展示。"
推荐阅读
《Widgets 边看边写》第二部分:Timelines 的基本使用
《Widgets 边看边写》第三部分:Timelines的进阶使用
关注我们
我们是「老司机技术周报」,每周会发布一份关于 iOS 的周报,也会定期分享一些和 iOS 相关的技术。欢迎关注。
支持作者
这篇文章的内容来自于 《WWDC20 内参》。在这里给大家推荐一下这个专栏,专栏目前已经创作了 101 篇文章,只需要 29.9 元。点击【阅读原文】,就可以购买继续阅读 ~
WWDC 内参 系列是由老司机周报、知识小集合以及 SwiftGG 几个技术组织发起的。已经做了几年了,口碑一直不错。 主要是针对每年的 WWDC 的内容,做一次精选,并号召一群一线互联网的 iOS 开发者,结合自己的实际开发经验、苹果文档和视频内容做二次创作。
参考资料
Widgets Code-along: https://developer.apple.com/news/?id=yv6so7ie
[2]源码: https://developer.apple.com/documentation/widgetkit/building_widgets_using_widgetkit_and_swiftui
[3]Introducing SiriKit: https://developer.apple.com/videos/play/wwdc2016/217/
[4]Introduction to Siri Shortcuts: https://developer.apple.com/videos/play/wwdc2018/211
[5]SiriKit Programming Guide: https://developer.apple.com/documentation/sirikit#//apple_ref/doc/uid/TP40016875
[6]Introduction to Siri Shortcuts: https://developer.apple.com/videos/play/wwdc2018/211
本文分享自微信公众号 - 老司机技术周报(LSJCoding)。
如有侵权,请联系 support@oschina.cn 删除。
本文参与“OSC源创计划”,欢迎正在阅读的你也加入,一起分享。

低调大师中文资讯倾力打造互联网数据资讯、行业资源、电子商务、移动互联网、网络营销平台。
持续更新报道IT业界、互联网、市场资讯、驱动更新,是最及时权威的产业资讯及硬件资讯报道平台。
转载内容版权归作者及来源网站所有,本站原创内容转载请注明来源。
- 上一篇
【性能优化实战】宝宝知道小程序FMP优化实录
背景 宝宝知道小程序从首次发布至今,经过了几十个版本的迭代。随着业务发展,页面功能内容的不断增多,相关性能数据不断变差,核心性能数据 FMP 长期处在 2000ms 以上。 在该项目之前,我们团队也对小程序做了一定的性能调优工作,内容包括: 包体积优化,去除了不少引用在项目中的图片素材文件,将包体积优化至 500kb 以下; 联合后端对耗时较高的业务接口做优化,单个接口返回速度需要控制在 100ms 左右; 优化了部分业务逻辑,小程序启动时减少了一些不必要的操作逻辑; 使用了小程序框架提供的最新生命周期 onInit ,可提前 100ms 左右发起业务网络请求; 使用 prelink 预连接网络,提升数据接口的请求效率。 经过上述手段之后,FMP 降到了 1900ms 左右,后续再也无法产生优化效果。 以上优化手段,基本排除了网络连接,包体积优化不到位引起的性能不佳。那么我们就只有一个问题需要仔细排查 —— 内容的渲染效率。 问题发现 目前从手百上打开宝宝知道小程序的最大入口页面为问答页,整体 pv 占比超过 6 成,那么我们优先优化这个页面,便可以带来性能收益的最大化。 通读问答页代...
- 下一篇
10分钟搞定 Java 并发队列好吗?好的
| 好看请赞,养成习惯 你有一个思想,我有一个思想,我们交换后,一个人就有两个思想 If you can NOT explain it simply, you do NOT understand it well enough 现陆续将Demo代码和技术文章整理在一起 Github实践精选 ,方便大家阅读查看,本文同样收录在此,觉得不错,还请Star🌟 前言 如果按照用途与特性进行粗略的划分,JUC 包中包含的工具大体可以分为 6 类: 执行者与线程池 并发队列 同步工具 并发集合 锁 原子变量 在【并发系列】中,主要讲解了 执行者与线程池,同步工具,锁 , 在分析源码时,或多或少的提及到了「队列」,队列在 JUC 中也是多种多样存在,所以本文就以「远看」视角,帮助大家快速了解与区分这些看似「杂乱」的队列 并发队列 Java 并发队列按照实现方式来进行划分可以分为 2 种: 阻塞队列 非阻塞队列 如果你已经看完并发系列锁的实现,你已经能够知道他们实现的区别: 前者就是基于锁实现的,后者则是基于 CAS 非阻塞算法实现的 常见的队列有下面这几种: 瞬间懵逼?看到这个没有人性的图想直接走人...
相关文章
文章评论
共有0条评论来说两句吧...
文章二维码
点击排行
推荐阅读
最新文章
- CentOS7安装Docker,走上虚拟化容器引擎之路
- MySQL8.0.19开启GTID主从同步CentOS8
- 设置Eclipse缩进为4个空格,增强代码规范
- Springboot2将连接池hikari替换为druid,体验最强大的数据库连接池
- Red5直播服务器,属于Java语言的直播服务器
- SpringBoot2全家桶,快速入门学习开发网站教程
- Windows10,CentOS7,CentOS8安装Nodejs环境
- SpringBoot2更换Tomcat为Jetty,小型站点的福音
- Docker使用Oracle官方镜像安装(12C,18C,19C)
- Eclipse初始化配置,告别卡顿、闪退、编译时间过长