天天看點

iOS14新特性探索之二:App Widget小元件應用(一)

 iOS 14除了引入了亮眼的App Clips功能外。還有一個也非常惹争議的功能就是App Widget。App Widget可以了解為小元件,在非常早的Android版本中就有了Widget的概念,應用開發者可以為系統開發自己應用相契合的Widget來讓使用者更加友善的使用應用提供的功能。例如Android早期系統中非常常見的鐘表時間元件、快捷設定元件等。使用者可以将這些小元件根據自己的喜好放在螢幕的指定位置。從這點看,iOS 14提供的App Widget功能的确不能算是一種創新,最多算是一種增強。

       其實,iOS Widget的概念并非是iOS 14突然引入的,在iOS 10釋出時,iOS系統就引入了Extension相關功能,其中有一種Extension叫做Today Extension,這就是iOS 14中Widget的前身。Today Extension允許開發者為負一屏開發快捷功能入口。關于Today Extension的應用,如下部落格有詳細的介紹:

iOS8新特性擴充(Extension)應用之一——Today擴充:

https://my.oschina.net/u/2340880/blog/485533

iOS中Today擴充插件與宿主APP的互動:

https://my.oschina.net/u/2340880/blog/711807

需要注意,在iOS 14中,Today Extension相關的接口都已經被廢棄,我們需要使用新的WidgetKit架構提供的小元件接口開發Widget。在iOS 14上,Today Extension依然可以使用,但是其功能受限,隻能在負一屏展示它,使用者不能随意的将其放在指定屏的指定位置。

1. 關于App Widget

       Widget為應用程式提供了這樣一種功能:其可以讓使用者在主螢幕上展示App中使用者所關心的資訊。例如一款天氣軟體,其可以附帶一個Widget讓使用者在主螢幕就可檢視今日的天氣情況,例如股票相關的軟體,使用者将自己感興趣的股票收藏,無需打開App,在主螢幕即可查到對應的股價資訊。如下圖所示,是系統提供的電池Widget展示在主螢幕上的示例:

iOS14新特性探索之二:App Widget小元件應用(一)

一個App也可以提供多個Widget元件,使用者可以選擇将其最關心的放置在最重要的位置上,以便最友善的擷取資訊。對于同一種Widget元件,開發者也可以提供不同的尺寸或不同的布局,這可以提供給使用者更多的選擇以滿足不同使用者的偏好。

       為應用程式添加一個Widget元件并不複雜,但是有一點需要注意,小元件的UI部分隻能夠使用SwiftUI來開發,是以如果你要開發Widget元件,必須有一些Swift的基礎并對SwiftUI有一定的了解。對于Swift與SwiftUI的相關内容,本篇部落格就不再做過多贅述。

2. 建立App Widget

       與其他的Extension擴充類似,App Widget本身也是一種擴充,是以其隻能依賴一個宿主App而存在,首先向已有的App中添加App Widget非常簡單,為項目建立一個新的Target,選擇其中的Widget Extension模闆進行建立,如下圖:

iOS14新特性探索之二:App Widget小元件應用(一)

建立完成後,Xcode會自動幫我們建立和配置的檔案的工作都完成,預設的模闆為我們建立了一個顯示目前時間的元件,我們可以直接在真機上運作它(Bate版本的Xcode模拟器運作會有些異常),之後,我們就可以将這個顯示時間的小元件放置在主螢幕的任意位置,并且,預設提供了3種尺寸供使用者選擇,如下圖所示:

iOS14新特性探索之二:App Widget小元件應用(一)

C

Xcode為我們建立的這個模闆雖然簡單,但是五髒俱全。Widget加載的入口是@main标記的結構體,代碼如下:

@main

struct WidgetExt: Widget {

   private let kind: String = "WidgetExt"

   public var body: some WidgetConfiguration {

       StaticConfiguration(kind: kind, provider: Provider(), placeholder: PlaceholderView()) { entry in

           WidgetExtEntryView(entry: entry)

       }

       .configurationDisplayName("My Widget")

       .description("This is an example widget.")

   }

}

