背景
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里,使用SwiftUI和WidgetKit來創(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里。
創(chuàng)建Widget Target
打開Info.plist文件,添加 Supports Live Activity ,設(shè)置為YES
創(chuàng)建ActivityAttributes 結(jié)構(gòu),描述Live Activity的靜態(tài)和動態(tài)數(shù)據(jù)。
創(chuàng)建ActivityConfiguration通過ActivityAttributes,啟動Live Activity
添加代碼以配置、啟動、更新和結(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。
參考文檔: