網易雲音樂 iOS 14 小組件實戰手冊

題圖
圖片來源: https://unsplash.com/photos/a...html

本文做者: 閆冰

前言

蘋果在今年的 WWDC20 上發佈了小組件(WidgetKit),支持在 iOS、iPadOS 主屏幕展現動態信息和個性化內容。加上 iOS 系統應用抽屜的加入,蘋果對一貫保守主屏幕大動干戈,致使用戶也對小組件很是期待。但小組件的運行限制不少,如何在有限的機制上怎樣作好用戶體驗就成爲須要完成的挑戰。前端

小組件簡述

小組件能夠在主屏幕上實現內容展現和功能跳轉。
系統會向小組件獲取時間線,根據當前時間對時間線上的數據進行展現。點擊正在展現的視覺元素能夠跳轉到APP內,實現對應的功能。
雲音樂的小組件效果以下:
previewios

開發思路淺談

Widget 技術棧
首先須要明確的是小組件是一個獨立於 App 環境(即 App Extension),小組件的生命週期/存儲空間/運行進程都和 App 不一樣。因此咱們須要引入這個環境下的一些基礎設施,好比網絡通訊框架,圖片緩存框架,數據持久化框架等。
小組件自己的生命週期是一個頗有意思的點。直白的來說小組件的生命週期是和桌面進程一致的,但這不意味着小組件能隨時的執行代碼完成業務。小組件使用 Timeline 定義好的數據來渲染視圖,咱們的代碼只能在刷新 Timeline (getTimeline)和建立快照(getSnapshot)時執行。通常而言,在刷新 Timeline 時獲取網絡數據,在建立快照時渲染合適的視圖。
大多數狀況下都須要使用數據來驅動視圖展現。這個數據能夠經過網絡請求得到,也能夠利用 App Groups 的共享機制從 App 中獲取。
在刷新 Time Line 時獲取到數據後,便可按照業務需求合成 Timeline。Timeline 是一個以 TimelineEntry 爲元素的數組。 TimelineEntry 包含一個 date 的時間對象,用以告知系統在什麼時候使用此對象來建立小組件的快照。也能夠繼承 TimelineEntry ,加入業務所須要的數據模型或其餘信息。
爲了使小組件展現視圖,須要用 SwiftUI 來完成對小組件的佈局和樣式搭建。在下面會介紹如何實現佈局和樣式。
在用戶點擊小組件後,會打開 App,並調用 AppDelegateopenURL: 方法。咱們須要在 openURL: 中處理這個事件,使用戶直接跳轉至所需的頁面或調用某個功能。
最後,若是須要開放給用戶小組件的自定義選項,則使用 Intents 框架,預先定義好數據結構,並在用戶編輯小組件提供數據,系統會根據數據來繪製界面。用戶選擇的自定義數據都會在刷新 Time Line (getTimeline)和建立快照(getSnapshot)時以參數的形式提供出來,以後根據不一樣的自定義數據執行不一樣的業務邏輯便可。git

App Extension

若是你已經有了 App Extension 的開發經驗,能夠略過這個章節。
按照蘋果的說法:App Extension 能夠將自定義功能和內容擴展到應用程序以外,並在用戶與其餘應用程序或系統交互時向用戶提供。例如,您的應用能夠在主屏幕上顯示爲小部件。也就是說小組件是一種 App Extension,小組件的開發工做,基本都在 App Extension 的環境中。
App 和 App Extension有什麼關係?
本質上是兩個獨立的程序,你的主程序既不能夠訪問 App Extension 的代碼,也不能夠訪問其存儲空間,這完徹底全就是兩個進程、兩個程序。App Extension 依賴你的 App 本體做爲載體,若是將 App 卸載,那麼 App Extension 也不會存在於系統中了。並且 App Extension 的生命週期大多數都做用於特定的領域,根據用戶觸發的事件由系統控制來管理。github

建立 App Extension 和配置文件

下面簡述一下如何建立小組件的 App Extension並配置證書環境。
在 Xcode 中新增一個 Widget Extension(路徑以下:File-New-Target-iOS選項卡-Widget Extension)。若是你須要小組件的自定義功能,則不要忘記勾選 Include Configuration Intent算法