WidgetExt是我們為元件target項目設定的名字,模闆自動使用這個名字幫我們生成了一個實作了Widget協定的結構體。結構體中實作了兩個屬性,其實Widget協定提供的核心隻讀屬性隻有一個body,将上面的代碼改寫如下也是一樣的:

       StaticConfiguration(kind: "WidgetExt", provider: Provider(), placeholder: PlaceholderView()) { entry in

上面代碼的核心在于body隻讀屬性的實作,其需要傳回一個實作了WidgetConfiguration協定的示例。這個協定描述了元件的配置資訊,StaticConfiguration是系統提供的元件配置結構體,其用來對靜态類型的元件提供配置。StaticConfiguration完整的構造方法如下:

public init<Provider, PlaceholderContent>(

kind: String,

provider: Provider,

placeholder: PlaceholderContent,

content: @escaping (Provider.Entry) -> Content)

where Provider : TimelineProvider, PlaceholderContent : View

可以看到,上面構造方法中的Provider和PlaceholderContent實際上是兩個泛型,我們後面再介紹。目前,我們先關注下構造方法需要傳的幾個參數。

kind:這個參數是一個字元号,我們可以任意提供,用來辨別這個Widget元件。

provider:簡單了解,這是一個資料提供對象,用來為小元件提供渲染資料,其必須實作TimelineProvider協定,即是基于時間線來驅動小元件的渲染。

placeholder:提供一個占位的視圖,當小元件沒有資料或者在鎖屏狀态時,會顯示這個占位視圖。

content:為小元件提供内容,是一個閉包,其中會把Provider的entry屬性傳入,是以小元件的視圖渲染實際是由Provider驅動的。

   明白了上面幾個參數的意義,開發小元件就非常輕松了。首先,需要建立一個合适的Provider來為小元件提供資料支援,以模闆中的代碼為例,如下:

struct Provider: TimelineProvider {

   public typealias Entry = SimpleEntry

   public func snapshot(with context: Context, completion: @escaping (SimpleEntry) -> ()) {

       let entry = SimpleEntry(date: Date())

       completion(entry)

   public func timeline(with context: Context, completion: @escaping (Timeline<Entry>) -> ()) {

       var entries: [SimpleEntry] = []

       // Generate a timeline consisting of five entries an hour apart, starting from the current date.

       let currentDate = Date()

       for hourOffset in 0 ..< 5 {

           let entryDate = Calendar.current.date(byAdding: .hour, value: hourOffset, to: currentDate)!

           let entry = SimpleEntry(date: entryDate)

           entries.append(entry)

       let timeline = Timeline(entries: entries, policy: .atEnd)

       completion(timeline)

struct SimpleEntry: TimelineEntry {

   public let date: Date

如上代碼所示,Provider結構體實作了TimelineProvider協定,這個協定中隻定義了兩個方法,分别是上面實作的snapshot方法和timeline方法。

       其中snapshop方法在小元件啟動時會被調用一次,用來為小元件提供首屏渲染所需要的資料,其通常用來提供一些初始化的資料。調用完snapshot方法後,會調用timeline方法來定義要更新元件的時間線,這個方法的回調中需要傳入一組Timeline對象,如上代碼所示,其定義目前時刻開始,每隔一個小時進行一次重新整理,将目前元件顯示的時間重新整理成最新的時刻,當最後一次重新整理任務結束後,會再次調用timeline函數重新設定一組更新的時間線。關于時間線的詳細介紹,後面會提及。

       有了Provider來對元件的更新提供驅動後,就是小元件頁面的渲染了,在StaticConfiguration構造方法的閉包中,我們需要傳回一個View作為小元件的内容,模闆提供的示例代碼如下:

struct WidgetExtEntryView : View {

   var entry: Provider.Entry

   var body: some View {

       Text(entry.date, style: .time)

       在向主螢幕添加小元件時,使用者可以選擇不同尺寸的小元件進行添加,在小元件的渲染布局時,開發者也可以根據不同的環境尺寸配置不同的渲染政策,例如下面代碼:

   @Environment(\.widgetFamily) var family: WidgetFamily

   @ViewBuilder

       switch family {

       case .systemSmall: Text(entry.date, style: .time)

       case .systemMedium: Text(entry.date, style: .date)

       case .systemLarge: Text(entry.date, style: .relative)

       default: Text(entry.date, style: .time)

其中通過Enviroment用來判斷目前元件的環境情況,即元件的尺寸資訊,上面代碼根據不同的尺寸渲染了不同格式的時間。

繼續閱讀