WidgetKit 在 iOS 14 初次推出的一個簡單框架,讓開發者可以創建主畫面 (Home Screen) Widget。從那時起,WidgetKit 這個框架逐漸進化,在 iOS 16 中支援不少備受期待的功能,像是鎖定畫面(Lock Screen) Widget、即時動態 (Live Activities)、和動態島 (Dynamic Island)。
可以肯定的是,WidgetKit 現在已經成為所有 iOS 開發者必須學習的框架了。因此,我決定寫一系列與 WidgetKit 相關的文章。這篇文章會是這個系列的第一篇文章,因此我們會先從基礎知識開始,了解一下如何創建我們第一個主畫面 Widget。
讓我們開始吧!
視圖尺寸 Widget
在這篇文章中,我們會建立一個簡單的主畫面 Widget,來顯示 Widget 的尺寸。此外,Widget 也會顯示 timeline provider 的資料,我們會在文章接下來的部分詳細解釋。
添加一個 Widget Extension
所有 iOS Widget 都必須綁定到一個 iOS App。在 Xcode 中創建 iOS App 後,讓我們添加一個新的 widget extension target,並把它命名為 “ViewSizeWidget”。為了簡單起見,不要勾選 “Include Configuration Intent”,因為這超出了本文的範圍。我會在日後的文章中再詳細介紹,敬請期待。
添加了新的 target 後,Xcode 會提示我們激活 Widget Extension 的 Scheme。讓我們按指示激活它。
在這個步驟,你應該會注意到 Xcode 中多了一個資料夾,當中有一個 Swift 檔案 (ViewSizeWidget.swift
)。
讓我們把 ViewSizeWidget.swift
內自動生成的程式碼刪除,我們將會在接下來的章節中一起實作 ViewSizeWidget.swift
。
建立一個 Widget
一個 widget 由 4 個主要組件組成:
- Timeline Entry
- Widget 視圖(SwiftUI 視圖)
- Timeline Provider
- Widget Configuration
讓我們來實作這 4 個組件吧!
Timeline Entry
Timeline Entry 就像是 Widget 視圖的 Model Object,當中一定要有最少一個 date
參數 (parameter)。有需要的話,我們也可以添加其它參數到 timeline entry。
然後,系統會利用 timeline entry 內的 date
,來決定何時顯示或刷新 widget 內的數據。
回到我們的範例,把我們的 timeline entry 命名為 ViewSizeEntry
。
import WidgetKit
import SwiftUI
struct ViewSizeEntry: TimelineEntry {
let date: Date
let providerInfo: String
}
在上面的程式碼中,我們使用了 providerInfo
保存與 timeline provider 相關的資訊來顯示在 widget 中。在稍後的部分我們會再詳細說明。
Widget 視圖
建立好 timeline entry 後,我們就可以去實作 Widget 的 UI 了。基本上,它就只是一個 SwiftUI 視圖。
struct ViewSizeWidgetView : View {
let entry: ViewSizeEntry
var body: some View {
GeometryReader { geometry in
VStack {
// Show view size
Text("\(Int(geometry.size.width)) x \(Int(geometry.size.height))")
.font(.system(.title2, weight: .bold))
// Show provider info
Text(entry.providerInfo)
.font(.footnote)
}
.frame(maxWidth: .infinity, maxHeight: .infinity)
.background(Color.green)
}
}
}
上面的程式碼沒有什麼特別。不過你可以留意一下 entry
參數,看看我們如何用它來把 timeline entry 中的資料 (providerInfo
) 顯示到 widget UI。
Timeline Provider
一如其名,Timeline Provider 就是為系統提供資料,在特定 timestamp 在 Widget 顯示什麼內容。你可能已經猜到,timeline provider 需要符合 TimelineProvider
協定,當中有以下 3 個方法要求:
struct ViewSizeTimelineProvider: TimelineProvider {
typealias Entry = ViewSizeEntry
func placeholder(in context: Context) -> Entry {
// Implementation here...
}
func getSnapshot(in context: Context, completion: @escaping (Entry) -> ()) {
// Implementation here...
}
func getTimeline(in context: Context, completion: @escaping (Timeline<Entry>) -> ()) {
// Implementation here...
}
}
另外,我們一定要把 Entry
別名設定為前文定義的 ViewSizeEntry
。
接下來,讓我們實作這 3 個方法。
首先,是 placeholder(in:)
方法。它可以向系統提供 dummy data,以在等待 widget 準備好時呈現 placeholder UI。SwiftUI 會對我們提供的 dummy data 應用編輯效果,因此 dummy data 的實際數值並不重要。以下是實作的程式碼:
func placeholder(in context: Context) -> Entry {
// This data will be masked
return ViewSizeEntry(date: Date(), providerInfo: "placeholder")
}
結果會是以下的 placeholder UI:
接下來是 getSnapshot(in:completion:)
方法。這個函式主要提供系統在 widget gallery 中渲染 widget 所需要的資料。以下是實作的程式碼:
func getSnapshot(in context: Context, completion: @escaping (Entry) -> ()) {
let entry = ViewSizeEntry(date: Date(), providerInfo: "snapshot")
completion(entry)
}
從以上的螢幕截圖可見,Widget 顯示了我們提供的 providerInfo
:“snapshot”。
最後就是 getTimeline(in:completion:)
方法。這是 timeline provider 中最重要的方法,因為它為當前的時間點提供 timeline entry 陣列,以及可在未來的任何時間點更新 widget。
因為我們的 widget 只會顯示靜態數據,因此我們可以如此簡單地回傳一個 timeline entry:
func getTimeline(in context: Context, completion: @escaping (Timeline<Entry>) -> ()) {
let entry = ViewSizeEntry(date: Date(), providerInfo: "timeline")
let timeline = Timeline(entries: [entry], policy: .never)
completion(timeline)
}
完成後,我們就可以去實作 widget configuration 了。
Widget Configuration
接下來,我們就可以把剛剛實作的東西放在 Widget Configuration。讓我們先看看實作的程式碼,之後我會詳細解釋。
@main
struct ViewSizeWidget: Widget {
let kind: String = "ViewSizeWidget"
var body: some WidgetConfiguration {
StaticConfiguration(kind: kind, provider: ViewSizeTimelineProvider()) { entry in
ViewSizeWidgetView(entry: entry)
}
.configurationDisplayName("View Size Widget")
.description("This is a demo widget.")
.supportedFamilies([
.systemSmall,
.systemMedium,
.systemLarge,
])
}
}
第一點要注意的是 @main
特性 (attribute),它指示 ViewSizeWidget
為我們 widget extension 的 entry point,也就表示這個 extionsion 只有一個 widget。多個 widget 的情況並不在這篇教學文章的範圍,因此讓我們把 widget bundle 標記為 entry point。
Widget configuration 主要的用途是連接 timeline provider 和 widget 視圖。我們在這裡使用的配置類型是 StaticConfiguration
,也就是說,我們的 widget 沒有任何使用者可配置的屬性。如果我們要添加使用者可配置的屬性,就要使用 IntentConfiguration
。
我們可以使用不同型別的修飾符,來繼續設置 widget configuration。舉個例子,我們可以使用 configurationDisplayName
和 description
修飾符,來設置 widget 的 gallery 的 title 和 subtitle。
最後,supportedFamilies
修飾符可以讓我們指定想支援的 widget 尺寸。在我們的範例中,我們會支持 small、medium、和 large 尺寸。請注意,其他 widget family 像是 accessoryCircular
、accessoryRectangular
等,主要是用於創建鎖定畫面 Widget。我之後會寫一篇關於鎖定畫面 widget 的文章,來深入介紹它們,不要錯過喔!
我們完成視圖尺寸 widget 的實作了!現在,利用 widget extension scheme 來執行 App 來看看實作結果吧!
你可以在 GitHub 上參考範例程式碼。
總結
大家在這篇文章學習了 WidgetKit 的基礎,我們還有更多內容可以去探索。我們網站將會有更多有關 WidgetKit 的文章,大家可以期待一下。歡迎大家在 Twitter 關注我,及訂閱我的 Newsletter,以免錯過我新發布的文章。
謝謝大家的閱讀。👨🏻💻