Watch App Lifecycle

watchOS App 的生命周期比 iOS App 的生命周期要更杂乱一些。watchOS App 或许会处于以下五种状况:

  • Not running – 未运转
  • Inactive – 不活跃
  • Active – 活跃
  • Background – 后台
  • Suspended – 挂起

常见的状况转换

watchOS App 的五种状况由下图中的紫色框表明:

「Apple Watch 应用开发系列」Apple Watch App Lifecycle

作为开发者,咱们只会与其中三个状况进行交互:Inactive、Active、Background。 而在 Not running 和 Suspended 状况下 App 未运转。

发动 App 到 Active 状况

假如用户没有运转 App 或体系从内存中铲除了 App,则 App 以 Not running 状况开端。App 发动后,它会从 Not running 转换到 Inactive 状况。

处于 Inactive 状况时,App 仍然在前台运转,但不会呼应用户的任何操作。但是 App 或许仍在履行某些代码。在此状况下,App 并不是无法运转。

App 几乎立行将转换到 Active 状况,这是在 Apple Watch 屏幕上运转的 App 的正常形式。当处于 Active 状况时,App 能够接收来自 Apple Watch 上的物理控件和用户手势的操作。

当用户发动咱们的 App 但没有运转时,watchOS 将履行以下操作:

  • 调用 applicationDidFinishLaunching() 并将 scenePhase 设置为 .inactive;
  • 创立 App 的初始 scene 和 rootView;
  • 调用 applicationWillEnterForeground();
  • 调用 applicationDidBecomeActive() 并将 scenePhase 设置为 .active;
  • App 将出现在屏幕上,watchOS 将调用 rootView 的 onAppear(perform:)。

留意:从 watchOS 7 及更高版别开端,在运用 SwiftUI 时,scenePhase 环境变量的更新,要早于 WKExtensionDelegate 办法的调用。

App 到 Inactive 状况

一旦用户放下手臂,watchOS 将变为 Inactive 状况。如前所述,该 App 仍在运转并履行咱们的代码。Inactive 状况需求削减 App 对 Apple Watch 电池的影响,咱们应暂停或撤销任何不需求的电池密集型操作。例如,咱们能够制止当时正在运转的动画的展现。

咱们还需求考虑是否需求保存一些数据,保存 Core Data Stack?向 UserDefaults 写入内容?其实,一旦用户再次抬起手臂,App 就会再次激活。因此,此时并不需求保存或保存太多内容,否则或许会对电量产生较大压力。

当咱们的 App 转换到 Inactive 状况时,watchOS 会将 scenePhase 设置为 .inactive,然后调用 WKExtensionDelegate 的 applicationWillResignActive() 办法。

App 到 Background 状况

在转换到 Inactive 状况两分钟后,或许当用户切换到另一个 App 时,咱们的 App 将转换到 Background 状况。经过体系也能够直接将 App 发动到 Background 状况,如后 Background session 和 Background task。

在 Suspended 状况之前,操作体系会为 Background 状况的 App 供给一小段不确定的时刻。假如咱们的 App 从 Inactive 状况转换到 Background 状况,咱们需求快速履行必要的操作来处理 App。

咱们能够运用 SwiftUI ScenePhase 或 WKExtensionDelegate 的 applicationDidEnterBackground 来确定咱们的 App 何时到 Background 状况。当从 Inactive 状况转换到 Background 状况时,watchOS 会将scenePhase 设置为 .background,然后调用applicationDidEnterBackground()。假如 App 需求太多资源,watchOS 将暂停该 App。

回来表盘

在 watchOS 7 之前,咱们能够在 App 转换到 Background 后恳求 8 分钟。在 watchOS 7 及后续版别中,用户能够经过 Apple Watch 上的 设置 ▸ 通用 ▸ 回来表盘 来进行超时配置。用户能够挑选三个选项:“一向”、“2 分钟后”或“1 小时后”。默认情况下,所选设置会应用到一切 App,但用户也能够为每个 App 选取自定时刻。默认值为两分钟。 根据功用需求,咱们或许希望告知用户如何将 App 的设置更改为一小时。

额外的 Background 履行时刻

假如在转换到 Background 时,App 需求履行的工作量比 watchOS 为咱们的App 供给时刻要长,那么咱们需求重新考虑 App 进行的操作,例如删去网络调用。假如咱们现已进行了一切能够进行的优化,但仍然需求更多的处理时刻,咱们能够调用 ProcessInfo 类的 performExpiringActivity(withReason:using:) 办法。假如在 App 处于前台时调用,将取得 30 秒。假如在后台调用,将取得 10 秒。

体系将异步测验履行咱们供给给 using 参数的代码块,它将回来一个布尔值,让咱们知道 App 是否行将暂停。假如咱们收到 false 值,那么咱们能够持续,并尽快履行咱们的操作。假如咱们收到一个 true 值,体系不会给咱们额外的时刻,App 需求立即中止。

