初探 Widget Extension in iOS 14

Sunny Cheng
8 min readJul 6, 2020

--

Widget為一 Extension,使用SwiftUI建立。使用者一天平均會按 90 次 Home 鍵,但只會花很短的時間在 Home Screen 上,因此 UI 需快速呈現,而非一直出現載入中的圖示影響體驗。

另外 widget 特性為只能讓使用者讀取,無法有 scroll 或 switch 等 UI 互動,意即不能使用 List,也無法播放動態圖片或影片,只能接受點擊行為。

設計特點

  1. Glanceable:可讓使用者一下子就清楚得到資訊
  2. Relevant:資訊與本身 App 有關聯
  3. Personalted:與使用者相關的,個人化設定可吸引使用者加入 widget

支援尺寸(Families)

函式架構

預設顯示畫面 (PlaceHolderUI / snapshot)

顯示時機:系統環境改變時,例如系統設定時區改變,或者在工具列選擇 widgets 時。

應避免使用數據,而是使用灰影方塊等來表示會字串或圖片顯示區域

更新時機 (Reload)

  1. Networking:Background Notification 呼叫時
  2. Timeline:Developers 給予時間軸去自動更新
  3. App-based:當使用者進入 App 做改變時
三種可更新 widget 的時機

時間軸 (Timeline)

使用時間軸告知下次 reload 時間,iOS 系統會依照設定背景下載。

當需要更新時,iOS將會訪問 TimelineProvider,並依據 TimelineProvider 所提供的 TimelineEntry 去執行,每個 TimelineEntry 物件將會含有 date and time 以及顯示 widget UI 所需的額外資訊。

Policy:

  1. atEnd:當 TimelineProvider 提供 TimelineEntry 為陣列的最後一個時,系統將會要求新的 Timeline。
  2. after(Date):當時間到期時抓取新的 Timeline。
  3. never:不再抓取,直到 WidgetCenter 要求 reload。

Networking & App-based

當使用者進入 App 並產生會改變 Widget 狀態的行為時,通知 WidgetCenter 更新 timeline 與 widget 介面。

ofKind 輸入 widget 識別碼

若 App 支援複數的 widgets 時,可將支援的 widgets 全部更新

當 widget 有提供使用者設定,根據設定不一定要更新時,可以先檢查設定再去 reloadTimelines

顯示動態時間

因為 widget 畫面並非隨時更新,但如果要顯示倒數或計時,使用 “Text“ view,讓 widget 顯示正確的時間在螢幕上。

顯示設定時間與現在時間的差距
顯示特定時間
顯示區間

使用者點選 (Deep Linking)

使用者點選 Widget 時,導入至 App 互動顯示更多資訊。iOS 將會傳送一 URL,啟動 App 後根據 URL 做處理

黃框為可點擊範圍

使用者設定 (Configuration)

可提供選項讓使用者客製化要顯示在 widget 上的資訊

使用者設定 Widget
  1. Static Configuration:預設值,不提供使用者設定
  2. Intent Configuration:提供 SiriKit Intent Definition File
在 Parameters 欄位加入選項

當新增 setting 在實機測試時,無法叫出使用者選單或沒有正確顯示更新,可將 Widget 從桌面移除再重新加入。

多個 Widgets 堆疊 (Smart Stacks)

可讓使用者將不同 widgets 堆疊在一起,由 iOS 系統根據時間且使用者習慣及相關性決定最上顯示的 widget。

單個 App 支援複數 Widgets

Main App 共享資料

當不同 App 間或 App 與其 Extension 需要共享資料時,可開 AppGroup(注意必須在同一個開發者帳號下),並有以下三種方式可共享資料。(AppGroup建立

當在實機測試時,記得至 TARGET -> Capabilities -> App Group 設定 shared container。

*UserDefaults:

let wantToGet = UserDefaults(suiteName: <your_app_group>)!.string(forKey: "keyName")

*File Container:

With the AppGroup entitlement you get access to the shared File Container

存入

let url = FileManager.default.containerURL(forSecurityApplicationGroupIdentifier: "yourapp.contents")?.appendingPathComponent("widget")
let data = Data("Widget Title".utf8)
try! data.write(to: url!)

讀取

let url = FileManager.default.containerURL(forSecurityApplicationGroupIdentifier: "yourapp.contents")?.appendingPathComponent("widget")
let data = try! Data(contentsOf: url!)
let string = String(data: data, encoding: .utf8)!
print(string) // 印出 "Widget Title"

*CoreData:

Core Data with SwiftUI Tutorial

let storeURL = containerURL.appendingPathComponent("DataModel.sqlite")
let description = NSPersistentStoreDescription(url: storeURL)

let container = NSPersistentContainer(name: "DataModel")
container.persistentStoreDescriptions = [description]
container.loadPersistentStores { ... }

圖片異步下載

SwiftUI 畫畫面的時候為 synchronous,所以無法在顯示畫面時執行異步任務,例如下載圖片。故使用第三方套件如 SDWebImageSwiftUI、Kingfisher 等皆無法正常運作。

若要在 Widget 顯示網頁下載的圖片,有兩個方式

  1. 在製作 TimelineProvider 時,預先把圖片載好,直接給予畫面 Image。

--

--

Sunny Cheng

礦冶工程碩士,職涯第一個轉彎為新加坡市場的業務經理,自學後又轉彎成 OTT 產業的 iOS 工程師。