iOS 16

初探 iOS 16 的 WidgetKit:一起創建一個主畫面 Widget

在 iOS 16,WidgetKit 支援不少備受期待的功能,像是鎖定畫面 Widget、即時動態、和動態島等,可以說是開發者必須學習的框架。在這篇文章中,Kah Seng 會從基礎知識開始,帶大家一起創建第一個主畫面 Widget!
初探 iOS 16 的 WidgetKit:一起創建一個主畫面 Widget
Photo by Amjith S on Unsplash
初探 iOS 16 的 WidgetKit:一起創建一個主畫面 Widget
Photo by Amjith S on Unsplash
In: iOS 16
本篇原文(標題:Getting Started With WidgetKit)刊登於 Swift Senpai,由 Lee Kah Seng 所著,並授權翻譯及轉載。

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 的資料,我們會在文章接下來的部分詳細解釋。

Static Home Screen Widget example in iOS
視圖尺寸 Widget

添加一個 Widget Extension

所有 iOS Widget 都必須綁定到一個 iOS App。在 Xcode 中創建 iOS App 後,讓我們添加一個新的 widget extension target,並把它命名為 “ViewSizeWidget”。為了簡單起見,不要勾選 “Include Configuration Intent”,因為這超出了本文的範圍。我會在日後的文章中再詳細介紹,敬請期待。

Adding widget extension target in Xcode
添加 widget extension target
Uncheck "Include Configuration Intent" in Xcode
不要勾選 “Include Configuration Intent”

添加了新的 target 後,Xcode 會提示我們激活 Widget Extension 的 Scheme。讓我們按指示激活它。

在這個步驟,你應該會注意到 Xcode 中多了一個資料夾,當中有一個 Swift 檔案 (ViewSizeWidget.swift)。

Folder generated for widget extension in Xcode
為 widget extension 生成的資料夾

讓我們把 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:

iOS Home Screen widget in placeholder state
Placeholder 狀態下的 Widget

接下來是 getSnapshot(in:completion:) 方法。這個函式主要提供系統在 widget gallery 中渲染 widget 所需要的資料。以下是實作的程式碼:

func getSnapshot(in context: Context, completion: @escaping (Entry) -> ()) {
    let entry = ViewSizeEntry(date: Date(), providerInfo: "snapshot")
    completion(entry)
}
Widget snapshot in iOS widget gallery
Widget gallery 內的 Widget

從以上的螢幕截圖可見,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。舉個例子,我們可以使用 configurationDisplayNamedescription 修飾符,來設置 widget 的 gallery 的 title 和 subtitle。

The widget gallery title and subtitle in iOS
Widget Gallery title 和 subtitle

最後,supportedFamilies 修飾符可以讓我們指定想支援的 widget 尺寸。在我們的範例中,我們會支持 small、medium、和 large 尺寸。請注意,其他 widget family 像是 accessoryCircularaccessoryRectangular 等,主要是用於創建鎖定畫面 Widget。我之後會寫一篇關於鎖定畫面 widget 的文章,來深入介紹它們,不要錯過喔!

我們完成視圖尺寸 widget 的實作了!現在,利用 widget extension scheme 來執行 App 來看看實作結果吧!

你可以在 GitHub 上參考範例程式碼。

總結

大家在這篇文章學習了 WidgetKit 的基礎,我們還有更多內容可以去探索。我們網站將會有更多有關 WidgetKit 的文章,大家可以期待一下。歡迎大家在 Twitter 關注我,及訂閱我的 Newsletter,以免錯過我新發布的文章。

謝謝大家的閱讀。👨🏻‍💻

本篇原文(標題:Getting Started With WidgetKit)刊登於 Swift Senpai,由 Lee Kah Seng 所著,並授權翻譯及轉載。
作者簡介:Lee Kah Seng,馬來西亞人,2011年成為 iOS 開發者,喜歡 Swift、音樂、和日本動畫,是一名兼職「背包客」。
譯者簡介:Kelly Chan-AppCoda 編輯小姐。
作者
AppCoda 編輯團隊
此文章為客座或轉載文章,由作者授權刊登,AppCoda編輯團隊編輯。有關文章詳情,請參考文首或文末的簡介。
評論
更多來自 AppCoda 中文版
WWDC 22 的重點更新:SwiftUI 4.0 新功能一覽
SwiftUI 框架

WWDC 22 的重點更新:SwiftUI 4.0 新功能一覽

WWDC 22 剛剛完結,隨著 iOS 16 和 Xcode 14,Apple 也推出了新版本的 SwiftUI。這次更新帶來了非常多功能,讓開發者可以構建更好的 App,並減少需要編寫的程式碼。在這篇文章中,我會為大家簡單介紹 SwiftUI 4.0 的新功能。
很好! 你已成功註冊。
歡迎回來! 你已成功登入。
你已成功訂閱 AppCoda 中文版 電子報。
你的連結已失效。
成功! 請檢查你的電子郵件以獲取用於登入的連結。
好! 你的付費資料已更新。
你的付費方式並未更新。