天天看點

如何進行 iOS Widget 開發?

如何進行 iOS Widget 開發?

Widget 簡介

Widget 是 iOS 14 重磅推出的新功能,使得使用者可以在主螢幕添加小元件,快速浏覽 app 提供的重要資訊。它的設計與舊版本 macOS 的 Widget 一脈相承,甚至連添加的動畫也是去掉了拟物化的水波紋效果。

如何進行 iOS Widget 開發?

設計定位

使用者可以通過 Widget 對主螢幕進行個性化定制,但是 iOS 14 的 Widget 跟其他系統上的小元件有很大的差別。在 Widget 的設計上蘋果也保持了一貫的克制,定位于輕量化、僅用作關鍵資訊的展示。比如系統自帶 Widget 中的股票、天氣、電量、運動資訊,他們的共同特征是更新頻率高、提供的資訊重要,讓使用者不用打開 app 就可以浏覽關心的内容。蘋果也不希望開發者将 Widget 僅僅當作 app 的一個快捷入口,這樣的需求更适合用 contextual menu 來實作。

限制

蘋果基于上面的設計定位,同時也為了節省系統資源保證續航,對 Widget 的做了一些限制:

  • 不支援動畫,僅支援靜态頁面展示。
  • 更新頻率由系統通過機器學習來動态配置設定。
  • 不支援拖拽、滾動等複雜的互動,不支援 Switch 等控件。
  • 使用者點選 Widget 一定會跳轉到 app。

Widget 開發實踐

盒馬小鎮 Widget 設計

如何進行 iOS Widget 開發?

盒馬小鎮是盒馬 app 内的一個小遊戲,使用者可以通過簽到、購物、内容互動來擷取盒花,使用者最關心的就是有沒有盒花可以收取,或者是否有盒花可以幫摘等資訊。這正好可以契合 Widget 的設計初衷,我們期望能借此提升使用者粘性。

要實作一個 Widget 首先要更新到 Xcode 12,然後在工程中增加一個新的 Widget Extension,Xcode 會自動生成需要的模闆檔案。

我們現在要做的隻有兩件事:資料加載、渲染界面。

登入及授權

加載資料之前首先要解決的是授權問題。由于集團的登入 SDK 暫時不支援 extension,加上全家桶依賴複雜、體積很大,我們不能使用 MTOP 來進行 API 調用,是以我們設計了一套認證機制,主 app 在登入後調用 MTOP 接口擷取 token,并且将 token 寫入 app group 中,Widget 通過 HTTPS 接口加上 token 來通路使用者資料。

如何進行 iOS Widget 開發?

其中 token 通過 UserDefaults 來共享到 Widget,因為開發過程中不同的證書打包的 bundle id 不同,是以我們将 group name 設定成 group.[bundle id] 的形式,保證能正确讀取 token。

var token: String? {
    let bundleIdentifier = Bundle.main.bundleIdentifier!
    let defaults = UserDefaults(suiteName: "group." + bundleIdentifier)    
    return defaults?.object(forKey: "widget.town.homeinfo.token") as? String}           

Token 的同步有幾個時機:

  • App 啟動後,如果已經登入并且 app group 中沒有 token,則擷取 token。
  • 使用者進入盒馬小鎮時擷取 token。
  • 使用者登入時擷取 token。
  • 使用者登出時删除 token。

資料更新

如何進行 iOS Widget 開發?

這裡蘋果借用了 timeline 的概念,timeline 裡面包含了一個個 entry,entry 就是我們在特定時間需要展示的資料。系統先回調我們來擷取 timeline 資料,再在特定時間來回調我們渲染界面。

我們在傳回 timeline 時可以設定更新政策:

  • atEnd:在 timeline 中所有的 entry 都展示完之後更新。
  • never:僅在主 app 觸發更新。
  • after:在特定時間後觸發更新。

當然這些都隻是我們建議系統的更新的時機,實際是否更新還是取決于系統。對于可以預測資訊的,比如天氣預報,可以在 timeline 中添加多個 entry,并且選擇 atEnd 作為更新時機;對于不可以預測資訊的,比如股市,可以選擇 after 政策,在一定時間後更新資訊。

除此之外,我們還可以在主 app 中調用 WidgetCenter 的方法來觸發更新,經我們測試使用這種方法每次調用都是能夠真正觸發資料更新的。更多資訊可以參考官方文檔[1]。

在盒馬小鎮的場景下,我們是無法預知到将來的資料的,是以每次傳回的 timeline 中隻包含一個 entry,選取的政策是 after,每 20 分鐘更新一次資料。

值得注意的是,在 getSnapshot 方法中也要傳回真實的資料,這樣使用者在 Widget 的添加過程中看到的就是正确的界面,真正做到所見即所得。

如何進行 iOS Widget 開發?

在使用者在主 app 中收取盒花之後,我們還會主動觸發一次資料的更新。

渲染界面

