實戰(zhàn):Live Activity與ActivityKit完整接入指南)
靈動島從 iPhone 14 Pro 發(fā)布到現(xiàn)在已經(jīng)不是一個新概念但對開發(fā)者來說它仍然是一個“看起來容易、做起來容易踩坑”的功能。很多人只是把靈動島理解為挖孔屏上的黑色藥丸動畫實際上它是基于系統(tǒng)級 UI 渲染、Live Activity 狀態(tài)管理和動態(tài)交互的一套完整能力。換句話說靈動島的開發(fā)價值不在于把按鈕塞進那個“島”里而在于怎么用 Live Activities 把后臺任務(wù)實時地呈現(xiàn)給用戶并且在一套系統(tǒng)規(guī)范下安全、穩(wěn)定地更新。這篇文章會把靈動島拆成開發(fā)向的問題來看它能做什么、做不了什么、需要哪些前置條件、用 SwiftUI 怎么搭、用 ActivityKit 怎么創(chuàng)建和更新 Live Activity、真機調(diào)試要注意什么、常見崩潰和顯示異常怎么排查。如果你正在做外賣進度、騎行導航、運動記錄、會議提醒、音樂播放這類需要“鎖屏也能看進度”的 App這篇文章可以直接當成一篇落地參考來用。1. 靈動島核心能力速覽動島是 iOS 在系統(tǒng)層面提供的一種交互式狀態(tài)展示區(qū)域。它的常用載體是 Live Activity實時活動開發(fā)者通過 ActivityKit 和 WidgetKit 來驅(qū)動顯示內(nèi)容。能力項說明系統(tǒng)要求支持 Live Activity 的系統(tǒng)版本通常為 iOS 16.1 及以上靈動島顯示以帶靈動島的機型為準開發(fā)語言Swift、SwiftUI部分場景需要 Widget Extension主要框架ActivityKit、WidgetKit、SwiftUI遠程更新場景需要 APNs啟動方式在 App 中調(diào)用 ActivityKit 請求啟動 Live Activity靈動島區(qū)域由系統(tǒng)接管核心功能鎖屏實時活動、靈動島緊湊顯示、靈動島擴展顯示、狀態(tài)更新、結(jié)束時刪除交互能力點擊靈動島區(qū)域可喚起 App也可配合 Deep Link 跳轉(zhuǎn)指定頁面推送更新支持通過遠程推送更新動態(tài)島內(nèi)容也支持本地刷新是否支持批量任務(wù)單個 App 可同時存在多個 Live Activity系統(tǒng)按順序展示在靈動島和鎖屏上適合場景外賣配送、打車、運動計步、航班動態(tài)、計時器、下載進度、賽事比分、會議提醒等2. 適用場景與使用邊界靈動島適合展示的是“用戶離開 App 之后仍然關(guān)心的短時任務(wù)”。典型特征是任務(wù)有明確的狀態(tài)變化、變化頻率不需要太高、用戶希望在不解鎖或不清掃的情況下就能看到結(jié)果。比較典型的場景包括外賣或快遞訂單的配送進度。打車時查看司機位置和預計到達時間。運動 App 記錄跑步距離和時長。航班 App 展示登機口和起飛時間變化。音樂播放器在切到后臺后顯示播放狀態(tài)。倒計時和番茄鐘場景。靈動島不適合做長駐通知也不適合做高頻刷新的信息流。它本質(zhì)上是“實時活動”的展示殼層蘋果對 Live Activity 有明確的系統(tǒng)級限制比如自動過期時間、更新頻率限制、展示位置策略等。把它當成普通通知中心來用很快就會遇到任務(wù)被系統(tǒng)自動清理或靈動島區(qū)域被多次活動擠占的問題。這里也要提醒一句不要用靈動島強制引導用戶點擊廣告或誘導開啟 Live Activity。展示內(nèi)容如果涉及訂單、定位、運動數(shù)據(jù)或用戶隱私必須在合法授權(quán)的前提下使用。騎手位置、司機信息、用戶行程這類數(shù)據(jù)單獨看一個字段可能沒問題但組合起來能推斷出用戶行為軌跡發(fā)布前要做隱私合規(guī)評估。涉及真實人物照片、聲音、人臉識別等相關(guān)功能時上線前必須確認已經(jīng)取得明確授權(quán)。3. 開發(fā)環(huán)境與前置條件3.1 硬件與系統(tǒng)前提靈動島是硬件和系統(tǒng)配合的結(jié)果不是所有 iPhone 都能顯示。做開發(fā)時至少需要一臺帶靈動島的 iPhone 真機因為模擬器不能完全驗證靈動島在真實亮度、鎖屏狀態(tài)、通知共存下的表現(xiàn)。如果你要在真機上調(diào)試 Live Activity需要把系統(tǒng)升級到 iOS 16.1 及以上。Xcode 也要升級到支持 ActivityKit 的版本過于舊的 Xcode 連 framework 都找不到。3.2 Xcode 開發(fā)配置開發(fā)靈動島功能會在一個普通 iOS App 工程里做這些事使用 ActivityKit 發(fā)起 Live Activity。新建一個 Widget Extension用來渲染鎖屏和靈動島。在 Widget Extension 中配置 ActivityConfiguration。在主 App 中根據(jù)業(yè)務(wù)事件調(diào)用更新和結(jié)束方法。創(chuàng)建項目和 Extension 時可以按以下路徑操作File - New - Target選擇Widget Extension勾選Include Live Activity填寫 Extension 名稱如果項目已經(jīng)建好但沒勾選 Include Live Activity也可以通過手動創(chuàng)建 ActivityConfiguration 的方式來補但更穩(wěn)妥的做法是重新生成一個帶 Live Activity 的 Widget Target然后再把代碼遷進去。Widget Extension 需要獨立的 Bundle Identifier通常是在主 App 的 Bundle ID 后面加.Widget。簽名、授權(quán)、Team ID 配置好之后App 和 Widget Extension 才能共享 ActivityKit 的數(shù)據(jù)。3.3 部署目標建議由于 ActivityKit 是 iOS 16.1 以后才有的能力開發(fā)時不要直接把 Widget 的 deployment target 設(shè)到更低版本否則需要在代碼里做可用性判斷if #available(iOS 16.1, *) { // 使用 ActivityKit } else { // 降級到普通通知或本地推送 }這是很關(guān)鍵的一點。靈動島能力只對支持 Live Activity 的系統(tǒng)版本生效老系統(tǒng)用戶需要走原來的通知邏輯不能把靈動島當成唯一的狀態(tài)出口。4. 靈動島 UI 設(shè)計與 SwiftUI 布局靈動島并不是一塊自由繪制的屏幕區(qū)域。蘋果把它分成幾類形態(tài)開發(fā)者的控制范圍取決于當前狀態(tài)。4.1 靈動島的三種主要形態(tài)形態(tài)使用時機可布置內(nèi)容緊湊形態(tài)只有一個 Live Activity 且不是最前時左側(cè)圖標或文字 右側(cè)圖標或文字最小形態(tài)多個 Live Activity 并存時單一小圖標或極簡信息擴展形態(tài)長按或處于前臺時展開左側(cè)、右側(cè)、底部區(qū)域可放更多內(nèi)容這種系統(tǒng)接管的設(shè)計決定你不應(yīng)該用自定義 View 把整個島蓋住。做 UI 時應(yīng)該先遵守系統(tǒng)給的區(qū)域劃分和布局約束否則在部分機型上會出現(xiàn)截斷、偏移或內(nèi)容顯示不全的問題。4.2 SwiftUI 基礎(chǔ)布局靈動島區(qū)域通常通過DynamicIslandExpandedRegion來組織內(nèi)容。下面是一段典型的 SwiftUI 布局示意DynamicIsland { DynamicIslandExpandedRegion(.leading) { Label(配送中, systemImage: shippingbox.fill) .font(.headline) } DynamicIslandExpandedRegion(.trailing) { Text(剩余 800 米) .font(.caption) } DynamicIslandExpandedRegion(.bottom) { ProgressView(value: 0.8) .progressViewStyle(.linear) .tint(.green) Text(騎手正在配送請保持電話暢通) .font(.caption2) .foregroundColor(.secondary) } } compactLeading: { Image(systemName: shippingbox.fill) } compactTrailing: { Text(800m) .font(.caption2) } minimal: { Image(systemName: shippingbox.fill) }上面這段代碼是在構(gòu)建訂單配送場景的靈動島擴展區(qū)域。注意SwiftUI 布局不能超過安全區(qū)域邊界信息過多時寧可精簡也不要為了“顯示完整”而使用會被裁剪的復雜布局。做靈動島 UI 時一個實際經(jīng)驗是先做鎖屏實時活動視圖再做靈動島視圖。因為鎖屏視圖代碼比較直接靈動島區(qū)域則依賴系統(tǒng)狀態(tài)切換調(diào)試起來成本更高。5. ActivityKit 接入與 Live Activity 開發(fā)UI 只是外觀真正控制靈動島生命周期的是 ActivityKit。5.1 定義 ActivityAttributes每個 Live Activity 需要在工程中定義一個遵循ActivityAttributes協(xié)議的類型。這個類型里要區(qū)分兩類數(shù)據(jù)固定數(shù)據(jù)啟動 Live Activity 之后不變化的屬性。ContentState任務(wù)過程中會不斷變化的狀態(tài)數(shù)據(jù)。比如訂單配送import ActivityKit struct DeliveryActivityAttributes: ActivityAttributes { public struct ContentState: Codable, Hashable { var statusText: String var progress: Double } var orderNumber: String var deliveryAddress: String }orderNumber適合放在外部固定數(shù)據(jù)中因為這一單啟動后不會變。配送文案、進度條數(shù)值則要放進ContentState方便每次更新活動時替換。5.2 啟動 Live Activity當用戶下單成功或進入某個實時任務(wù)時主 App 調(diào)用 ActivityKit 啟動活動。在 iOS 16.2 之前常見寫法是let initialState DeliveryActivityAttributes.ContentState( statusText: 商家已接單, progress: 0.1 ) let activity try? ActivityDeliveryActivityAttributes.request( attributes: DeliveryActivityAttributes( orderNumber: A10086, deliveryAddress: 杭州市余杭區(qū) ), contentState: initialState, pushType: nil )在 iOS 16.2 以后建議使用ActivityContentlet initialContent ActivityContent( state: DeliveryActivityAttributes.ContentState( statusText: 騎手已取餐, progress: 0.6 ), staleDate: nil ) let activity try? ActivityDeliveryActivityAttributes.request( attributes: DeliveryActivityAttributes( orderNumber: A10086, deliveryAddress: 杭州市余杭區(qū) ), content: initialContent, pushType: nil )啟動成功后系統(tǒng)會返回一個Activity實例。后面所有更新和結(jié)束操作都基于這個實例完成。注意啟動 Live Activity 并不是一定成功。系統(tǒng)在磁盤空間不足、后臺活動過多、狀態(tài)受限等情況下可能拒絕創(chuàng)建。代碼里不要用try!應(yīng)該對失敗做降級處理比如退回本地通知。5.3 更新 Live Activity當業(yè)務(wù)狀態(tài)發(fā)生變化時主 App 要創(chuàng)建新的ContentState并調(diào)用更新。以騎手到達為例let newContent ActivityContent( state: DeliveryActivityAttributes.ContentState( statusText: 騎手已送達, progress: 1.0 ), staleDate: nil ) Task { await activity?.update(newContent) }實時狀態(tài)更新不要太頻繁。靈動島面向的是短時任務(wù)不需要在幾百毫秒內(nèi)連續(xù)刷新幾十次。頻繁更新會造成電量消耗上升也容易觸發(fā)系統(tǒng)對 Live Activity 的刷新限制。如果 App 在前臺狀態(tài)變化可以用本地 API 更新如果 App 退到后臺甚至被殺死要依賴推送來喚醒系統(tǒng)更新。5.4 結(jié)束 Live Activity任務(wù)一旦到達終止狀態(tài)就應(yīng)該及時調(diào)用結(jié)束方法避免占用系統(tǒng)資源let finalContent ActivityContent( state: DeliveryActivityAttributes.ContentState( statusText: 訂單已完成, progress: 1.0 ), staleDate: nil ) Task { await activity?.end(finalContent, dismissalPolicy: .immediate) }結(jié)束策略有三種常用情況終止方式適用場景.default由系統(tǒng)決定何時移除.immediate任務(wù)已經(jīng)完成立即移除.after(Date)延遲一段時間后再移除適合展示完成后的獎勵或總結(jié)不要忘記結(jié)束邏輯。尤其是外賣、打車、倒計時這類最終會終止的任務(wù)如果用戶已經(jīng)取消訂單但 Live Activity 還掛在島上體驗會非常錯亂。6. Widget Extension 中渲染靈動島ActivityKit 只是控制生命周期真正畫出來的是 Widget Extension。因為靈動島會被系統(tǒng)在多個區(qū)域、多種形態(tài)下調(diào)度所有 UI 必須收斂到同一個ActivityConfiguration中。6.1 注冊 Live Activity Widget在 Widget Bundle 中需要把鎖屏 Widget 和 Live Activity Widget 都放進去main struct DeliveryWidgetBundle: WidgetBundle { var body: some Widget { DeliveryLockScreenWidget() DeliveryLiveActivity() } } struct DeliveryLiveActivity: Widget { var body: some WidgetConfiguration { ActivityConfiguration(for: DeliveryActivityAttributes.self) { context in // 這里是鎖屏上的實時活動視圖 LockScreenDeliveryView(context: context) } dynamicIsland: { context in // 這里是靈動島視圖 } } }這里的ActivityConfiguration是整個開發(fā)的橋接點。context.state對應(yīng)ContentStatecontext.attributes對應(yīng)自定義的固定屬性。6.2 鎖屏實時活動視圖鎖屏視圖是一塊相對寬裕的展示區(qū)域可以放置更多信息struct LockScreenDeliveryView: View { let context: ActivityViewContextDeliveryActivityAttributes var body: some View { HStack(spacing: 12) { Image(systemName: shippingbox.fill) .font(.title2) .foregroundColor(.blue) VStack(alignment: .leading, spacing: 4) { Text(訂單 \(context.attributes.orderNumber)) .font(.headline) Text(context.state.statusText) .font(.subheadline) .foregroundColor(.secondary) ProgressView(value: context.state.progress) .tint(.blue) } } .padding() } }鎖屏視圖和普通 Widget 一樣會被系統(tǒng)緩存不要在里面發(fā)起網(wǎng)絡(luò)請求或執(zhí)行耗時操作。它只是狀態(tài)的“投影”不是業(yè)務(wù)邏輯執(zhí)行器。6.3 讓靈動島能點擊跳轉(zhuǎn)靈動島不是純展示點擊它可以喚起 App。這一步要做的是在動態(tài)島區(qū)域里添加 SwiftUI 的Link或Button并通過 deep link 讓 App 打開對應(yīng)頁面。DynamicIslandExpandedRegion(.bottom) { HStack { Text(context.state.statusText) Spacer() Link(destination: URL(string: yourapp://order/\(context.attributes.orderNumber))!) { Text(查看詳情) } } }主 App 側(cè)需要用onOpenURL處理這個 deep link根據(jù)訂單號跳轉(zhuǎn)到對應(yīng)的詳情頁。如果 App 被靈動島喚起時進程已經(jīng)被系統(tǒng)清理就要在冷啟動路由中解析 URL保證用戶點進去后能看到正確頁面。7. 遠程推送更新與動態(tài)內(nèi)容下發(fā)在實際業(yè)務(wù)中App 進程不一定常駐。訂單狀態(tài)往往由服務(wù)端更新這時候不能依賴 App 自己刷新 Live Activity必須通過推送更新靈動島內(nèi)容。遠程更新 Live Activity 會用到 APNs 推送的content-state數(shù)據(jù)。服務(wù)端發(fā)送推送時請求體里包含活動對應(yīng)的push-type、activity-id和狀態(tài)字段。不同業(yè)務(wù)字段需要與 App 里的ContentState字段保持一致否則系統(tǒng)可能解析失敗或展示舊數(shù)據(jù)。從開發(fā)流程上看需要先請求pushTokenTask { let pushToken try await activity.pushToken // 把 pushToken 上傳到服務(wù)端 }請求到 token 之后由服務(wù)端保存并關(guān)聯(lián)到當前訂單。后面每個訂單狀態(tài)節(jié)點服務(wù)端都往 APNs 發(fā)一條 Live Activity 推送。推送內(nèi)容不需要攜帶全部 UI 數(shù)據(jù)只需攜帶變化的ContentState字段。這套機制能把實時性從“App 還活著”擴展到“App 被殺死之后依然能更新”。遠程推送更新需要后端配合所以做靈動島功能時建議和后端把推流協(xié)議先對齊不要把字段定義放在客戶端開發(fā)最后階段才確認。8. 功能測試與效果驗證靈動島功能必須在真機上驗證不能只看 SwiftUI 預覽。建議按下面的順序做測試8.1 基礎(chǔ)啟動測試先在 App 里點擊一個按鈕啟動 Live Activity確認控制臺沒有報錯隨后按電源鍵鎖屏觀察靈動島上是否出現(xiàn)緊湊形態(tài)。如果屏幕上同時存在多個 Live Activity長按靈動島區(qū)域觀察是否能展開并看到自己 App 的內(nèi)容。8.2 狀態(tài)更新測試啟動 Live Activity 后切到后臺再通過通知或本地模擬按鈕觸發(fā)狀態(tài)更新。每更新一次鎖屏和靈動島上的文字、進度條都應(yīng)該變化。若內(nèi)容不變先檢查是不是同一個 Activity 實例更新。一個常見錯誤是每次狀態(tài)變化都調(diào)用request重新創(chuàng)建 Activity結(jié)果舊的 Activity 一直不結(jié)束導致靈動島被多個重復活動占滿。正常業(yè)務(wù)里應(yīng)該把activity實例做成訂單維度的單例或由管理器持有。8.3 結(jié)束測試結(jié)束 Activity 后鎖屏和靈動島上的內(nèi)容都應(yīng)消失。如果結(jié)束后仍然殘留很可能是結(jié)束的不是同一個實例或者結(jié)束方法沒有走完。測試時可以用這個順序創(chuàng)建 Activity。寫入日志記錄 activity.id。用這個 id 執(zhí)行更新和結(jié)束。檢查 UI 是否按預期消失。8.4 推送測試推送更新是最容易出問題的環(huán)節(jié)。測試時不要直接從 App 內(nèi)部調(diào)用更新而要模擬服務(wù)端推送從 APNs 發(fā)一條 payload 過來觀察 UI 是否正常變化。一些推送服務(wù)會阻塞在證書或 token 校驗階段排錯時先檢查 APNs 響應(yīng)碼不要只看客戶端是否報錯。9. 資源占用與性能觀察靈動島日常承載的 UI 非常輕量它不是無限動畫的畫布。系統(tǒng)對 Live Activity 的更新頻率、運行時長度、展示數(shù)據(jù)量都有限制這也是為了控制耗電和系統(tǒng)資源占用。在開發(fā)時觀察性能和穩(wěn)定性可以重點關(guān)注這幾點大量動畫是否卡頓。靈動島不是動畫播放器盡量避免使用復雜的 Spring 動畫。高頻更新是否被系統(tǒng)丟棄。如果業(yè)務(wù)要求每秒鐘刷新多次位置建議先降到 5 到 10 秒一次再觀察效果。多個 Activity 并存時是否出現(xiàn)覆蓋。測試時至少要創(chuàng)建兩個 Activity確認系統(tǒng)如何排列和切換。App 被殺死后Activity 是否還能正常更新。這個場景依賴推送鏈路本地更新無法覆蓋。如果想分析耗電可以在真機上用 Xcode 的 Energy Log 或位置跟蹤記錄對比有 Live Activity 和無 Live Activity 時的耗電差異。但需要注意這跟推送頻率、定位權(quán)限、屏幕常亮都有關(guān)系不能單看靈動島一個變量。9.1 降低資源占用的常見做法禁用不必要的動態(tài)效果。減少Text和圖片資源的頻繁變化。同一時間盡量只保留一個 Live Activity。狀態(tài)已經(jīng)結(jié)束時就立即調(diào)用結(jié)束方法。推送頻率控制在業(yè)務(wù)可容忍的最低水平。10. 常見問題與排查方法開發(fā)靈動島功能時最經(jīng)常遇到的是下面這些問題。問題現(xiàn)象可能原因排查方式解決方案Activity.request 無法創(chuàng)建活動系統(tǒng)版本低于 iOS 16.1或系統(tǒng)資源受限檢查版本、檢查磁盤可用空間和后臺活動數(shù)量降級到 iOS 16.1 以下的通知重啟設(shè)備后再試靈動島區(qū)域不顯示自己的 AppWidget Target 沒有包含 Live Activity 配置檢查 Widget Bundle 中是否注冊 ActivityConfiguration注冊對應(yīng) ActivityConfiguration動態(tài)內(nèi)容更新后 UI 沒變化更新了舊的 Activity 實例對比控制臺打印的 activity.id更新正確的活動實例必要時統(tǒng)一管理 Activity更新后 UI 延遲或丟失更新頻率過高或被系統(tǒng)節(jié)流檢查業(yè)務(wù)側(cè)是否每幾百毫秒觸發(fā)一次更新降低更新頻率合并多次中間狀態(tài)多個活動同時存在時被擠掉單 App 或系統(tǒng)同時活動數(shù)量限制創(chuàng)建多個 Activity 觀察切換優(yōu)先保留最關(guān)鍵的實時活動及時結(jié)束不重要的活動鎖屏正常但靈動島空白Widget 中沒有實現(xiàn) dynamicIsland 閉包檢查 ActivityConfiguration 的 dynamicIsland 部分代碼補充 compactLeading、compactTrailing、minimal 實現(xiàn)推送更新失敗pushToken 未上傳、payload 字段不一致或 APNs 證書錯誤檢查服務(wù)端 APNs 返回值按 APNs 文檔修正 payload 和簽名模擬器無法完整驗證模擬器沒有真實硬件形態(tài)和環(huán)境換真機測試所有最終驗證必須在支持靈動島的 iPhone 真機上執(zhí)行此外許多在 SwiftUI 預覽中正常顯示的布局放到真機后可能被截斷。排查時先把文本數(shù)量、字體大小、自定義 padding 都降到系統(tǒng)默認狀態(tài)確認是不是自建布局超出安全區(qū)域?qū)е碌膯栴}。11. 最佳實踐與使用建議如果團隊第一次接靈動島建議按這套流程來做先做鎖屏實時活動再做靈動島。這樣能把 ActivityKit 生命周期和 UI 渲染兩件事拆開減少一次調(diào)多個變量的排查成本。一個 Activity 對應(yīng)一個有明確 id 的業(yè)務(wù)實體不要散落在任意 View 里。創(chuàng)建、更新、結(jié)束三組方法統(tǒng)一封裝成服務(wù)類方便在業(yè)務(wù)層調(diào)用也方便打日志。所有 ContentState 字段需要設(shè)計成可 Codable 的穩(wěn)定結(jié)構(gòu)因為推送更新和服務(wù)端共用同一套 JSON 字段。在線狀態(tài)變化時把中間態(tài)壓縮為最終態(tài)。比如配送過程里騎手位置連續(xù)變化了幾十次推到 Live Activity 上時只保留最近幾次關(guān)鍵狀態(tài)即可。測試推送更新時從 APNs 返回的失敗信息開始排查而不是反復在客戶端里嘗試。上線前要檢查深鏈接是否在冷啟動、熱啟動、后臺恢復三種狀態(tài)下都能正確跳轉(zhuǎn)。涉及訂單、地理位置、用戶身份等數(shù)據(jù)時確認推送內(nèi)容里不包含不必要的敏感字段。即使推送內(nèi)容需要展示地址信息也最好在前端截取展示字段不要把原始完整地址傳給臨時推送鏈路。發(fā)布前留出真機回歸時間靈動島最終效果依賴真實硬件自動化測試很難完全覆蓋。靈動島這項技術(shù)本身不復雜難點在于和業(yè)務(wù)生命周期耦合。只要 Live Activity 的啟動時機、狀態(tài)更新頻率、結(jié)束條件這三個點設(shè)計得足夠清楚開發(fā)過程就不會太痛苦。接下來的重點可以繼續(xù)放在遠程推送鏈路和業(yè)務(wù)穩(wěn)定性上先把一個高頻場景跑通再逐步擴展到更多任務(wù)類型。