请留意,仅仅是体系答应咱们开端额外的工作,并不意味着它会给咱们足够的时刻来完结它。假如咱们的代码块仍在运转,而且操作体系需求暂停咱们的 App ,那么咱们的代码块将被运用 true 参数再次调用。咱们的代码应能够处理此撤销恳求。

例如,咱们能够在每个操作之前查看 watchOS 是否告知咱们中止工作。假定咱们有一个布尔实例特点 cancel,咱们会履行以下操作:

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")
}

在代码中:

  1. 立即查看咱们是否被答应运转 假如体系告知你暂停,那么咱们将撤销特点设置为 true。

  2. 在测验保存咱们的 CoreDataModel 之前,请确保未设置 cancel。另一个线程或许现已调用了相同的办法并恳求挂起。

  3. 在保存到 UserDefaults 之前,请查看操作体系是否告知咱们中止。

每次查看撤销或许看起来有点古怪,但这样做能够确保咱们恪守操作体系的指示。 在示例中,咱们只需在被告知时中止操作,而时刻情况下,咱们或许需求快速履行其他操作来标记咱们无法完结的操作。

App 到 Active 状况

假如用户在 App 处于 Background 状况时与其交互,watchOS 将经过以下过程将其转换回 Active 状况:

  • 以 .background 状况重新发动应用程序;
  • 调用 applicationWillEnterForeground();
  • 将 scenePhase 设置为 .active;
  • 调用 applicationDidBecomeActive()。

咱们或许会对用户在后台状况下如何与应用交互感到困惑。这是用户运用了 App 供给的杂乱功用。

App 到 Suspended 状况

当咱们的 App 终究转换到 Suspended 状况时,一切代码履行都会中止。 App 仍在内存中,但不会处理事情。

当咱们的 App 处于 Background 状况而且没有任何待处理的任务要完结时,体系会将咱们的 App 转换为 Suspended 状况。

一旦咱们的 App 进入 Suspended 状况,它就有资历被铲除。 假如操作体系需求更多内存,它或许会在不通知的情况下从内存中铲除任何处于 Suspended 状况的 App。

体系将尽最大尽力不铲除最近履行的 App、Dock 中的任何 App 以及在当时表盘上有杂乱功用的任何 App。 假如体系有必要铲除上述 App 之一,它将在内存可用时重新发动该 App。

一向显现 Always on

在 watchOS 6 之前,当用户最近没有与之交互时,Apple Watch 会息屏。 Always On 改变了这一点,使手表持续显现时刻。但是,watchOS 会含糊当时运转的 App,并在显现屏上显现时刻。

而现在,在默认情况下,会显现咱们的 App 的用户界面,而不是时刻。只需它是最前面的 App 或运转 Background session,watchOS 就不会含糊它。处于 Always On 时,手表屏幕会变暗,而且 UI 更新速度会变慢,然后延长电池运用时长。

假如用户与咱们的 App 交互,体系将回来其 Active 状况。 Always On 的一个明显优势与日期和时刻有关。假如 App 显现计时器或相对日期等,则 UI 将持续更新为正确的值。

假如你希望为咱们的 App 禁用 Always On,只需在 Info.plist 中将 WKSupportsAlwaysOnDisplay 键设置为 false。用户还能够经过“设置”▸“显现和亮度”▸“一向显现”来为某些 App 或整个设备禁用 Always on。

状况变化示例

创立一个新项目:

「Apple Watch 应用开发系列」Apple Watch App Lifecycle
「Apple Watch 应用开发系列」Apple Watch App Lifecycle

新增 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)")
        }
    }
}

咱们能够在物理设备上运转该项目以调查状况变化。当咱们抬起和放下手腕时,会看到状况在 Active 和 Inactive 之间变化。假如咱们让应用程序处于 Inactive 状况两分钟,它会切换到 Background 形式。

「Apple Watch 应用开发系列」Apple Watch App Lifecycle

WKExtendedRuntimeSession

有四种特定的类型,能够让咱们的 App 坚持运转,甚至在后台运转。

Self care

专注于用户情绪健康或健康的 App 将在前台运转,即便在手表屏幕未打开。 watchOS 将为 App 序供给 10 分钟的 Session,该 Session 将持续到用户切换到另一个 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 小时内发动会话,并在咱们的 App 处于 WKApplicationState.active 状况时组织它。咱们的Aoo 能会暂停或停止,但 Sesion 将持续。

当需求处理 Session 时,watchOS 将调用 App 的 WKExtensionDelegate 的 handle(_:)。

留意:咱们有必要在 App 退出之前设置会话的托付,否则 Session 将停止。

