
[閱讀難度:簡單]
準(zhǔn)備工作
技術(shù)儲備:
需對Xcode、Swift、SwiftUI 有一定了解
環(huán)境要求:
iOS >= 16.1
Xcode >= 14.1
設(shè)備(iPhone14Pro/iPhone14ProMax,模擬器也可以)
靈動島簡介
一句話總結(jié):“靈動島是展示在鎖屏界面與主屏幕狀態(tài)欄的“特殊”小組件”
靈動島的狀態(tài):

默認(rèn)狀態(tài):可展示左右兩個小視圖(
LeadingSide、TrailingSide)
展開狀態(tài):用戶長按靈動島的展開狀態(tài)(Leading、Center、Trailing、Bottom),
緊湊狀態(tài):當(dāng)有多個APP靈動島時展示分離狀態(tài),依創(chuàng)建順序展示(MinimalDetached)
靈動島的生命周期:
靈動島主要分為
Start、Update、End三種狀態(tài),可由ActivityKit與遠(yuǎn)程推送控制其狀態(tài)。
一個靈動島默認(rèn)展示8小時,結(jié)束后可繼續(xù)在鎖屏界面存在4個小時后由系統(tǒng)徹底移除,也可由開發(fā)者自行移除。
那么,我們開始吧·····
接入Widget Extension
info.plist 文件中添加
NSSupportsLiveActivities字段,設(shè)置為YES-
選中項目添加target,創(chuàng)建
Widget Extension,假設(shè)將其命名為“DynamicDemo”:
target.png 在Widget中,主要關(guān)注
WidgetsExtensionBundle,該結(jié)構(gòu)體下的body返回所有小組件的Widget,Xcode 會默認(rèn)生成小組件卡片的初始代碼,不需要的話可以移除。
@main
struct WidgetsExtensionBundle: WidgetBundle {
var body: some Widget {
// DynamicDemo() //原本的小組件卡片Widget
DynamicDemoLiveActivity() // 靈動島Widget
}
}
Tips:模擬器下偶現(xiàn)APP動態(tài)小組件權(quán)限被關(guān)閉,從“設(shè)置-對應(yīng)APP-實時活動”打開即可
2. 配置靈動島Widget
在Xcode14.1及之后的版本引入 WidgetsExtension 后,會默認(rèn)創(chuàng)建XXXXXXLiveActivity.swift文件:
struct DynamicDemoAttributes: ActivityAttributes {
public struct ContentState: Codable, Hashable {
// Dynamic stateful properties about your activity go here!
var value: Int
}
// Fixed non-changing properties about your activity go here!
var name: String
}
struct DynamicDemoLiveActivity: Widget {
var body: some WidgetConfiguration {
ActivityConfiguration(for: DynamicDemoAttributes.self) { context in
// Lock screen/banner UI goes here
VStack {
Text("Hello")
}
.activityBackgroundTint(Color.cyan)
.activitySystemActionForegroundColor(Color.black)
} dynamicIsland: { context in
DynamicIsland {
// Expanded UI goes here. Compose the expanded UI through
// various regions, like leading/trailing/center/bottom
DynamicIslandExpandedRegion(.leading) {
Text("Leading")
}
DynamicIslandExpandedRegion(.trailing) {
Text("Trailing")
}
DynamicIslandExpandedRegion(.bottom) {
Text("Bottom")
// more content
}
} compactLeading: {
Text("L")
} compactTrailing: {
Text("T")
} minimal: {
Text("Min")
}
.widgetURL(URL(string: "http://www.apple.com"))
.keylineTint(Color.red)
}
}
}
DynamicDemoLiveActivity: 靈動島的Widget,通過ActivityConfiguration來配置所有狀態(tài)下的視圖,包括不支持靈動島設(shè)備的設(shè)備在鎖屏頁面的視圖。
DynamicDemoAttributes: 靈動島屬性屬性結(jié)構(gòu)體,用于數(shù)據(jù)更新,由對應(yīng)的Activity使用。
3. 控制靈動島
在需要控制靈動島的地方,引入ActivityKit。針對“Start”、“Update”、“End”三種狀態(tài)分別舉個簡單的例子:
開啟:
Task{
// 配置Attributes
let data = DynamicDemoAttributes(value: 100)
let state = DynamicDemoAttributes.ContentState()
// 根據(jù) Attributes 開啟一個靈動島
do {
// reqeust 指定 pushType 為 .token 可獲取遠(yuǎn)程推送所需的 token
try await MainActor.run {
let activity = try Activity<DynamicDemoAttributes>.request(attributes: data, contentState: state)
}
} catch (let error) {
print(error.localizedDescription)
}
}
更新:
do {
let data = DynamicDemoAttributes(value:100)
let state = DynamicDemoAttributes.ContentState(progressMsg: "更新的文案")
if let activity = Activity<DynamicDemoAttributes>.activities.first {
await activity.update(using: state)
} else {
try Activity<DynamicDemoAttributes>.request(attributes: data, contentState: state)
}
} catch (let error) {
print(error.localizedDescription)
}
代碼中取的 activities.first,實際開發(fā)中應(yīng)該根據(jù)數(shù)據(jù)來區(qū)分并獲取需要更新的靈動島實例。
通過獲取當(dāng)前需要更新的靈動島實例,通過調(diào)用update方法傳入最新的Attributes更新。
Tips: 靈動島可由宿主APP與遠(yuǎn)程推送更新,普通APP會在退到后臺后10s內(nèi)被掛起,例如音樂、健身、定位、通話等可常駐后臺的應(yīng)用可直接通過宿主APP及時更新。對于退出APP后需要更新的場景,可由遠(yuǎn)程推送實現(xiàn)。具體可參考文末。
結(jié)束:
//移除所有
Activity<DynamicDemoAttributes>.activities.forEach { item in
Task{
// await item.end() //默認(rèn)結(jié)束后,會在鎖屏界面等待4小時徹底移除
await item.end(dismissalPolicy:.immediate) // 立即結(jié)束
}
}
Tips: 指定
.immediate可以立即移除
Q&A
Q1:如何識別用戶點擊的視圖:
當(dāng)前的靈動島不支持非系統(tǒng)自定義交互(截止20221121),支持DeepLink、Link跳轉(zhuǎn)。點擊靈動島默認(rèn)跳轉(zhuǎn)我們的宿主APP,我們可以在靈動島視圖中配置widgetURL()、Link()來達(dá)到識別用戶點擊的效果、以Link()舉例:
···············
DynamicIslandExpandedRegion(.bottom) {
Link(destination: URL(string: "xxxxxx://hotel/contact")!) {
Label("Contact Hotel", systemImage: "phone").padding()
}.background(Color.accentColor)
.clipShape(RoundedRectangle(cornerRadius: 15))
}
···············
上面代碼可以在用戶點擊了靈動島的bottom視圖跳轉(zhuǎn)進(jìn)APP時在 AppDelegate 或 SceneDelegate 方法中識別其所帶入的 url ,以此來做對應(yīng)處理。
Q2:多個靈動島展示的優(yōu)先級
- 單個APP的多個靈動島:默認(rèn)視圖(依次展示)
- 多個APP的靈動島:緊湊視圖/分離視圖(依次展示)
Q3:如何在模擬器模擬遠(yuǎn)程推送更新
常規(guī)的推送可查看《iOS 在模擬器上測試遠(yuǎn)程推送》。在調(diào)研靈動島的過程中,需要模擬器測試遠(yuǎn)程推送的功能。查閱一圈資料后成功實現(xiàn),具體步驟如下:
環(huán)境要求:
Xcode >= 14.1,
MacOS >= 13.0
準(zhǔn)備工作
如何獲取遠(yuǎn)程推送的token:
let activity = try
// 指定pushType為.token
Activity<OrderProgressWidgetAttributes>.request(attributes: data, contentState: state, pushType: .token)
orderProgressActivity = activity
Task {
for await data in activity.pushTokenUpdates {
let myToken = data.map {String(format: "%02x", $0)}.joined()
print(myToken);
// 將token告知后端用于遠(yuǎn)程推送
}
}
運(yùn)行CommandLine
配置所需要使用的宏:
$ export TEAM_ID={TEAM ID}
$ export TOKEN_KEY_FILE_NAME={Token Key file path}
$ export AUTH_KEY_ID={your Auth Key ID}
$ export DEVICE_TOKEN={myToken from the activity push token} 設(shè)備Token(靈動島是Activity start后返回的pushtoken)
$ export APNS_HOST_NAME=api.sandbox.push.apple.com
說明:
TEAM_ID:開發(fā)者TeamID(開發(fā)者賬戶可查)
TOKEN_KEY_FILE_NAME:開發(fā)者賬戶生成的Keys下載后的文件路徑(.p8格式)
AUTH_KEY_ID:開發(fā)者賬戶生成的keys的ID(開發(fā)者賬號內(nèi)可查、下載的.p8文件默認(rèn)后綴前的十個字符)
DEVICE_TOKEN:設(shè)備Token(靈動島場景下是Activity指定pushType為.token后start返回的pushtoken)
配置命令行推送(命令行逐行運(yùn)行,注意中英字符):
$ export JWT_ISSUE_TIME=$(date +%s)
$ export JWT_HEADER=$(printf '{ "alg": "ES256", "kid": "%s" }' "${AUTH_KEY_ID}" | openssl base64 -e -A | tr -- '+/' '-_' | tr -d =)
$ export JWT_CLAIMS=$(printf '{ "iss": "%s", "iat": %d }' "${TEAM_ID}" "${JWT_ISSUE_TIME}" | openssl base64 -e -A | tr -- '+/' '-_' | tr -d =)
$ export JWT_HEADER_CLAIMS="${JWT_HEADER}.${JWT_CLAIMS}"
$ export JWT_SIGNED_HEADER_CLAIMS=$(printf "${JWT_HEADER_CLAIMS}" | openssl dgst -binary -sha256 -sign "${TOKEN_KEY_FILE_NAME}" | openssl base64 -e -A | tr -- '+/' '-_' | tr -d =)
$ export AUTHENTICATION_TOKEN="${JWT_HEADER}.${JWT_CLAIMS}.${JWT_SIGNED_HEADER_CLAIMS}"
發(fā)送推送:
curl -v \
--header "apns-topic:po.test.push-type.liveactivity" \
--header "apns-push-type:liveactivity" \
--header "authorization:bearer $AUTHENTICATION_TOKEN" \
--data \
'{"Simulator Target Bundle": "po.test",
"aps": {
"timestamp":1668764100,
"event": "update",
"content-state": {
"progressMsg": "酒店訂單已確認(rèn)",
}
}}' \
--http2 \
https://${APNS_HOST_NAME}/3/device/$DEVICE_TOKEN
apns-topic:固定為{BundleId}.push-type.liveactivity
apns-push-type:固定為liveactivity
Simulator Target Bundle:測試模擬器推送,設(shè)置為對應(yīng)應(yīng)用的{BundleId}
timestamp:刷新時間戳。需設(shè)置正確,否則靈動島的推送不會生效
event:可填入update、end,對應(yīng)靈動島的更新與結(jié)束。
content-state:對應(yīng)靈動島的Attributes
參考資料
本文只是簡單介紹了基礎(chǔ)功能的實現(xiàn),更豐富的探索可查閱官方文檔:
https://developer.apple.com/documentation/activitykit/displaying-live-data-with-live-activities
https://developer.apple.com/documentation/widgetkit/dynamicisland/
https://developer.apple.com/documentation/activitykit/update-and-end-your-live-activity-with-remote-push-notifications
https://developer.apple.com/design/human-interface-guidelines/components/system-experiences/live-activities
https://developer.apple.com/news/?id=mis6swzt
