天天看点

当 Widget 遇到智能化

作者: kAzec, iOS 开发者,目前就职于字节跳动

在阅读本文前,推荐先对新的 Widget 系统有个大致的了解,同时也推荐先熟悉 Apple 在 SiriKit 中引入的 Intents API。

在 WWDC 2020 中,Apple 引入了 Widget (小挂件)这一全新的 App Extension,允许开发者在设备主屏幕、“今天”视图和 macOS 通知中心上显示自定义的小挂件。

当 Widget 遇到智能化

该 session 首先介绍了如何通过 Intents API,让我们开发的 Widget 支持让用户进行个性化配置,并介绍了目前支持的配置参数的类型,如何自定义参数类型,并且支持为用户动态生成待选项列表。

之后介绍了 Widget 支持在 Smart Stack 中堆叠展示,同时可以通过开发者的配置让 Widget 智能地被系统展现:

通过复用 <code>Intents</code> 的“捐赠(donate)”的概念,让用户在合适的时机看到 TA 需要的内容。

通过为特定的 <code>TimelineEntry</code> 指定相关性信息,让用户能及时看到开发者认为和当前用户高度相关的内容。

让我们开始吧~

当 Widget 遇到智能化

为了更好的解释相关概念,这个 session 使用了一个信用卡记账示例应用,这个应用实现了两个 Widget:

<code>RecentPurchases</code>: 显示某个信用卡账户最近的购买记录

<code>DueDate</code>: 显示某个信用卡账户消费额 &amp; 还款日期

当 Widget 遇到智能化
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])。

为了让用户可以自定义 <code>RecentPurchases</code> Widget 所显示的信用卡账户、对应的消费分类的功能,我们可以定义一个包含了两个参数的 Intent:

Card:Widget 所显示消费记录对应的信用卡账户

Category:Widget 所显示消费记录的特定分类

基于我们定义的这个 Intent,WidgetKit 会自动为我们生成如下图所示的配置界面,其中 Card 和 Category 参数分别对应一个配置行。

当 Widget 遇到智能化

在用户进行 Widget 配置时,WidgetKit 可以通过 Intent Handling Protocol 从我们的 Widget Extension 或 Host App 获取要显示的待选项列表(这一点在后面会详细讲到)。

最后,在获取 Widget Timeline 数据时,WidgetKit 会将用户所自定义的 Intent 实例传入我们的 Widget Extension,我们可以使用该 Intent 实例中的信息来返回个性化的 Timeline 数据。

Widget 支持配置从整型、字符串等基础类型到日期、URL 等高级类型的参数,同时支持自定义参数类型 &amp; 自定义枚举类型。

所有支持的类型配置可见下图:

当 Widget 遇到智能化

其中,特定的类型还有近一步的自定义选项来定制输入 UI。例如,Decimal 类型可以选择采用输入框(Number Field)输入或者是滑块(Slider)输入,同时可以定制输入的上下限;Duration 类型可以定制输入值的单位为秒、分或者时;Date Components 可以指定输入日期还是时间,指定日期的格式等等。

当 Widget 遇到智能化

除了系统自带的参数类型以外,也支持自定义参数类型。可以通过 <code>Add Type...</code> 添加自定义的参数类型,通过<code>Add Enum...</code> 添加自定义的枚举类型,Xcode 会自动生成/更新对应的 Swift 类型(在 Xcode 12 beta 中添加自定义类型后,大部分情况下需要重新 build 项目,或者重启 Xcode 才能看到生成的 Swift 类型????)。

当 Widget 遇到智能化

可能有读者会注意到,上图中显示的第二个自定义枚举类型 <code>Dynamic</code>,支持动态生成所显示的待选项列表,这个我们会在后面详细介绍。

大部分类型的参数支持输入多个值,即输入一个数组。同时,支持根据不同的 Widget 大小,限制数组的固定长度。

当 Widget 遇到智能化

下面,基于前述的信用卡实例应用中的 <code>RecentPurchases</code> Widget,对如何为 Widget 添加个性化配置的能力做逐步、详细的介绍。

<code>RecentPurchases</code> Widget 目前一次只能显示一张卡片的消费记录等信息,所以很自然,我们会想要让这个 Widget 支持自定义要显示的信用卡账号。同时,多个消费记录可能属于不同的类别(<code>category</code>),那么很自然的,我们可以让用户选择只显示某一个类别的消费记录。

所以,我们希望<code>RecentPurchases</code> Widget 可以支持自定义信用卡账号(<code>card</code>)以及消费类别(<code>category</code>)两个参数。

首先,我们在工程中的一个 Intent 定义文件中 (如项目中没有已有文件,可以通过 Choose File &gt; New &gt; File 选择 SiriKit Intent Definition File 添加)中,定义一个新的 Intent,叫做 <code>ViewRecentPurchasesIntent</code>。

当 Widget 遇到智能化
需要注意以下几点: Intent 的 Category 选择为 View(即用于展示/配置 UI) 选中 Intent is eligible for widgets 取消选中 Siri can ask value for run(除非该 Intent 也用于 Siri Shortcuts)