建立 App Extension 第一步
在 Widget Extension 的 Target 中添加 App Groups,並保持和主程序相同的 App Group ID 。若是主程序中尚未App Groups,則須要這個時候同時增長主 App 的 App Groups,並定義好 Group ID。
建立 App Extension 第二步
若是你的開發者帳號登陸在 Xcode 中,那麼此時應用程序的配置文件和 App ID 等配置都會是正確的。若是你沒有登陸 Xcode 中,則須要前往蘋果開發者中心,手動建立 App Extension 的 App ID 和配置文件。此時不要忘記在 App ID 中配置 App Groups。swift

App Groups 數據通訊

由於 App 和 App Extension 是不能直接通信的,因此須要共享信息時,須要使用 App Groups 來進行通信。App Groups 有兩種共享數據的方式,NSUserDefaultsNSFileManagerapi

NSUserDefaults 共享數據

使用 NSUserDefaults 的 initWithSuiteName: 初始化實例。 suitename傳入以前定義好的 App GroupID。數組

- (instancetype)initWithSuiteName:(NSString *)suitename;

以後便可使用NSUserDefaults的實例的存取方法來儲存和獲取共享數據了。好比咱們須要和小組件共享當前的用戶信息,則能夠以下操做。瀏覽器

//使用 Groups ID 初始化一個供 App Groups 使用的 NSUserDefaults 對象
NSUserDefaults *userDefaults = [[NSUserDefaults alloc] initWithSuiteName:@"group.company.appGroupName"];
//寫入數據
[userDefaults setValue:@"123456789" forKey:@"userID"];
//讀取數據
NSString *userIDStr = [userDefaults valueForKey:@"userID"];

NSFileManager 共享數據

使用 NSFileManager 的 containerURLForSecurityApplicationGroupIdentifier: 獲取 App Group 共享的儲存空間地址,便可進行文件的存取操做。

- (NSURL *)containerURLForSecurityApplicationGroupIdentifier:(NSString *)groupIdentifier;

SwiftUI 構建組件

應該是基於耗電量等方面的考量,蘋果要求小組件只能使用 SwiftUI ,也不能經過 UIViewRepresentable 橋接 UIKit 來使用。
小組件的交互方式簡單,只有點擊,且視圖較小。開發所須要的 SwiftUI 知識比較簡單,合理構建出小組件視圖便可,通常而言不會涉及到數據綁定等操做。
這個章節主要介紹如何使用 SwiftUI 構建小組件,我會假設讀者已經有了對 SwiftUI 的基礎知識。若是你對 SwiftUI 還較爲陌生,能夠經過參考資料中的兩個視頻教程來增進了解(【十五分鐘搞懂SwiftUI】佈局篇/【十五分鐘搞懂SwiftUI】樣式篇)。也能夠查閱開發文檔或者 WWDC19/20 的相關專題獲取 SwiftUI 更多知識。

使用 SwiftUI 完成小組件視圖

下面使用一個簡單的開發例子,來幫助你們使用 SwiftUI 開發小組件視圖。
首先看小組件的視覺稿:
小組件視覺稿
簡單分析一下視覺稿中的視圖元素:

  1. 鋪滿所有的背景圖片(Image)
  2. 從底部至上的黑色漸變(LinearGradient)
  3. 右上角的雲音樂 Logo(Image)
  4. 小組件中間的日曆圖標(Image)
  5. 日曆圖標下面兩行文字(Text)

經過分析,不難發現要實現視覺稿的效果,須要使用 TextImageLinearGradient 三個組件便可完成。
將視覺元素 1/2/3 歸爲背景視圖,方便其餘組件複用。隨後把組件內容類型相關的 4/5 歸爲前景視圖。
小組件視圖分析
先來實現背景視圖:

struct WidgetSmallBackgroundView: View {
 
 // 底部遮罩的佔比爲總體高度的 40%
 var contianerRatio : CGFloat = 0.4
 
 // 背景圖片
 var backgroundImage : Image = Image("backgroundImageName")
 
 // 從上到下的漸變顏色
 let gradientTopColor = Color(hex:0x000000, alpha: 0)
 let gradientBottomColor = Color(hex:0x000000, alpha: 0.35)
 
 // 遮罩視圖 簡單封裝 使代碼更爲直觀
 func gradientView() -> LinearGradient {
 return LinearGradient(gradient: Gradient(colors: [gradientTopColor, gradientBottomColor]), startPoint: .top, endPoint: .bottom)
 }
 
 var body: some View {
 // 使用 GeometryReader 獲取小組件的大小
 GeometryReader{ geo in
 // 使用 ZStack 疊放 logo 圖標 和 底部遮罩
 ZStack{
 // 構建 logo 圖標, 使用 frame 肯定圖標大小, 使用 position 定位圖標位置
 Image("icon_logo")
 .resizable()
 .scaledToFill()
 .frame(width: 20, height: 20)
 .position(x: geo.size.width - (20/2) - 10 , y : (20/2) + 10)
 .ignoresSafeArea(.all)
 // 構建 遮罩視圖, 使用 frame 肯定遮罩大小, 使用 position 定位遮罩位置
 gradientView()
 .frame(width: geo.size.width, height: geo.size.height * CGFloat(contianerRatio))
 .position(x: geo.size.width / 2.0, y: geo.size.height * (1 - CGFloat(contianerRatio / 2.0)))
 }
 .frame(width: geo.size.width, height: geo.size.height)
 // 添加上覆蓋底部的背景圖片
 .background(backgroundImage
 .resizable()
 .scaledToFill()
 )
 }
 }
}

背景視圖完成的效果以下圖:
小組件背景視圖
接下來把背景視圖放置在小組件的視圖中,並實現中間的圖標和文案視圖,這樣就完成了整個組件的視覺構建過程:

struct WidgetSmallView : View {
 
 // 設置大圖標的寬高爲小組件高度的 40%
 func bigIconWidgetHeight(viewHeight:CGFloat) -> CGFloat {
 return viewHeight * 0.4
 }
 
 var body: some View {
 
 GeometryReader{ geo in
 VStack(alignment: .center, spacing : 2){
 Image("iconImageName")
 .resizable()
 .scaledToFill()
 .frame(width: bigIconWidgetHeight(viewHeight: geo.size.height), height: bigIconWidgetHeight(viewHeight: geo.size.height))
 
 Text("每日推薦")
 .foregroundColor(.white)
 .font(.system(size: 15))
 .fontWeight(.medium)
 .lineLimit(1)
 .frame(height: 21)
 
 Text("爲你帶來每日驚喜")
 .foregroundColor(.white)
 .font(.system(size: 13))
 .fontWeight(.regular)
 .opacity(0.8)
 .lineLimit(1)
 .frame(height: 18)
 }
 // 增長 padding 使 Text 過長時不會觸及小組件邊框
 .padding(EdgeInsets(top: 0, leading: 14, bottom: 0, trailing: 14))
 .frame(width: geo.size.width, height: geo.size.height, alignment: .center)
 // 設置背景視圖
 .background(WidgetSmallBackgroundView())
 }
 }
}

經過上述簡單的例子能夠發現,在常規的流式佈局中,使用 VStackHStack 便可達到佈局效果。而若是想要實現例子中 logo 圖標的效果的話,就須要使用 position/offset 來改變定位座標來達成目標了。

關於 Link 視圖的一點補充

Link 是一個能夠點擊的視圖,若是可能的話,它將在關聯的應用程序中打開,不然將在用戶的默認Web瀏覽器中打開。中/大尺寸的小組件能夠用它來給點擊區域設定不一樣的跳轉參數。由於上面的例子是小尺寸的組件,不能使用 Link 來區分跳轉,因此在這裏補充一下。

Link("View Our Terms of Service", destination: URL(string: "https://www.example.com/TOS.html")!)

獲取數據

網絡請求

小組件中可使用 URLSession,因此網絡請求和 App 中基本一致,在此就不贅述了。
須要注意的點:

  1. 使用第三方框架須要引入小組件所在的 Target。
  2. 在刷新 Timeline 時調用網絡請求。
  3. 若是須要和 App 共享信息,則須要經過 App Group 的方式存取。

圖片的加載緩存

圖片緩存則和 App 中不一樣。目前在 SwiftUI 中的 Image 視圖不支持傳入 URL 加載網絡圖片。也不能使用異步獲取網絡圖片的 Data的方式完成網絡圖片的加載。
只能經過刷新 Timeline ,調用網絡請求完成後,再去獲取 Timeline 上全部的網絡圖片的 data

