iOS靈動島

背景

2022.9.8蘋果發(fā)布會上,最引人注目的一個功能靈動島問世,當(dāng)然整個發(fā)布會也只有這一個功能能拿出來提一嘴。對于用戶而言靈動島是一種新的交互式,劉海屏改成了藥片屏。對于開發(fā)者而言,我們需要研究一下能為我們的APP做些什么。

靈動島是什么

  • 靈動島是iphone14Pro的專屬特性,是iphone14pro和4 pro max兩個產(chǎn)品的交互式。

  • 在這兩個系列中,把劉海屏改為藥片屏幕,給了傳統(tǒng)的打孔屏幕一個新的觀念。

  • 提到靈動島,就必須知道實(shí)時活動Live Activity,實(shí)時活動分為兩部分,一部分是鎖屏界面的展示,一部分是靈動島展示。如果開發(fā)靈動島就需要同時兼顧鎖屏的展示。在鎖定屏幕上,Live Activity顯示在屏幕底部。(因為靈動島屬于LiveActivity的一部分,后邊我們統(tǒng)一使用Live Activity)

靈動島與Widget的關(guān)系

開發(fā)Live Activity的代碼都在Widget的Target里,使用SwiftUIWidgetKit來創(chuàng)建Live Activity的用戶界面。所以Live Activity的開發(fā)跟開發(fā)Widget差不多。

與Widget不同的是,Live Activity使用不同的機(jī)制來接受更新。Live Activity不使用時間線機(jī)制,使用ActivityKit或者通過遠(yuǎn)程推送通知更新。

Live Activity的要求和限制

  • 除非應(yīng)用程序或者用戶結(jié)束,否則Live Activity最多可以保活8小時,超過時間,系統(tǒng)會自動結(jié)束。系統(tǒng)會將其從靈動島中移除。但是Live Activity會一直保留在鎖屏上,知道用戶刪除或者系統(tǒng)4小時后刪除,所以Live Activity在鎖屏上最多能存活12小時。

  • 每個Live Activity在自己應(yīng)用的沙盒中運(yùn)行,無法訪問網(wǎng)絡(luò)和位置更新,要更新數(shù)據(jù),需要使用ActivityKit或者使用遠(yuǎn)程推送。動態(tài)數(shù)據(jù)的大小不能超過4Kb

  • Live Activity針對鎖屏和靈動島提供不同的視圖。鎖屏?xí)霈F(xiàn)所有設(shè)備上。支持靈動島的設(shè)備具有四種視圖:緊湊前視圖、緊湊尾視圖、最小視圖和擴(kuò)展視圖。

  • 為確保系統(tǒng)可以在每個位置顯示您的 Live Activity,開發(fā)時候必須支持所有視圖。

  • 一個應(yīng)用可以啟動多個 Live Activity,而一個設(shè)備可以從多個應(yīng)用運(yùn)行 Live Activity,啟動可能會失敗,LiveActivity有上限個數(shù)

Live Activity的開發(fā)

Live Activity的界面開發(fā)是Widget的一部分,如果應(yīng)用已經(jīng)支持了Widget,就可以將代碼寫到Widget里。

  1. 創(chuàng)建Widget Target

  2. 打開Info.plist文件,添加 Supports Live Activity ,設(shè)置為YES

  3. 創(chuàng)建ActivityAttributes 結(jié)構(gòu),描述Live Activity的靜態(tài)和動態(tài)數(shù)據(jù)。

  4. 創(chuàng)建ActivityConfiguration通過ActivityAttributes,啟動Live Activity

  5. 添加代碼以配置、啟動、更新和結(jié)束。

創(chuàng)建Live Activity

import SwiftUI
import WidgetKit

@main
struct PizzaDeliveryActivityWidget: Widget {
    var body: some WidgetConfiguration {
        ActivityConfiguration(for: PizzaDeliveryAttributes.self) { context in
            // Create the view that appears on the Lock Screen and as a
            // banner on the Home Screen of devices that don't support the
            // Dynamic Island.
            // ...
        } dynamicIsland: { context in
            // Create the views that appear in the Dynamic Island.
            // ...
        }
    }
}