在定义完新的 Intent 类型后,Xcode 会自动生成对应的 Swift 类型文件(以及相对应的 <code>IntentHandling</code> 协议,见下),我们在 Widget Extension &amp; Host App 均可以使用这个 Intent 类型。(Xcode 12 beta 中添加新的 Intent 类型后可能需要重新 Build target,或者重启 Xcode 才能看到/使用新的类型)

在示例应用中的 <code>card</code> 参数,用户添加了那些信用卡账号只有运行时才知道,对于那些需要在运行时确定有哪些待选项的参数,我们可以选中 Options are provided dynamically 选项,并实现对应的 <code>IntentHandling</code> 协议。

一般我们通过创建一个 Intent Extension target 来处理和系统 Intents 相关的交互。 关于什么是 Intent Handling,如何提供某个 Intent 的 Handler 实现可以参考 SiriKit Programming Guide[5] 中的 Siri Intents 部分内容。

以之前定义的 <code>ViewRecentPurchasesIntent</code> 为例,Xcode 会自动生成一个 <code>ViewRecentPurchasesIntentHandling</code> 协议。通过指定一个 Handler 类,并实现下面两个方法:

当 Widget 遇到智能化

<code>provideCardOptionsCollection(for:with:)</code>在用户点击 Widget 中 Card 配置项的时候,WidgetKit 会展示上图右侧中的列表 UI,其中的数据由这个方法异步返回。

<code>defaultCard(for:)</code>我们可以通过实现该方法,在用户首次添加我们的 Widget 时,对于该 Widget 的某一个可配置项返回一个默认的参数值。例如在图示的实现中,我们返回了用户的主要信用卡(Primary Card)。

通过使用 <code>INObjectCollection(sections:)</code> 构造器,传入 <code>INObjectSection</code> 数组,可以分区展示待选项列表。 自定义 Intent 类型继承自 <code>INObject</code>,通过重载/设置 <code>displayString</code>、 <code>subtitleString</code> 等属性,可以定制自定义类型在待选项列表中的显示内容。 Intent Handling 协议中定义的 <code>defaultXXX(for:)</code> 方法被标记为 <code>optional</code>,但是依然推荐实现,因为一个好的默认视图对我们的 Widget 来说是十分重要的。

你可能注意到了待选项列表上方的搜索框。默认情况下,搜索框会对我们所返回的全部内容进行搜索过滤。但是,当待选数据较多,或者说待选数据取决于用户具体输入时,我们可以打开 Intent handler provides search results as the user types 选项,实现对待选项列表的实时更新。

在打开该选项后,Xcode 会为生成的 <code>IntentHandling</code> 协议的 <code>provideCardOptionsCollection(for:with:)</code> 方法添加一个 <code>searchTerm</code> 参数:

当 Widget 遇到智能化

当用户在搜索框中输入字符时,<code>WidgetKit</code> 会调用该方法对待选项列表进行更新。首次显示待选项列表时,该参数值为 nil。

现在我们定义了用于配置 <code>RecentPurchases</code> Widget 的 Intent 类型,同时实现了对应的 <code>IntentHandling</code> 协议。下面我们可以将 <code>RecentPurchases</code> Widget 切换至 Intent-based API 用以展示用户自定义的内容。

从 <code>StaticConfiguration</code> 切换至 <code>IntentConfiguration</code>,并传入所配置的 Intent 类型(示例中为 <code>ViewRecentPurchasesIntent.self</code>)。

从 <code>TimelineProvider</code> 切换至 <code>IntentTimelineProvider</code>,并更新相关的方法(<code>snapshot(for:with:completion:)</code>、<code>timeline(for:with:completion:)</code>),添加 <code>intent</code> 参数,修改实现,使用 <code>intent</code> 参数中的配置信息,返回个性化的 <code>TimelineEntry</code> 数据。

当 Widget 遇到智能化

通过对 <code>WidgetConfiguration</code> 添加下面两个 <code>modifier</code>,自定义配置界面标题、描述文案。

当 Widget 遇到智能化

通过在 Widget Extension target 的 Build Settings 页面中,配置 Widget 的 Global Accent Color Name 和  Widget Background Color Name,自定义配置界面的强调色和背景色。对应的颜色资源需要添加在 target 中的 Assets Catalog 中。

当 Widget 遇到智能化

你可以控制某一个配置项,只在另一个配置项含有任何/特定值时展示。如下图,日历 App 的 Up Next Widget,仅在 Mirror Calendar App 选项没有被选中时,才会显示 Calendars 配置项。

当 Widget 遇到智能化

在 Intent 定义文件中,将某一个参数 A,设置为另一个参数 B 的 Parent Parameter,这样,参数 B 的显示与否就取决于参数 A 的值。

例如,在下图中,<code>calendar</code> 参数仅在 <code>mirrorCalendarApp</code> 参数的值为 <code>false</code> 时展示:

当 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 提供了两种机制来实现上述目标。

