iOS 16.1 Live Activities (即時動態) 開發上手教學 (上)

iOS 16.1 Live Activities (即時動態)

相信大家都已經習慣iOS 16一陣子了吧?在這版新增的諸多功能的確令人非常期待,包含Home screen的widgets 以及電池電量的顯示優化等等,但今天要向大家介紹的則是iOS 16.1才開始支援的Live Activities (即時動態)。


Live Activities 是一種能夠動態顯示在鎖定畫面及動態島(目前只有14 Pro & 14 Pro Max支援)的 widget,蘋果官方給予這項功能很大的設計彈性,它能讓開發者更多的去客製化自己想要展現的資訊,例如Uber 司機的即時位置狀態、Foodpanda 的外送狀態、飛機航班等等。
那廢話不多說就讓我們直接開始吧!

ActivityKit

基本介紹

在介紹 ActivityKit 之前,相信大部分有實作過 widget 的開發者都對 WidgetKit 不陌生,這套在iOS 13由 Apple 官方釋出的 framework 對當時的開發者可以說是個完全新的世界,他對於 iOS 生態來說是一種全新但卻是未來不可或缺的一套 UI 工具。

那回到 ActivityKit,他和 WidgetKit 有什麼區別呢?

其實 ActivityKit 就是基於 WidgetKit 衍生出來的新 framework,他能同時支持 iOS 原本就存在的 widgets 以及即時動態的 widgets,但其中有個最明顯的差別就是 Life-cycle:Activity widget 並沒有所謂的 Time-line Provider (也就是其他widget用來更新的驅動),因此要更新 Activity widget 只能透過 app target 調用 ActivityKit 來實時更新它。


基礎設置

首先,在開始製作你的第一個 Live Activities Widget 時,請務必確認先讓你的 App 支援即時動態的權限,添加的方式是在 Info.plist 裏面添加 NSSupportsLiveActivities 並設定為 YES。




接著,File -> New -> Target -> Widget Extension





如果你是要製作有 Activity widget 的 extension 的話,記得下方的兩個相都要記得勾選喔,如果發現沒有這兩個選項的話,記得先確認自己的 Xcode 版本是不是 14。

Activity widget 有分兩種 Data 模式:靜態 (Static) & 動態 (Dynamic),靜態的 data 表示為 widget 生成之後不會隨著時間或任何參數影響或更新的資料,而動態的 data 則表示為在整個 widget 當中會不斷更新狀態的資料。以外送平台送單來作為舉例的話:餐點為靜態 data,因為餐點不會因為時間或其他因素而突然改變 (有變化的話記得去找客服xD) ; 而外送員的位置跟預計到達時間則是動態 data,會隨著時間改變。

這裡有一個要特別留意的一點就是:動態 data 的大小不可以超過 4 kb。

前情提要搞懂了之後,我們就開始建立我們的 data model,ActivityKit 需要我們建立一個繼承於 AcitivityAttributes 的 structure:
import Foundation
import ActivityKit

struct PizzaDeliveryAttributes: ActivityAttributes {
    public typealias PizzaDeliveryStatus = ContentState

    public struct ContentState: Codable, Hashable {
        var driverName: String
        var deliveryTimer: ClosedRange<Date>
    }

    var numberOfPizzas: Int
    var totalAmount: String
    var orderNumber: String
}
 

以Apple 官網提供的範例來看:ActivityKit 實做的 ActivityAttributes 裏面會包含一個 ContentState 和多個自訂的參數,其中宣告在 ContentState 裏面的參數為動態的 data,而下面的參數則是靜態的。

接下來就是製作我們的 widget 以及其內容:
import SwiftUI
import WidgetKit

@available(iOSApplicationExtension 16.1, *)
@main
struct PizzaDeliveryWidget: Widget {
    var body: some WidgetConfiguration {
        ActivityConfiguration(for: PizzaDeliveryAttributes.self) { context in
            // 製作鎖定畫面應該顯示的畫面,這也同時支援沒有動態島的iOS裝置。
        } dynamicIsland: { context in
            // 製作動態島顯示畫面,這部分下一章會再細說
            // ...
        }
    }
} 


不知道各位有沒有發現這裡有一個地方跟 iOS 13 - 14 的 WidgetConfiguration 有一個地方不太一樣,那就是 ActivityConfiguration,這是在建立 Activity widget 所使用的 structure,而一般的 widget 使用的是 IntentConfiguration ,千萬別搞錯囉。

ActivityConfiguration 跟 IntentConfiguration 的差別在於,ActivityConfiguration 多一個動態島的設置供開發者設置,因為即時動態如果在有動態島的裝置上的話,是會同步顯示的喔!

那我相信一定有很多開發者會遇到跟我一樣的問題,那就是:

我原本就有使用一般的widget,但是現在如果要再新增 Activity widget,我該怎麼讓他們同時存在呢?

這問題 Apple 官方也有提供了一個很棒的解法,那就是 WidgetBundle,他能讓兩種 widget 同時被 @main 調用,這樣一來就不會有其中一個沒辦法使用的問題。
@main
struct PizzaDeliveryWidgets: WidgetBundle {
    var body: some Widget {
        FavoritePizzaWidget() // iOS 13 - 14 原本存在的widget

        if #available(iOS 16.1, *) {
            PizzaDeliveryWidget() // Activity widget
        }
    }
} 


使用這個方法後,原本在宣告 PizzaDeliveryWidget 上方的 @main 就要記得拿掉喔,不然你會被錯誤搞得頭很疼~

開始使用

再來就是如何在 app 裏面呼叫 Activity widget:
var future = Calendar.current.date(byAdding: .minute, value: (Int(minutes) ?? 0), to: Date())!
future = Calendar.current.date(byAdding: .second, value: (Int(seconds) ?? 0), to: future)!
let date = Date.now...future
let initialContentState = PizzaDeliveryAttributes.ContentState(driverName: "Bill James", deliveryTimer:date)
let activityAttributes = PizzaDeliveryAttributes(numberOfPizzas: 3, totalAmount: "$42.00", orderNumber: "12345")

do {
    deliveryActivity = try Activity.request(attributes: activityAttributes, contentState: initialContentState)
    print("Requested a pizza delivery Activity (String(describing: deliveryActivity?.id)).")
} catch (let error) {
    print("Error requesting pizza delivery Activity (error.localizedDescription).")
}
可以看到程式碼裡,我們透過呼叫 ActivityKit 的 Activity.request 去請求建立一個 PizzaDeliveryAttributes 的 Activity widget,當然請求也有可能失敗,大多數的原因都會是 Info.plist 並沒有加上 NSSupportsLiveActivities (至少我是這樣:( ),那就再記得檢查設定有沒有都做對。

Activity widget 必須要 app 在前景的時候才能夠被呼叫啟用,但更新或終止則可以在背景執行,這就需要用到 Task 來幫我們做這件事情:
  • 更新:
    var future = Calendar.current.date(byAdding: .minute, value: (Int(minutes) ?? 0), to: Date())!
    future = Calendar.current.date(byAdding: .second, value: (Int(seconds) ?? 0), to: future)!
    let date = Date.now...future
    let updatedDeliveryStatus = PizzaDeliveryAttributes.PizzaDeliveryStatus(driverName: "Anne Johnson", deliveryTimer: date)
    let alertConfiguration = AlertConfiguration(title: "Delivery Update", body: "Your pizza order will arrive in 25 minutes.", sound: .default)
    
    Task{
    	await deliveryActivity?.update(using: updatedDeliveryStatus, alertConfiguration: alertConfiguration)
    }
    
  • 終止:
    let finalDeliveryStatus = PizzaDeliveryAttributes.PizzaDeliveryStatus(driverName: "Anne Johnson", deliveryTimer: Date.now...Date())
    
    Task {           
    	await deliveryActivity?.end(using:finalDeliveryStatus, dismissalPolicy: .default)
    } 
而 end 的 function 裏面有個參數叫做 dismissalPolicy,預設的話是 default,但如果你想要它在特定時間終止或是立即中止的話,可以用.after(_ date: Date) 跟 .immediate 來操作。

以上就是iOS 16.1 新加入的Live Activities(即時動態)的基本操作,當然一定還有更多更細微的調整可以去實作,那我會在下一章節介紹即時動態在動態島上的實作,以及更多畫面上的彈性調整。

希望大家可以多多關注,下一篇繼續延伸即時動態的應用,一起實作動態島上的即時動態,如果有任何問題歡迎到我的IG私訊我或在底下留言告訴我喔!至於為什麼我是拿 Apple 官網的實作來說呢?因為我的實際應用是放在我公司的產品,基於法律及隱私問題,我就只好拿官網的範例了...


如果想給我一些支持,也歡迎買杯咖啡給我,謝謝大家!

留言

這個網誌中的熱門文章

iOS 16.1 Live Activities (即時動態) 開發上手教學 (下)