func getTimeline(for configuration: Intent, in context: Context, completion: @escaping (Timeline<Entry>) -> ()) {
 // 發起網絡請求
 widgetManager.requestAPI(family : context.family, configuration: configuration) { widgetResponse, date in
 // 在接口回調中生成 Timeline entry
 let entry = WidgetEntry(date: Date(), configuration: configuration, response: widgetResponse, family : context.family)
 // 解析出 Timeline entry 所須要的網絡圖片
 let urls = entry.urlsNeedDownload()
 // 查詢本地緩存以及下載網絡圖片
 WidgetImageManager().getImages(urls: urls) {
 let entries = [entry]
 let timeline = Timeline(entries: entries, policy: .after(date))
 completion(timeline)
 }
 }
 }

getImages 方法中,咱們須要維護一個隊列去依次查詢本地緩存以及在緩存未命中時下載網絡圖片。

public func getImages(urls : [String] , complition : @escaping () -> ()){
 
 // 建立目錄
 WidgetImageManager.createImageSaveDirIfNeeded()
 
 // 去重
 let urlSet = Set(urls)
 let urlArr = Array(urlSet)
 
 self.complition = complition
 
 self.queue = OperationQueue.main
 self.queue?.maxConcurrentOperationCount = 2
 let finishBlock = BlockOperation {
 self.complition?()
 }
 
 for url in urlArr {
 let op = SwiftOperation { finish in
 self.getImage(url: url) {
 finish(true)
 }
 }
 
 finishBlock.addDependency(op)
 self.queue?.addOperation(op)
 }
 
 self.queue?.addOperation(finishBlock)
 }
 
 public func getImage(url : String , complition : @escaping () -> ()) -> Void {
 let path = WidgetImageManager.pathFromUrl(url: url)
 if FileManager.default.fileExists(atPath: path) {
 complition()
 return
 }
 
 let safeUrl = WidgetImageManager.filterUrl(url: url)
 WidgetHttpClient.shareInstance.download(url: safeUrl, dstPath: path) { (result) in
 complition()
 }
 }

預覽狀態的數據獲取

在用戶添加小組件時,會在預覽界面看到小組件的視圖。此時,系統會觸發小組件的 placeholder 方法,咱們須要在這個方法中返回一個 Timeline,用以渲染出預覽視圖。
爲了保證用戶的體驗,須要爲接口調用準備一份本地的兜底數據,確保用戶能夠在預覽界面看到真實的視圖,儘可能不要展現無數據的骨架屏。
PreviewStatus

TimeLine

小組件的內容變化都依賴於 Timeline 。小組件本質上是 Timeline 驅動的一連串靜態視圖。

理解 TimeLine

在前面提到過,Timeline 是一個以 TimelineEntry 爲元素的數組。 TimelineEntry 包含一個 date 的時間對象,用以告知系統在什麼時候使用此對象來建立小組件的快照。也能夠繼承 TimelineEntry ,加入業務所須要的數據模型或其餘信息。
TimeLine
在生成新的 Timeline 以前,系統會一直使用上一次生成的 Timeline 來展現數據。
若是 Timeline 數組裏面只有一個 entry ,那麼視圖就是一成不變的。假如須要小組件隨着時間產生變化,能夠在 Timeline 中生成多個 entry 並賦予他們合適的時間,系統就會在指定的時間使用 entry 來驅動視圖。

Reload

所謂的小組件刷新,實際上是刷新了 Timeline ,致使由 Timeline 數據驅動的小組件視圖發生了改變。
刷新方法分爲兩種:

  1. System reloads
  2. App-driven reloads

System reloads

由系統發起的 Timeline 刷新。系統決定每一個不一樣的 Timeline 的 System Reloads 的頻次。超過頻次的刷新請求將不會生效。高頻使用的小組件能夠得到更多的刷新頻次。
ReloadPolicy:
在生成 Timeline 時,咱們能夠定義一個 ReloadPolicy ,告訴系統更新 Timeline 的時機。ReloadPolicy 有三種形式:

  • atEnd

    • 在 Timeline 提供的全部 entry 顯示完畢後刷新,也就是說只要還有沒有顯示的 entry 在就不會刷新當前時間線