在 iOS 12 中,Apple 引入了 Siri Shortcuts &amp; Custom intent donations(参见 Introduction to Siri Shortcuts[6])。当用户在宿主 App 内进行某一操作时,App 可以主动 <code>donate</code> 一个 Intent 实例告知系统用户进行了此操作,从而让系统了解用户的行为规律。这一信息过去被用于在 Spotlight 中预测用户可能进行的操作,而在 iOS 14 中,同样的信息也可以让系统预测 Smart Stack 中某一 Widget 适合的展示时机。

对于 Widget 来说,利用这一机制的前提是实现了可配置化的能力。Widget 的可配置化是基于自定义 Intent 类型实现的,那么同样的 Intent 也可以被 <code>donate</code> 给系统。

以前述的 <code>RecentPurchases</code> Widget 为例,我们希望系统能掌握用户查看特定信用卡消费记录的规律,并在合适的时机展示对应卡片的 Widget,下面进行逐步介绍:

将 Widget 对应的 Intent 标记为 Intent is eligible for Siri Suggestions。

当 Widget 遇到智能化

在 Suggestions 部分添加 Supported Combinations,我们只关注用户所查看的信用卡账户,所以添加一个只有 <code>card</code> 参数的 combination。

当 Widget 遇到智能化

在宿主 App 内显示对应的信用卡消费记录时,创建并 <code>donate</code> 一个 <code>ViewRecentPurchasesIntent</code>。

当 Widget 遇到智能化

让系统理解用户在 App 内的行为的关键在于配置合适的 Supported Combinations,直译过来是**“支持的参数组合”**。通过配置一个或多个参数组合,我们可以告诉系统用户在 App 内的行为的哪些特征(特征由对应的 Intent Parameter 决定)是值得关注的。

例如在上面的 Demo 实现中,我们制定了包含一个参数 <code>card</code> 的参数组合,那么系统会提取所有包含同一 <code>card</code> 参数的行为数据,归纳总结出用户在特定的时间,查看某一信用卡账户信息的行为规律。而在匹配最合适的 Widget 时,相应的会去查找所有配置展示了对应卡片的 Widget,不论该 Widget 的另一个参数 <code>category</code> 的取值。

当 Widget 遇到智能化

例如上图的例子中,系统最后匹配到了两个 Widget:Acme Card - Grocery 和 Aceme Card - Travel。

而如果我们在参数组合中添加 category,则在匹配 Widget 时,会同时考虑两个参数,最后匹配到 Acme Card - Grocery:

当 Widget 遇到智能化

在之前的学习中我们已经了解了如何使用 <code>TimelineProvider/IntentTimelineProvider</code> 来提供不同时间点的 Widget 渲染所需的数据。

<code>Timeline</code> 数据由一个个 <code>TimelineEntry</code> 组成,一个 <code>TimelineEntry</code> 除了包含 Widget 对应显示的时刻信息、 Widget 渲染的内容数据外,还可以包含一个 <code>TimelineEntryRelevance</code> 对象,用来表示这个 entry 的相关性。

当 Widget 遇到智能化

<code>TimelineEntryRelevance</code> 信息包含两个 <code>score</code> 和 <code>duration</code> 两个属性:

A value that indicates the relevance of an entry compared to other entries in the past.

<code>score</code> 值的高低,反应了在对应的时间点,Widget 所展示的内容和用户的相关程度(或者说用户可能感兴趣的程度、对用户的重要程度)。

例如以前述的 <code>RecentPurchases</code> 挂件为例,我们将 <code>score</code> 值设置为要展示的消费记录的金额,那么相应的,金额越大的消费记录越有可能被系统展示给用户。

当 Widget 遇到智能化

需要注意的是,<code>score</code> 值是一个相对值,它不会用来和别的 App 所提供的值进行比较,只会用于和过去该 App 所提供过的所有值进行比较。

当 <code>score</code> 值小于或等于0时,系统认为对应时刻的 Widget 内容对用户来说是完全不重要的(比如显示为空占位图视图时),所以不会主动展示该 Widget 。

The length of time following the entry's date that the widget has the relevance score set.

简而言之,<code>duration</code> 值代表了一个 <code>TimelineEntry</code> 的用户相关性所持续的时长。这个持续过程可以跨越多个 <code>entries</code>,直到下一个指定了 <code>non-nil relevance</code> 值的 <code>TimelineEntry</code> 把它覆盖,又或者是指定的持续时间结束。

例如,下图所示体育比赛信息挂件中,在 6:30 指定了一个长达三小时的相关性信息,由于后续(6:40、7:02、9:30)的 <code>TimelineEntry</code> 并没有提供新的 <code>relevance</code> 信息,那么后续的挂件展示依然会继承之前所指定的相关性数据。

当 Widget 遇到智能化

当 <code>duration</code> 的值为 0 时,WidgetKit 认为该 entry 对应内容的相关性会持续到下一个提供了 <code>relevance</code> 信息的 <code>TimelineEntry</code> 被展示。"

当 Widget 遇到智能化

本文分享自微信公众号 - 老司机技术周报(LSJCoding)。

如有侵权,请删除。