Apple?Watch?App?Lifecycle應(yīng)用開發(fā)
Watch App Lifecycle
watchOS App 的生命周期比 iOS App 的生命周期要更復雜一些。watchOS App 可能會處于以下五種狀態(tài):
- Not running - 未運行
- Inactive - 不活躍
- Active - 活躍
- Background - 后臺
- Suspended - 掛起
常見的狀態(tài)轉(zhuǎn)換
watchOS App 的五種狀態(tài)由下圖中的紫色框表示:
作為開發(fā)者,我們只會與其中三個狀態(tài)進行交互:Inactive、Active、Background。 而在 Not running 和 Suspended 狀態(tài)下 App 未運行。
啟動 App 到 Active 狀態(tài)
如果用戶尚未運行 App 或系統(tǒng)從內(nèi)存中清除了 App,則 App 以 Not running 狀態(tài)開始。App 啟動后,它會從 Not running 轉(zhuǎn)換到 Inactive 狀態(tài)。
處于 Inactive 狀態(tài)時,App 仍然在前臺運行,但不會響應(yīng)用戶的任何操作。但是 App 可能仍在執(zhí)行某些代碼。在此狀態(tài)下,App 并不是無法運行。
App 幾乎立即將轉(zhuǎn)換到 Active 狀態(tài),這是在 Apple Watch 屏幕上運行的 App 的正常模式。當處于 Active 狀態(tài)時,App 可以接收來自 Apple Watch 上的物理控件和用戶手勢的操作。
當用戶啟動我們的 App 但尚未運行時,watchOS 將執(zhí)行以下操作:
- 調(diào)用 applicationDidFinishLaunching() 并將 scenePhase 設(shè)置為 .inactive;
- 創(chuàng)建 App 的初始 scene 和 rootView;
- 調(diào)用 applicationWillEnterForeground();
- 調(diào)用 applicationDidBecomeActive() 并將 scenePhase 設(shè)置為 .active;
- App 將出現(xiàn)在屏幕上,watchOS 將調(diào)用 rootView 的 onAppear(perform:)。
注意:從 watchOS 7 及更高版本開始,在使用 SwiftUI 時,scenePhase 環(huán)境變量的更新,要早于 WKExtensionDelegate 方法的調(diào)用。
App 到 Inactive 狀態(tài)
一旦用戶放下手臂,watchOS 將變?yōu)?Inactive 狀態(tài)。如前所述,該 App 仍在運行并執(zhí)行我們的代碼。Inactive 狀態(tài)需要減少 App 對 Apple Watch 電池的影響,我們應(yīng)暫?;蛉∠魏尾恍枰碾姵孛芗筒僮?。例如,我們可以禁止當前正在運行的動畫的展示。
我們還需要考慮是否需要保存一些數(shù)據(jù),保存 Core Data Stack?向 UserDefaults 寫入內(nèi)容?其實,一旦用戶再次抬起手臂,App 就會再次激活。因此,此時并不需要保存或保存太多內(nèi)容,否則可能會對電量產(chǎn)生較大壓力。
當我們的 App 轉(zhuǎn)換到 Inactive 狀態(tài)時,watchOS 會將 scenePhase 設(shè)置為 .inactive,然后調(diào)用 WKExtensionDelegate 的 applicationWillResignActive() 方法。
App 到 Background 狀態(tài)
在轉(zhuǎn)換到 Inactive 狀態(tài)兩分鐘后,或者當用戶切換到另一個 App 時,我們的 App 將轉(zhuǎn)換到 Background 狀態(tài)。通過系統(tǒng)也可以直接將 App 啟動到 Background 狀態(tài),如后 Background session 和 Background task。
在 Suspended 狀態(tài)之前,操作系統(tǒng)會為 Background 狀態(tài)的 App 提供一小段不確定的時間。如果我們的 App 從 Inactive 狀態(tài)轉(zhuǎn)換到 Background 狀態(tài),我們需要快速執(zhí)行必要的操作來處理 App。
我們可以使用 SwiftUI ScenePhase 或 WKExtensionDelegate 的 applicationDidEnterBackground 來確定我們的 App 何時到 Background 狀態(tài)。當從 Inactive 狀態(tài)轉(zhuǎn)換到 Background 狀態(tài)時,watchOS 會將scenePhase 設(shè)置為 .background,然后調(diào)用applicationDidEnterBackground()。如果 App 需要太多資源,watchOS 將暫停該 App。
返回表盤
在 watchOS 7 之前,我們可以在 App 轉(zhuǎn)換到 Background 后請求 8 分鐘。在 watchOS 7 及后續(xù)版本中,用戶可以通過 Apple Watch 上的 設(shè)置 ? 通用 ? 返回表盤 來進行超時配置。用戶可以選擇三個選項:“始終”、“2 分鐘后”或“1 小時后”。默認情況下,所選設(shè)置會應(yīng)用到所有 App,但用戶也可以為每個 App 選取自定時間。默認值為兩分鐘。 根據(jù)功能需要,我們可能希望告訴用戶如何將 App 的設(shè)置更改為一小時。
額外的 Background 執(zhí)行時間
如果在轉(zhuǎn)換到 Background 時,App 需要執(zhí)行的工作量比 watchOS 為我們的App 提供時間要長,那么我們需要重新考慮 App 進行的操作,例如刪除網(wǎng)絡(luò)調(diào)用。如果我們已經(jīng)進行了所有可以進行的優(yōu)化,但仍然需要更多的處理時間,我們可以調(diào)用 ProcessInfo 類的 performExpiringActivity(withReason:using:) 方法。如果在 App 處于前臺時調(diào)用,將獲得 30 秒。如果在后臺調(diào)用,將獲得 10 秒。
系統(tǒng)將異步嘗試執(zhí)行我們提供給 using 參數(shù)的代碼塊,它將返回一個布爾值,讓我們知道 App 是否即將暫停。如果我們收到 false 值,那么我們可以繼續(xù),并盡快執(zhí)行我們的操作。如果我們收到一個 true 值,系統(tǒng)不會給我們額外的時間,App 需要立即停止。
請注意,僅僅是系統(tǒng)允許我們開始額外的工作,并不意味著它會給我們足夠的時間來完成它。如果我們的代碼塊仍在運行,并且操作系統(tǒng)需要暫停我們的 App ,那么我們的代碼塊將被使用 true 參數(shù)再次調(diào)用。我們的代碼應(yīng)能夠處理此取消請求。
例如,我們可以在每個操作之前檢查 watchOS 是否告訴我們停止工作。假設(shè)我們有一個布爾實例屬性 cancel,我們會執(zhí)行以下操作:
processInfo.performExpiringActivity( withReason: "求求你" ) { suspending in guard !suspending else { cancel = true return } guard !cancel else { return } try? managedObjectContext.save() guard !cancel else { return } userDefaults.set(someData(), forKey: "criticalData") }
在代碼中:
立即檢查我們是否被允許運行 如果系統(tǒng)告訴你暫停,那么我們將取消屬性設(shè)置為 true。
在嘗試保存我們的 CoreDataModel 之前,請確保未設(shè)置 cancel。另一個線程可能已經(jīng)調(diào)用了相同的方法并請求掛起。
在保存到 UserDefaults 之前,請檢查操作系統(tǒng)是否告訴我們停止。
每次檢查取消可能看起來有點奇怪,但這樣做可以確保我們遵守操作系統(tǒng)的指示。 在示例中,我們只需在被告知時停止操作,而時間情況下,我們可能需要快速執(zhí)行其他操作來標記我們無法完成的操作。
App 到 Active 狀態(tài)
如果用戶在 App 處于 Background 狀態(tài)時與其交互,watchOS 將通過以下過程將其轉(zhuǎn)換回 Active 狀態(tài):
- 以 .background 狀態(tài)重新啟動應(yīng)用程序;
- 調(diào)用 applicationWillEnterForeground();
- 將 scenePhase 設(shè)置為 .active;
- 調(diào)用 applicationDidBecomeActive()。
我們可能會對用戶在后臺狀態(tài)下如何與應(yīng)用交互感到困惑。這是用戶使用了 App 提供的復雜功能。
App 到 Suspended 狀態(tài)
當我們的 App 最終轉(zhuǎn)換到 Suspended 狀態(tài)時,所有代碼執(zhí)行都會停止。 App 仍在內(nèi)存中,但不會處理事件。
當我們的 App 處于 Background 狀態(tài)并且沒有任何待處理的任務(wù)要完成時,系統(tǒng)會將我們的 App 轉(zhuǎn)換為 Suspended 狀態(tài)。
一旦我們的 App 進入 Suspended 狀態(tài),它就有資格被清除。 如果操作系統(tǒng)需要更多內(nèi)存,它可能會在不通知的情況下從內(nèi)存中清除任何處于 Suspended 狀態(tài)的 App。
系統(tǒng)將盡最大努力不清除最近執(zhí)行的 App、Dock 中的任何 App 以及在當前表盤上有復雜功能的任何 App。 如果系統(tǒng)必須清除上述 App 之一,它將在內(nèi)存可用時重新啟動該 App。
始終顯示 Always on
在 watchOS 6 之前,當用戶最近沒有與之交互時,Apple Watch 會息屏。 Always On 改變了這一點,使手表繼續(xù)顯示時間。但是,watchOS 會模糊當前運行的 App,并在顯示屏上顯示時間。
而現(xiàn)在,在默認情況下,會顯示我們的 App 的用戶界面,而不是時間。只要它是最前面的 App 或運行 Background session,watchOS 就不會模糊它。處于 Always On 時,手表屏幕會變暗,并且 UI 更新速度會變慢,從而延長電池使用時長。
如果用戶與我們的 App 交互,系統(tǒng)將返回其 Active 狀態(tài)。 Always On 的一個顯著優(yōu)勢與日期和時間有關(guān)。如果 App 顯示計時器或相對日期等,則 UI 將繼續(xù)更新為正確的值。
如果你希望為我們的 App 禁用 Always On,只需在 Info.plist 中將 WKSupportsAlwaysOnDisplay 鍵設(shè)置為 false。用戶還可以通過“設(shè)置”?“顯示和亮度”?“始終顯示”來為某些 App 或整個設(shè)備禁用 Always on。
狀態(tài)變化示例
創(chuàng)建一個新項目:
新增 ExtensionDelegate.swift 文件:
import WatchKit final class ExtensionDelegate: NSObject, WKExtensionDelegate { func applicationDidFinishLaunching() { print( #function) } func applicationWillEnterForeground() { print( #function) } func applicationDidBecomeActive() { print( #function) } func applicationWillResignActive() { print( #function) } func applicationDidEnterBackground() { print( #function) } }
修改 LifecycleApp.swift 代碼:
import SwiftUI @main struct Lifecycle_Watch_AppApp: App { @Environment(.scenePhase) private var scenePhase @WKExtensionDelegateAdaptor(ExtensionDelegate.self) private var extensionDelegate var body: some Scene { WindowGroup { ContentView() } .onChange(of: scenePhase) { print("onChange: ($0)") } } }
我們可以在物理設(shè)備上運行該項目以觀察狀態(tài)變化。當我們抬起和放下手腕時,會看到狀態(tài)在 Active 和 Inactive 之間變化。如果我們讓應(yīng)用程序處于 Inactive 狀態(tài)兩分鐘,它會切換到 Background 模式。
WKExtendedRuntimeSession
有四種特定的類型,可以讓我們的 App 保持運行,甚至在后臺運行。
Self care
專注于用戶情緒健康或健康的 App 將在前臺運行,即使在手表屏幕未打開。 watchOS 將為 App 序提供 10 分鐘的 Session,該 Session 將持續(xù)到用戶切換到另一個 App 或 App 使 Session 無效。
Mindfulness
冥想已越來越成為一種流行的,Mindfulness - 正念 App 將保持在前臺。不過,這是一個耗時的過程,所以 watchOS 會給 App 一個 1 小時的 Session。
Physical therapy
伸展等鍛煉非常適合物理治療課程。 與最后兩種 Session 類型不同,物理治療 Session 在后臺運行。 后臺 Session 將一直運行,直到到時間限制或 App 使 Session 無效,即使用戶啟動另一個 App 也是如此。物理治療課程可長達 1 小時。
Smart alarm
當我們需要安排時間檢查用戶的心率和運動時,智能提醒是一個不錯的選擇。 App 將獲得一個 30 分鐘的 Sesion。
與其他三種會話類型不同,我們必須安排智提醒鐘在未來某一個時刻開始。 我們需要在接下來的 36 小時內(nèi)啟動會話,并在我們的 App 處于 WKApplicationState.active 狀態(tài)時安排它。我們的Aoo 能會暫?;蚪K止,但 Sesion 將繼續(xù)。
當需要處理 Session 時,watchOS 將調(diào)用 App 的 WKExtensionDelegate 的 handle(_:)。
注意:我們必須在 App 退出之前設(shè)置會話的委托,否則 Session 將終止。
一旦 Session 運行,我們必須通過調(diào)用會話的 notifyUser(hapticType:repeatHandler:) 來觸發(fā)提醒。 如果我們忘記了,watchOS 將顯示警告并提議禁用 Session。
刷牙提醒 Demo - Dentisit
搭建項目框架
我們將實現(xiàn)一個刷牙時,提醒用戶刷牙時間的 App Dentisit,首先創(chuàng)建項目:
修改 ContentView.swift,這樣能讓我們更好的看到 App 的狀態(tài):
struct ContentView: View { @Environment(.scenePhase) private var scenePhase var body: some View { Text("Hello, World!") .onChange(of: scenePhase) { print($0) } } }
新增 GetReadyView.swift,它將實現(xiàn)一個準備視圖:
import SwiftUI struct GetReadyView: View { private let color: Color // 色環(huán)顏色 @State private var stage: Int // 倒計時秒數(shù),屏幕上展示的值 private let onComplete: (() -> Void)? // 倒計時完成后回調(diào) private let denominator: Double // 倒計時秒數(shù),保存總值,用來計算色環(huán) @State private var trim = 1.0 // 色環(huán)顯示比例 @State private var text = "Ready" // 色環(huán)中心文案 private let timer = Timer.publish(every: 1, on: .main, in: .common).autoconnect() init(color: Color = .green, stages: Int = 4, onComplete: (() -> Void)? = nil) { self.color = color self.onComplete = onComplete _stage = State(initialValue: stages) denominator = Double(stages) } var body: some View { ZStack { Color.black.ignoresSafeArea() // 背景色環(huán) Circle() .stroke(style: StrokeStyle(lineWidth: 10, lineCap: .round)) .foregroundColor(color.opacity(0.5)) // 色環(huán) Circle() .trim(from: 0, to: trim) .stroke(style: StrokeStyle(lineWidth: 10, lineCap: .round)) .foregroundColor(color) .rotationEffect(.degrees(-90)) .animation(.linear, value: trim) // 文案 Text(text) .font(.title) } .onReceive(timer) { _ in tick() } .background(.black) } private func tick() { stage -= 1 // 更新當前時間 self.text = "(self.stage)" trim = Double(stage) / denominator // 更新色環(huán) guard stage > 0 else { timer.upstream.connect().cancel() WKInterfaceDevice.current().play(.success) // 播放音效 if let onComplete = onComplete { onComplete() } return } WKInterfaceDevice.current().play(.start) // 播放音效 } } struct GetReadyView_Previews: PreviewProvider { static var previews: some View { GetReadyView() } }
具體代碼內(nèi)容,已經(jīng)添加注釋以輔助閱讀,可以在 ContentView 中添加 GetReadyView() 來查看效果:
struct ContentView: View { @Environment(.scenePhase) private var scenePhase var body: some View { // Text("Hello, World!") GetReadyView() .onChange(of: scenePhase) { print($0) } } }
最終效果如下:
選擇 Session 類型
刷牙屬于 Self care 類型,按照下圖中的步驟添加新功能。 首先,從 Project Navigator 菜單中選擇 Dentisit。 然后選擇 Dentisit Watch App,選擇 Signing & Capabilities,然后按 + Capability 選項。 出現(xiàn)提示時,從功能列表中選擇 Background Modes,并修改 Session Type:
添加 ContentModel
創(chuàng)建一個名為 ContentModel.swift 的新文件:
import SwiftUI final class ContentModel: NSObject, ObservableObject { @Published var roundsLeft = 0 @Published var endOfRound: Date? @Published var endOfBrushing: Date? private var timer: Timer! private var session: WKExtendedRuntimeSession! }
在代碼中,ContentModel 需要符合 ObservableObject 以便模型可以更新 ContentView。我們還需要繼承 NSObject,這是 WKExtendedRuntimeSessionDelegate 的要求。
前三個屬性用 @Published 包裝,我們將使用它們來跟蹤用戶還需要刷幾輪、刷多久。
最后,我們需要一種方法來知道時間到了,并控制會話。
用戶開始刷牙后,我們需要創(chuàng)建會話并更新表盤按鈕上顯示的文本。將此添加到 ContentModel:
func startBrushing() { session = WKExtendedRuntimeSession() session.delegate = self session.start() }
將以下代碼添加到文件末尾實現(xiàn) WKExtendedRuntimeSessionDelegate:
extension ContentModel: WKExtendedRuntimeSessionDelegate { func extendedRuntimeSessionDidStart( _ extendedRuntimeSession: WKExtendedRuntimeSession ) { } func extendedRuntimeSessionWillExpire( _ extendedRuntimeSession: WKExtendedRuntimeSession ) { } func extendedRuntimeSession( _ extendedRuntimeSession: WKExtendedRuntimeSession, didInvalidateWith reason: WKExtendedRuntimeSessionInvalidationReason, error: Error? ) { } }
遵守協(xié)議非常簡單:
- 一旦 Session 開始運行,系統(tǒng)就會調(diào)用 extendedRuntimeSessionDidStart(_:)。
- 如果 App 即將超過 Session的時間限制,watchOS 將在強制使會話過期之前調(diào)用 extendedRuntimeSessionWillExpire(_:)。
- 無論出于何種原因,當會話完成時,watchOS 都會調(diào)用 extendedRuntimeSession(_:didInvalidateWith:error:)。
繼續(xù)在 在extendedRuntimeSessionDidStart(_:) 中添加:
let secondsPerRound = 30.0 let now = Date.now roundsLeft = 4 endOfRound = now.addingTimeInterval(secondsPerRound) endOfBrushing = now.addingTimeInterval(secondsPerRound * 4) let device = WKInterfaceDevice.current() device.play(.start)
我們不關(guān)心實際的日期或時間:我們只需要特定的秒數(shù)。當 Session 開始時,讓手表快速振動是很好的用戶體驗。
現(xiàn)在我們知道每輪刷牙需要多長時間,繼續(xù)設(shè)置一個計時器。 添加以下代碼以完成該方法:
timer = Timer( fire: endOfRound!, interval: secondsPerRound, repeats: true ) { _ in self.roundsLeft -= 1 guard self.roundsLeft == 0 else { self.endOfRound = Date.now.addingTimeInterval(secondsPerRound) device.play(.success) return } extendedRuntimeSession.invalidate() device.play(.success) device.play(.success) } RunLoop.main.add(timer, forMode: .common)
我們生成一個計時器,該計時器在當前刷牙 Round 結(jié)束時開始,并每隔 secondsPerRound 秒重復一次。如果仍有幾輪要執(zhí)行,則更新一輪結(jié)束的時間,以便更新視圖的顯示。 讓手表振動讓用戶知道是時候切換到他們嘴巴的新部分了。如果最后一輪完成,我們可以進行兩次振動提醒用戶。最后,將計時器安排到 run loop 中。
extendedRuntimeSession(_:didInvalidateWith:error:) 是禁用計時器的地方:
func extendedRuntimeSession( _ extendedRuntimeSession: WKExtendedRuntimeSession, didInvalidateWith reason: WKExtendedRuntimeSessionInvalidationReason, error: Error? ) { timer.invalidate() timer = nil endOfRound = nil endOfBrushing = nil roundsLeft = 0 }
更新 UI
修改 ContentView:
import SwiftUI struct ContentView: View { @Environment(.scenePhase) private var scenePhase @ObservedObject private var model = ContentModel() @State var showGettingReady = false var body: some View { ZStack { VStack { Button { showGettingReady = true } label: { Text("Start brushing") } .disabled(model.roundsLeft != 0) .padding() if let endOfBrushing = model.endOfBrushing, let endOfRound = model.endOfRound { Text("Rounds Left: (model.roundsLeft - 1)") Text("Total time left: (endOfBrushing, style: .timer)") Text("This round time left: (endOfRound, style: .timer)") } } if showGettingReady { GetReadyView { showGettingReady = false model.startBrushing() } } else { EmptyView() } } } } struct ContentView_Previews: PreviewProvider { static var previews: some View { ContentView() } }
添加開始按鈕、時間展示,并運行項目,我們的 Dentisit 就開始工作了:
附件
你可以在這里獲得文章項目:github.com/LLLLLayer/A…
以上就是Apple Watch App Lifecycle應(yīng)用開發(fā)的詳細內(nèi)容,更多關(guān)于Apple Watch App Lifecycle的資料請關(guān)注腳本之家其它相關(guān)文章!
相關(guān)文章
iOS中的應(yīng)用啟動原理以及嵌套模型開發(fā)示例詳解
這篇文章主要介紹了iOS中的應(yīng)用啟動原理以及嵌套模型開發(fā)示例詳解,代碼基于傳統(tǒng)的Objective-C,需要的朋友可以參考下2015-12-12iOS 檢測網(wǎng)絡(luò)狀態(tài)的兩種方法
一般有Reachability和AFNetworking監(jiān)測兩種方式,都是第三方的框架,下文逐一詳細給大家講解,感興趣的朋友一起看看吧2016-10-10詳解IOS開發(fā)之實現(xiàn)App消息推送(最新)
這篇文章主要介紹了詳解IOS開發(fā)之實現(xiàn)App消息推送(最新),具有一定的參考價值,有興趣的可以了解一下。2016-12-12禁止iPhone Safari video標簽視頻自動全屏的辦法
本篇文章給大家分析有沒有辦法禁止iPhone Safari video標簽視頻自動全屏,以下給出好多種情況分享,感興趣的朋友可以參考下2015-09-09解決iOS11圖片下拉放大出現(xiàn)信號欄白條的bug問題
這篇文章主要介紹了iOS11圖片下拉放大出現(xiàn)信號欄白條的bug問題,需要的朋友參考下吧2017-09-09