ReloadPolicyAtEnd

  • after(date)

    • date 是指定的下次刷新的時間,系統會在這個時間對 Timeline 進行刷新。

ReloadPolicyAfter

  • never

    • ReloadPolicy 永遠不會刷新 Timeline,最後一個 entry 也展現完畢以後 小組件就會一直保持那個 entry 的顯示內容

ReloadPolicyNever

Timeline Reload 的時機是由系通通一控制的,而爲了保證性能,系統會根據各個 Reload 請求的重要等級來決定在某一時刻是否按照 APP 要求的刷新時機來刷新 Timeline。所以若是過於頻繁的請求刷新 Timeline,頗有可能會被系統限制從而不能達到理想的刷新效果。換句話說,上面所說的 atEnd, after(date) 中定義的刷新 Timeline 的時機能夠看做刷新 Timeline 的最先時間,而根據系統的安排,這些時機可能會被延後。

App-driven reloads

由 App 觸發小組件 Timeline 的刷新。當 App 在後臺時,後臺推送能夠觸發 reload;當 App 在前臺時,經過 WidgetCenter 能夠主動觸發 reload 。
App-driven Reloads
調用 WidgetCenter 能夠根據 kind 標識符刷新部分小組件,也能夠刷新所有小組件。

/// Reloads the timelines for all widgets of a particular kind.
/// - Parameter kind: A string that identifies the widget and matches the
///   value you used when you created the widget's configuration.
public func reloadTimelines(ofKind kind: String)
/// Reloads the timelines for all configured widgets belonging to the
/// containing app.
public func reloadAllTimelines()

點擊落地

用戶點擊了小組件上的內容或功能入口時,須要在打開 App 後正確響應用戶的需求,呈現給用戶相應的內容或功能。
這須要分兩部分來作,首先在小組件中對不一樣的點擊區域定義不一樣的參數,以後在 App 的 openURL: 中根據不一樣的參數呈現不一樣的界面。

區分不一樣的點擊區域

想要對於不一樣的區域定義不一樣的參數,須要把 widgetURL 和 Link 結合使用。

widgetURL

widgetURL 做用範圍是整個小組件,且一個小組件上只能有一個 widgetURL 。多添加的 widgetURL 參數是不會生效的。
widgetURL
代碼以下:

struct WidgetLargeView : View {
 var body: some View {
 GeometryReader{ geo in
 WidgetLargeTopView()
 ...
 }
 .widgetURL(URL(string: "jump://Large")!)
 }
}

Link

Link 做用範圍是 Link 組件的實際大小。能夠添加多個 Link ,在數量上是沒有限制的。須要注意的是小組件的 systemSmall 類型下,不能使用 Link API。
Link
代碼以下:

struct WidgetLargeView : View {
 var body: some View {
 GeometryReader{ geo in
 WidgetLargeTopView()
 Link(destination: URL(string: "自定義的Scheme://Unit")!) {
 WidgetLargeUnitView()
 }
 ...
 }
 .widgetURL(URL(string: "自定義的Scheme://Large")!)
 }
}

URL Schemes

URL Schemes 是小組件跳轉到 App 的橋樑,也是 App 之間相互跳轉的通道。通常的開發者對其應該並不陌生。
註冊自定義 URL Scheme 很是簡單,經過 info.plist --> URL Types --> item0 --> URL Schemes --> 自定義的Scheme 來設置。
以後,在小組件中,便可經過 自定義的Scheme:// 拼接成的 URL 對象來打開本身的 App ,在 :// 後面能夠增長參數來代表所須要功能或內容。
須要注意:增長參數時,出現的中文要進行轉義。這裏可使用 NSURLComponentsNSURLQueryItem 來拼接跳轉 URL 字符串。自帶轉義效果且操做 URL 更加規範。

NSURLComponents *components = [NSURLComponents componentsWithString:@"自定義的Scheme://"];
NSMutableArray<NSURLQueryItem *> *queryItems = @[].mutableCopy;
NSURLQueryItem *aItem = [NSURLQueryItem queryItemWithName:@"a" value:@"參數a"];
[queryItems addObject:aItem];
NSURLQueryItem *bItem = [NSURLQueryItem queryItemWithName:@"b" value:@"參數b"];
[queryItems addObject:bItem];
components.queryItems = queryItems;
NSURL *url = components.URL;