蘋果為了推廣 SwiftUI,規定 Widget 隻能使用 SwiftUI 來編寫界面。SwiftUI 與 Flutter 類似,是随 iOS 13 推出一套聲明式的 UI 架構。SwiftUI 在這一年中有了很大的進步,補齊了很多能力上的不足。它使得跨平台(僅蘋果生态)的界面開發變得非常簡單。此外 SwiftUI 還支援 preview(類似 Flutter 的 hot reload),代碼的改動可以近乎實時地反應在 UI 上,極大提高了開發效率和體驗。

如果有 Flutter 開發經驗,隻要熟悉 VStack、HStack、ZStack 以及常見的幾個 modifier 就可以輕松上手。

如何進行 iOS Widget 開發?

Bundle 拆分

與集團的大多數 app 一樣,我們的 app 工程也分為殼工程和業務 bundle,我們不希望每次修改 Widget 都去修改殼工程,于是要把 Widget 的業務邏輯拆分開來。經過嘗試發現一個可行的辦法:

  • 建立一個 Swift framework bundle,将 Widget 實作放在裡面,并且将裡面的類設定成 public。
  • 在殼工程中建立一個 Widget Extension,引入上面的 framework,在裡面保留 @main 方法。
@main
struct HMTownWidget: Widget {

let kind: String = "HMTownWidget"

var body: some WidgetConfiguration {
    StaticConfiguration(kind: kind, provider: Provider()) { entry in
        HMTownWidgetEntryView(entry: entry)
    }
    .configurationDisplayName("盒馬小鎮")
    .description("快速檢視盒花動态,通路盒馬小鎮收盒花")
    .supportedFamilies([.systemSmall, .systemMedium])
}
}
           

這種方法可以減少殼工程改動的風險,并且可以正常使用 SwiftUI 的 preview 和調試功能,缺點是 Widget 的配置資訊仍在保留在殼工程中,暫時沒有更好的辦法在保證調試功能的同時完全分離殼工程和業務代碼。

Swift Bridge

前面提到在使用者摘取盒花之後,我們需要觸發 Widget 的更新,這裡需要調用 WidgetCenter 的 API,而這個 API 僅提供了 Swift 版本,而我們的主 app 是純 OC 寫的,需要做一個 bridge。要實作 bridge 必須開啟 Define Modules,而我們的工程由于曆史原因在開啟後無法編譯通過,現在的解決方法是建立一個 Swift framework,裡面調用 Widget Center API,再由業務去引用這個 bridge 的 framework。

埋點

Widget 的曝光事件我們是無法感覺的,由于點選 Widget 會直接跳轉到主 app,是以我們在跳轉到主 app 的 URL 上增加了埋點參數,主 app 解析 URL 中的參數調用 UT 來埋點。

稽核

在我們一開始打完包送出到 TF 稽核時被拒了:

如何進行 iOS Widget 開發?

理由是我們使用了無效的字型,這個字型是我們在很早的版本就已經依賴的 Flutter 三方庫引入的,之前的稽核也一直沒問題,在去掉了依賴之後成功過審,原因不明。Flutter 官方 issue[2] 提到隻有在增加了 Widget 之後才會遇到這個問題。

Swift 的副作用

目前盒馬 app 最低支援 iOS 9.0,而 iOS 直到 12.2 才将 Swift 基礎庫內建到系統中,是以我們需要在 Build Settings 裡面将“ALWAYS_EMBED_SWIFT_STANDARD_LIBRARIES”設定為 “YES” 才能在 12.2 及以下的系統上運作。

Swift 雖然在 5.0 版本完成了 ABI 穩定,但是在低版本作業系統中 (iOS 12.2 以下) 仍舊會有一些不夠完美的地方。

在低于 iOS12.2 以下的作業系統會帶來約有 3MB 的包大小問題,但幸運的是蘋果放開了蜂窩資料網絡下 200M 的下載下傳大小。

在低于 iOS12.2 以下的作業系統會有 100-200ms 不等的啟動耗時增加,但在團隊同學的努力下上線了二進制重排,啟動性能大幅度上升。具體分析請檢視:手淘架構組最新實踐 | iOS 基于靜态庫插樁的⼆進制重排啟動優化[3]。

——《手淘 Swift 2019 大事記》

好在 iOS 的更新率比較高,相信随着時間的推移 Swift 的應用會越來越多。

最後

目前新版本已經正式釋出(4.54.1),歡迎下載下傳體驗,App Clip 商品溯源也同步上線。現在的版本仍存在一些問題,比如使用者如果已經打開小鎮頁面,通過點選 Widget 打開 app 仍然會再次打開小鎮頁面,需要從路由的層面優化;中尺寸的卡片相對于小尺寸也并沒有透出更多的資訊,與蘋果的設計初衷相違背。不過也算是對新技術的一次嘗試,後面會持續優化,期待能産生業務上的一些增量。

相關連結

[1]

https://developer.apple.com/documentation/widgetkit/keeping-a-widget-up-to-date

[2]

https://github.com/flutter/flutter/issues/65991

[3]

https://mp.weixin.qq.com/s/YDO0ALPQWujuLvuRWdX7dQ