iOS 靈動島Dynamic Island實現(xiàn)+模擬器測試遠(yuǎn)程推送

靈動島展示.jpeg

[閱讀難度:簡單]

準(zhǔn)備工作

技術(shù)儲備:

需對Xcode、Swift、SwiftUI 有一定了解

環(huán)境要求:

iOS >= 16.1
Xcode >= 14.1
設(shè)備(iPhone14Pro/iPhone14ProMax,模擬器也可以)

靈動島簡介

一句話總結(jié):“靈動島是展示在鎖屏界面與主屏幕狀態(tài)欄的“特殊”小組件

靈動島的狀態(tài)

靈動島不同狀態(tài).png

默認(rèn)狀態(tài):可展示左右兩個小視圖(LeadingSide、TrailingSide
展開狀態(tài):用戶長按靈動島的展開狀態(tài)(LeadingCenter、TrailingBottom),
緊湊狀態(tài):當(dāng)有多個APP靈動島時展示分離狀態(tài),依創(chuàng)建順序展示(MinimalDetached)

靈動島的生命周期:

靈動島主要分為StartUpdate、End三種狀態(tài),可由ActivityKit遠(yuǎn)程推送控制其狀態(tài)。
一個靈動島默認(rèn)展示8小時,結(jié)束后可繼續(xù)在鎖屏界面存在4個小時后由系統(tǒng)徹底移除,也可由開發(fā)者自行移除。

那么,我們開始吧·····


接入Widget Extension

  1. info.plist 文件中添加 NSSupportsLiveActivities 字段,設(shè)置為YES

  2. 選中項目添加target,創(chuàng)建Widget Extension,假設(shè)將其命名為“DynamicDemo”:

    target.png

  3. 在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時在 AppDelegateSceneDelegate 方法中識別其所帶入的 url ,以此來做對應(yīng)處理。

Q2:多個靈動島展示的優(yōu)先級

  1. 單個APP的多個靈動島:默認(rèn)視圖(依次展示)
  2. 多個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

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

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

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