LiveActivity是一個Widget,所以代碼是寫在Widget的target里邊??梢钥吹届`動島的主函數(shù)就是繼承與Wdiget。

上邊函數(shù)的ActivityConfiguration第一個block就是鎖屏界面的view。第二個block為靈動島view。

創(chuàng)建鎖屏界面的視圖

系統(tǒng)要求高度不能超過160,超過的話會截掉

@main
struct PizzaDeliveryWidget: Widget {    
    var body: some WidgetConfiguration { 
        ActivityConfiguration(for: PizzaDeliveryAttributes.self) { context in
        // Create the view that appears on the Lock Screen and as a
            // banner on the Home Screen of devices that don't support the
            // Dynamic Island.
            LockScreenLiveActivityView(context: context)
        } dynamicIsland: { context in
            // Create the views that appear in the Dynamic Island.
            // ...
        }
    }
}

struct LockScreenLiveActivityView: View {
    let context: ActivityViewContext<PizzaDeliveryAttributes>

    var body: some View {
        VStack {
            Spacer()
            Text("\(context.state.driverName) is on their way with your pizza!")
            Spacer()
            HStack {
                Spacer()
                Label {
                    Text("\(context.attributes.numberOfPizzas) Pizzas")
                } icon: {
                    Image(systemName: "bag")
                        .foregroundColor(.indigo)
                }
                .font(.title2)
                Spacer()
                Label {
                    Text(timerInterval: context.state.deliveryTimer, countsDown: true)
                        .multilineTextAlignment(.center)
                        .frame(width: 50)
                        .monospacedDigit()
                } icon: {
                    Image(systemName: "timer")
                        .foregroundColor(.indigo)
                }
                .font(.title2)
                Spacer()
            }
            Spacer()
        }
        .activitySystemActionForegroundColor(.indigo)
        .activityBackgroundTint(.cyan)
    }
}

創(chuàng)建靈動島視圖

靈動島視圖分為兩大部分

  • 一部分是靈動島默認(rèn)狀態(tài),就是緊湊狀態(tài)

  • 一部分是長按靈動島的擴(kuò)展?fàn)顟B(tài)

創(chuàng)建緊湊和最小的視圖

默認(rèn)情況下,靈動島中的緊湊和最小視圖使用黑色背景顏色和白色文本??梢允褂眯薷?a target="_blank">keylineTint(_:)

import SwiftUI
import WidgetKit

@main
struct PizzaDeliveryWidget: Widget {    
    var body: some WidgetConfiguration {
        ActivityConfiguration(for: PizzaDeliveryAttributes.self) { context in
            // Create the view that appears on the Lock Screen and as a
            // banner on the Home Screen of devices that don't support the
            // Dynamic Island.
            // ...
        } dynamicIsland: { context in
            // Create the views that appear in the Dynamic Island.
            DynamicIsland {
                    // Create the expanded view.
                    // ...

            } compactLeading: {
                Label {
                    Text("\(context.attributes.numberOfPizzas) Pizzas")
                } icon: {
                    Image(systemName: "bag")
                        .foregroundColor(.indigo)
                }
                .font(.caption2)
            } compactTrailing: {
                Text(timerInterval: context.state.deliveryTimer, countsDown: true)
                    .multilineTextAlignment(.center)
                    .frame(width: 40)
                    .font(.caption2)
            } minimal: {
                VStack(alignment: .center) {
                    Image(systemName: "timer")
                    Text(timerInterval: context.state.deliveryTimer, countsDown: true)
                        .multilineTextAlignment(.center)
                        .monospacedDigit()
                        .font(.caption2)
                }
            }
            .keylineTint(.cyan)
        }
    }
}
創(chuàng)建擴(kuò)展視圖

視圖最高160,多余截斷


擴(kuò)展視圖分為4部分

center將內(nèi)容放置在原深感攝像頭下方。

leading將內(nèi)容沿展開的 Live Activity 的前沿放置在原深感攝像頭旁邊,并在其下方包裹其他內(nèi)容。

trailing將內(nèi)容放置在 TrueDepth 攝像頭旁邊展開的 Live Activity 的后沿,并在其下方包裹其他內(nèi)容。

bottom將內(nèi)容置于前導(dǎo)、尾隨和居中內(nèi)容之下。


struct PizzaDeliveryWidget: Widget {
    var body: some WidgetConfiguration {
        ActivityConfiguration(for: PizzaDeliveryAttributes.self) { context in
            // Create the view that appears on the Lock Screen and as a
            // banner on the Home Screen of devices that don't support the
            // Dynamic Island.
            LockScreenLiveActivityView(context: context)
        } dynamicIsland: { context in
            // Create the views that appear in the Dynamic Island.
            DynamicIsland {
                // Create the expanded view.
                DynamicIslandExpandedRegion(.leading) {
                    Label("\(context.attributes.numberOfPizzas) Pizzas", systemImage: "bag")
                        .foregroundColor(.indigo)
                        .font(.title2)
                }

                DynamicIslandExpandedRegion(.trailing) {
                    Label {
                        Text(timerInterval: context.state.deliveryTimer, countsDown: true)
                            .multilineTextAlignment(.trailing)
                            .frame(width: 50)
                            .monospacedDigit()
                    } icon: {
                        Image(systemName: "timer")
                            .foregroundColor(.indigo)
                    }
                    .font(.title2)
                }

                DynamicIslandExpandedRegion(.center) {
                    Text("\(context.state.driverName) is on their way!")
                        .lineLimit(1)
                        .font(.caption)
                }

                DynamicIslandExpandedRegion(.bottom) {
                    Button {
                        // Deep link into your app.
                    } label: {
                        Label("Call driver", systemImage: "phone")
                    }
                    .foregroundColor(.indigo)
                }
            } compactLeading: {
                // Create the compact leading view.
                // ...
            } compactTrailing: {
                // Create the compact trailing view.
                // ...
            } minimal: {
                // Create the minimal view.
                // ...
            }
            .keylineTint(.yellow)
        }
    }
}
使用自定義顏色

系統(tǒng)默認(rèn)設(shè)置的顏色最適合用戶。如果要設(shè)置自定義顏色,可以使用activityBackgroundTint(_:)activitySystemActionForegroundColor(_:)

設(shè)置半透明,使用opacity(_:)

點(diǎn)擊處理

跟Widget相同。

鎖屏、緊湊視圖使用widgetURL(_:)跳轉(zhuǎn),全局相應(yīng)。

擴(kuò)展視圖,使用Link,可以單獨(dú)按鈕相應(yīng)。

啟動Live Activity
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 Live Activity \(String(describing: deliveryActivity?.id)).")
} catch (let error) {
    print("Error requesting pizza delivery Live Activity \(error.localizedDescription).")
}

只能通過應(yīng)用程序在前臺時候啟動,可以在后臺運(yùn)行時更新或者結(jié)束,比如使用Background Tasks

使用遠(yuǎn)程推送更新和關(guān)閉LiveActivity Updating and ending your Live Activity with remote push notifications.

更新Live Activity

應(yīng)用程序處于前臺或后臺時,使用此API更新實(shí)時活動。還可以使用該功能在 iPhone 和 Apple Watch 上顯示警報,告知人們新的 Live Activity 內(nèi)容

更新數(shù)據(jù)的大小不能超過 4KB。

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)

await deliveryActivity?.update(using: updatedDeliveryStatus, alertConfiguration: alertConfiguration)

在 Apple Watch 上,系統(tǒng)將title和body屬性用于警報。在 iPhone 上,系統(tǒng)不會顯示常規(guī)警報,而是顯示動態(tài)島中展開的實(shí)時活動。在不支持動態(tài)島的設(shè)備上,系統(tǒng)會在主屏幕上顯示一個橫幅,該橫幅使用您的實(shí)時活動的擴(kuò)展視圖。

結(jié)束Live Activity

已結(jié)束的實(shí)時活動將保留在鎖定屏幕上,直到用戶將其刪除或系統(tǒng)自動將其刪除。自動刪除的控制取決于函數(shù)的dismissalPolicy參數(shù)。

另外,及時結(jié)束了也要包含更新,保證在Live Activity結(jié)束后顯示最新和最終的結(jié)果。

let finalDeliveryStatus = PizzaDeliveryAttributes.PizzaDeliveryStatus(driverName: "Anne Johnson", deliveryTimer: Date.now...Date())

Task {           
    await deliveryActivity?.end(using:finalDeliveryStatus, dismissalPolicy: .default)
}

實(shí)例顯示的是default策略,結(jié)束后會保留在鎖定屏幕上一段時間。用戶可以選擇移除,或者4個小時候系統(tǒng)自動移除。

要立即從鎖屏上移除的話,需要使用immedia屬性,或者after指定時間。時間需要指定在4小時之內(nèi),超過4小時,系統(tǒng)會主動刪除。

移除live activity,只會影響活動的展示,并不會對業(yè)務(wù)上產(chǎn)生影響。

通過推送來更新和結(jié)束Live Activity
{
    "aps": {
        "timestamp": 1168364460,
        "event": "update",
        "content-state": {
            "driverName": "Anne Johnson",
            "estimatedDeliveryTime": 1659416400
        },
        "alert": {
            "title": "Delivery Update",
            "body": "Your pizza order will arrive soon.",
            "sound": "example.aiff" 
        }
    }
}

跟蹤更新

您啟動 Live Activity 時,ActivityKit 會返回一個Activity對象。除了id唯一標(biāo)識每個活動之外,Activity還提供觀察內(nèi)容狀態(tài)、活動狀態(tài)和推送令牌更新的序列。使用相應(yīng)的序列在您的應(yīng)用中接收更新,使您的應(yīng)用和 Live Activity 保持同步,并響應(yīng)更改的數(shù)據(jù):

  • 要觀察正在進(jìn)行的 Live Activity 的狀態(tài)——例如,確定它是處于活動狀態(tài)還是已經(jīng)結(jié)束——使用.activityStateUpdates

  • 要觀察 Live Activity 動態(tài)內(nèi)容的變化,請使用.contentState

  • 要觀察 Live Activity 的推送令牌的變化,請使用.pushTokenUpdates

獲取活動列表

您的應(yīng)用可以啟動多個 Live Activity。例如,體育應(yīng)用程序可能允許用戶為他們感興趣的每個現(xiàn)場體育比賽啟動現(xiàn)場活動。如果啟動多個現(xiàn)場活動,請使用該功能獲取有關(guān)您的應(yīng)用程序正在進(jìn)行的現(xiàn)場活動的通知。跟蹤正在進(jìn)行的 Live Activity 以確保您的應(yīng)用程序的數(shù)據(jù)與 ActivityKit 跟蹤的活動 Live Activity 同步。activityUpdates

以下代碼段顯示了披薩外賣應(yīng)用程序如何檢索正在進(jìn)行的活動列表:

// Fetch all ongoing pizza delivery Live Activities.
for await activity in Activity<PizzaDeliveryAttributes>.activityUpdates {
    print("Pizza delivery details: \(activity.attributes)")
}

獲取所有活動的另一個用例是維護(hù)正在進(jìn)行的實(shí)時活動,并確保您不會讓任何活動持續(xù)運(yùn)行超過需要的時間。例如,系統(tǒng)可能會停止您的應(yīng)用程序,或者您的應(yīng)用程序可能會在 Live Activity 處于活動狀態(tài)時崩潰。當(dāng)應(yīng)用下次啟動時,檢查是否有任何活動仍處于活動狀態(tài),更新應(yīng)用存儲的 Live Activity 數(shù)據(jù),并結(jié)束任何不再相關(guān)的 Live Activity。

參考文檔:

官方文檔

?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
【社區(qū)內(nèi)容提示】社區(qū)部分內(nèi)容疑似由AI輔助生成,瀏覽時請結(jié)合常識與多方信息審慎甄別。
平臺聲明:文章內(nèi)容(如有圖片或視頻亦包括在內(nèi))由作者上傳并發(fā)布,文章內(nèi)容僅代表作者本人觀點(diǎn),簡書系信息發(fā)布平臺,僅提供信息存儲服務(wù)。

相關(guān)閱讀更多精彩內(nèi)容

友情鏈接更多精彩內(nèi)容