落地 App 後的處理

點擊小組件跳轉 App 後會觸發 AppDelegate 的 openURL 方法。

- (BOOL)application:(UIApplication *)app openURL:(NSURL *)url options:(NSDictionary<UIApplicationOpenURLOptionsKey,id> *)options

在 openURL 方法中,經過解析 url 參數,明確用戶須要的功能跳轉或內容的展現,隨後進行對應的實現。這對項目的路由能力提出了必定的要求,因和小組件開發聯繫不大,不作詳述。

動態配置小組件

小組件支持用戶在不打開應用的狀況下配置自定義數據,使用 Intents 框架,能夠定義用戶在編輯小組件時看到的配置頁面。
這裏用的詞的定義而不是繪製,是由於只能經過 Intents 來生成配置數據,系統會根據生成的數據來構建配置頁面。
ConfigurationWidget

構建一個簡單的自定義功能

構建一個簡單的自定義功能須要兩步:

  1. 建立和配置 IntentDefinition 文件
  2. 修改 Widget 的相關參數支持 ConfigurationIntent 。

1. 建立和配置 IntentDefinition 文件

若是你在建立小組件 Target 時勾選了 Include Configuration Intent ,Xcode 會自動生成 IntentDefinition 文件。
假如沒有勾選 Include Configuration Intent 選項,那麼你須要手動添加 IntentDefinition 文件。
菜單 File -> New -> File 而後找到 Siri Intent Definition File 以後添加到小組件 Target 中。
IntentDefinition1
建立文件後,打開 .intentdefinition 文件進行配置。
IntentDefinition2
首先須要記住左側的 Custom Class 中的類名,Xcode 會根據這個名稱,在編譯後自動生成一個 ConfigurationIntent 類,這個類儲存了用戶配置信息。固然這裏也能夠填寫一個你指定的類名,須要注意項目編譯事後纔會生成這個類。
而後咱們須要建立自定義參數模板,點擊Parameter 下方的 + 號便可建立一個參數。
以後能夠定義建立出的 Parameter 的 Type ,除了相對直觀的系統類型之外,還有兩個比較難以理解的 Enums 和 Types 分欄。
IntentDefinition3
系統類型
特定的類型有近一步的自定義選項來定製輸入 UI。例如,Decimal 類型能夠選擇採用輸入框(Number Field)輸入或者是滑塊(Slider)輸入,同時能夠定製輸入的上下限;Duration 類型能夠定製輸入值的單位爲秒、分或者時;Date Components 能夠指定輸入日期仍是時間,指定日期的格式等等。
IntentDefinitionSystemType
Enum
簡單的理解就是 Enums 是寫死在 .intentdefinition 文件中的靜態配置,只有發版才能夠更新。
Type
Types 就靈活多了,能夠在運行時動態的生成,通常而言咱們使用 Types 來作自定義選項。
IntentDefinitionType
支持輸入多個值
大部分類型的參數支持輸入多個值,即輸入一個數組。同時,支持根據不一樣的 Widget 大小,限制數組的固定長度。
IntentDefinitionFixedSize
控制配置項的顯示條件
能夠控制某一個配置項,只在另外一個配置項含有任何/特定值時展現。以下圖,日曆 App 的 Up Next Widget,僅在 Mirror Calendar App 選項沒有被選中時,纔會顯示 Calendars 配置項。
IntentDefinitionParametersControl1
在 Intent 定義文件中,將某一個參數 A,設置爲另外一個參數 B 的 Parent Parameter ,這樣,參數 B 的顯示與否就取決於參數 A 的值。
例如,在下圖中,calendar 參數僅在 mirrorCalendarApp 參數的值爲 false 時展現:
IntentDefinitionParametersControl2

2. 修改 Widget 的相關參數支持 ConfigurationIntent

替換 Widget 類中的 StaticConfigurationIntentConfiguration
舊:

@main
struct MyWidget: Widget {
 let kind: String = "MyWidget"
 var body: some WidgetConfiguration {
 StaticConfiguration(kind: kind, provider: Provider()) { entry in
 MyWidgetEntryView(entry: entry)
 }
 }
}

新:

@main
struct MyWidget: Widget {
 let kind: String = "MyWidget"
 var body: some WidgetConfiguration {
 IntentConfiguration(kind: kind, intent: WidgetConfiguratIntent.self, provider: Provider()) { entry in
 MyWidgetEntryView(entry: entry)
 }
 }
}

在 Timeline Entry 類中增長 ConfigurationIntent 參數
代碼以下:

struct SimpleEntry: TimelineEntry {
 let date: Date
 let configuration: WidgetConfiguratIntent
}

修改 IntentTimelineProvider 的繼承
Provider 的繼承改爲 IntentTimelineProvider,而且增長 Intent 的類型別名。
舊:

struct Provider: TimelineProvider {
 ...
}

新:

struct Provider: IntentTimelineProvider {
 typealias Intent = WidgetConfiguratIntent
 ...
}

依次修改 getSnapshot / getTimeline 的入參以增長對自定義的支持。並在建立 Timeline Entry 時,傳入 configuration 。

使用接口數據構建自定義入口

Intent Target 中,找到 IntentHandler 文件,遵照 ConfigurationIntent 生成類中 ConfiguratIntentHandling 協議。
實現協議要求的 provideModeArrOptionsCollectionForConfiguration:withCompletion: 方法。
在這個方法中,咱們能夠調用接口獲取自定義數據,生成 completion block 所須要的數據源入參。

- (void)provideModeArrOptionsCollectionForConfiguration:(WidgetConfiguratIntent *)intent withCompletion:(void (^)(INObjectCollection<NMWidgetModel *> * _Nullable modeArrOptionsCollection, NSError * _Nullable error))completion {
 
 [self apiRequest:(NSDictionary *result){
 // 處理獲取到的數據
 ....
 NSMutableArray *allModelArr = ....;
 // 生成配置所須要的數據
 INObjectCollection *collection = [[INObjectCollection alloc] initWithItems:allModeArr];
 completion(collection,nil);
 }];
}

小組件獲取自定義參數

在小組件根據 Timeline Entry 生成視圖時,讀取 Entry 的 configuration 屬性便可獲取用戶是否自定義屬性,以及自定義屬性的詳細值。

總結

優點和缺點並存

WidgetKitWorks
小組件是一個優缺點都很是明顯的事物,在桌面即點即用確實方便,可是交互方式的匱乏以及不能實時更新數據又是很是大的缺陷。正如蘋果所說:"Widgets are not mini-apps",不要用開發 App 的思惟來作小組件,小組件只是由一連串數據驅動的靜態視圖。

優點:

  1. 常駐桌面,大大增長了對產品的曝光。
  2. 利用網絡接口和數據共享,能夠展現與用戶相關的個性化內容。
  3. 縮短了功能的訪問路徑。一次點擊便可讓用戶觸達所需功能。
  4. 能夠屢次重複添加,搭配自定義和推薦算法,添加多個小組件樣式和數據均可以不一樣。
  5. 自定義配置簡單。
  6. 多種尺寸,大尺寸能夠承載複雜度高的內容展現。

缺點:

  1. 不能實時更新數據。
  2. 只能點擊交互。
  3. 小組件的背景不能設置透明效果。
  4. 不能展現動態圖像(視頻/動圖)。

尾巴

小組件的開發實踐到此告一段落,能夠看到組件雖小,須要的知識仍是挺多的。包括 Timeline 、Intents 、SwiftUI 等平時開發很難接觸到的框架和概念須要瞭解學習。
小組件孱弱的交互能力和數據刷新機制是它的硬傷。蘋果對於小組件的能力是很是剋制的。在開發中,不少構思和需求都受限於框架能力沒法實現,但願蘋果在後續迭代中能夠開放出新的能力。好比支持部分不須要啓動 App 的交互形式存在。
但瑕不掩瑜,向用戶展現喜歡的內容或提供用戶想要的功能入口,放大小組件的優點,纔是當前小組件的正確開發方式。

參考資料

本文發佈自 網易雲音樂大前端團隊,文章未經受權禁止任何形式的轉載。咱們常年招收前端、iOS、Android,若是你準備換工做,又剛好喜歡雲音樂,那就加入咱們 grp.music-fe(at)corp.netease.com!
相關文章
相關標籤/搜索