天天看點

當 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)。

如有侵權,請删除。