一旦 Session 运转,咱们有必要经过调用会话的 notifyUser(hapticType:repeatHandler:) 来触发提示。 假如咱们忘记了,watchOS 将显现正告并提议禁用 Session。

刷牙提示 Demo – Dentisit

建立项目框架

咱们将完结一个刷牙时,提示用户刷牙时刻的 App Dentisit,首先创立项目:

「Apple Watch 应用开发系列」Apple Watch App Lifecycle
「Apple Watch 应用开发系列」Apple Watch App Lifecycle

修正 ContentView.swift,这样能让咱们更好的看到 App 的状况:

struct ContentView: View {
    @Environment(.scenePhase) private var scenePhase
    var body: some View {
        Text("Hello, World!")
        .onChange(of: scenePhase) { print($0) }
    }
}

新增 GetReadyView.swift,它将完结一个预备视图:

import SwiftUI
struct GetReadyView: View {
 private let color: Color // 色环颜色
 @State private var stage: Int // 倒计时秒数,屏幕上展现的值
 private let onComplete: (() -> Void)? // 倒计时完结后回调
 private let denominator: Double // 倒计时秒数,保存总值,用来计算色环
 @State private var trim = 1.0 // 色环显现比例
 @State private var text = "Ready" // 色环中心案牍
 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()
   // 布景色环
   Circle()
    .stroke(style: StrokeStyle(lineWidth: 10, lineCap: .round))
    .foregroundColor(color.opacity(0.5))
   // 色环
   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 // 更新色环
  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()
 }
}

具体代码内容,现已增加注释以辅佐阅读,能够在 ContentView 中增加 GetReadyView() 来查看效果:

struct ContentView: View {
    @Environment(.scenePhase) private var scenePhase
    var body: some View {
//        Text("Hello, World!")
        GetReadyView()
        .onChange(of: scenePhase) { print($0) }
    }
}

终究效果如下:

「Apple Watch 应用开发系列」Apple Watch App Lifecycle

挑选 Session 类型

刷牙属于 Self care 类型,按照下图中的步骤增加新功用。 首先,从 Project Navigator 菜单中挑选 Dentisit。 然后挑选 Dentisit Watch App,挑选 Signing & Capabilities,然后按 + Capability 选项。 出现提示时,从功用列表中挑选 Background Modes,并修正 Session Type:

「Apple Watch 应用开发系列」Apple Watch App Lifecycle

「Apple Watch 应用开发系列」Apple Watch App Lifecycle

增加 ContentModel

创立一个名为 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 包装,咱们将运用它们来盯梢用户还需求刷几轮、刷多久。

最终,咱们需求一种办法来知道时刻到了,并操控会话。

用户开端刷牙后,咱们需求创立会话并更新表盘按钮上显现的文本。将此增加到 ContentModel:

func startBrushing() {
    session = WKExtendedRuntimeSession()
    session.delegate = self
    session.start()
}

将以下代码增加到文件末尾完结 WKExtendedRuntimeSessionDelegate:

extension ContentModel: WKExtendedRuntimeSessionDelegate {
    func extendedRuntimeSessionDidStart(
        _ extendedRuntimeSession: WKExtendedRuntimeSession
    ) {
    }
    func extendedRuntimeSessionWillExpire(
        _ extendedRuntimeSession: WKExtendedRuntimeSession
    ) {
    }
    func extendedRuntimeSession(
        _ extendedRuntimeSession: WKExtendedRuntimeSession,
        didInvalidateWith reason: WKExtendedRuntimeSessionInvalidationReason,
        error: Error?
    ) {
    }
}

恪守协议十分简略:

  1. 一旦 Session 开端运转,体系就会调用 extendedRuntimeSessionDidStart(_:)。
  1. 假如 App 行将超越 Session的时刻约束,watchOS 将在强制使会话过期之前调用 extendedRuntimeSessionWillExpire(_:)。
  1. 不管出于何种原因,当会话完结时,watchOS 都会调用 extendedRuntimeSession(_:didInvalidateWith:error:)。

持续在 在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)

咱们不关心实际的日期或时刻:咱们只需求特定的秒数。当 Session 开端时,让手表快速振荡是很好的用户体会。

现在咱们知道每轮刷牙需求多长时刻,持续设置一个计时器。 增加以下代码以完结该办法:

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 结束时开端,并每隔 secondsPerRound 秒重复一次。假如仍有几轮要履行,则更新一轮结束的时刻,以便更新视图的显现。 让手表振荡让用户知道是时分切换到他们嘴巴的新部分了。假如最终一轮完结,咱们能够进行两次振荡提示用户。最终,将计时器组织到 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 就开端工作了:

「Apple Watch 应用开发系列」Apple Watch App Lifecycle

附件

  • 你能够在这里取得文章项目:github.com/LLLLLayer/A…