宋琼 發表於 2022-10-31 10:33:45

Apple Watch App Lifecycle应用开发

<div id="navCategory"><h5 class="catalogue">目录</h5><ul class="first_class_ul"><li>Watch App Lifecycle</li><li>常见的状态转换</li><ul class="second_class_ul"><li>启动 App 到 Active 状态</li><li>App 到 Inactive 状态</li><li>App 到 Background 状态</li><ul class="third_class_ul"><li>返回表盘</li><li>额外的 Background 执行时间</li></ul><li>App 到 Active 状态</li><ul class="third_class_ul"></ul><li>App 到 Suspended 状态</li><ul class="third_class_ul"></ul><li>始终显示 Always on</li><ul class="third_class_ul"></ul></ul><li>状态变化示例</li><ul class="second_class_ul"></ul><li>WKExtendedRuntimeSession</li><ul class="second_class_ul"><li>Self care</li><ul class="third_class_ul"></ul><li>Mindfulness</li><ul class="third_class_ul"></ul><li>Physical therapy</li><ul class="third_class_ul"></ul><li>Smart alarm</li><ul class="third_class_ul"></ul></ul><li>刷牙提醒 Demo - Dentisit</li><ul class="second_class_ul"><li>搭建项目框架</li><ul class="third_class_ul"></ul><li>选择 Session 类型</li><ul class="third_class_ul"></ul><li>添加 ContentModel</li><ul class="third_class_ul"></ul><li>更新 UI</li><ul class="third_class_ul"></ul></ul></ul></div><p class="maodian"></p><h2>Watch App Lifecycle</h2>
<p>watchOS App 的生命周期比 iOS App 的生命周期要更复杂一些。watchOS App 可能会处于以下五种状态:</p>
<ul><li>Not running - 未运行</li><li>Inactive - 不活跃</li><li>Active - 活跃</li><li>Background - 后台</li><li>Suspended - 挂起</li></ul>
<p class="maodian"></p><h2>常见的状态转换</h2>
<p>watchOS App 的五种状态由下图中的紫色框表示:</p>
<p style="text-align:center"><img alt="" src="https://img.jbzj.com/file_images/article/202210/20221031084343015.png" /></p>
<p>作为开发者,我们只会与其中三个状态进行交互:Inactive、Active、Background。 而在 Not running 和 Suspended 状态下 App 未运行。</p>
<p class="maodian"></p><h3>启动 App 到 Active 状态</h3>
<p>如果用户尚未运行 App 或系统从内存中清除了 App,则 App 以 Not running 状态开始。App 启动后,它会从 Not running 转换到 Inactive 状态。</p>
<p>处于 Inactive 状态时,App 仍然在前台运行,但不会响应用户的任何操作。但是 App 可能仍在执行某些代码。在此状态下,App 并不是无法运行。</p>
<p>App 几乎立即将转换到 Active 状态,这是在 Apple Watch 屏幕上运行的 App 的正常模式。当处于 Active 状态时,App 可以接收来自 Apple Watch 上的物理控件和用户手势的操作。</p>
<p>当用户启动我们的 App 但尚未运行时,watchOS 将执行以下操作:</p>
<ul><li>调用 applicationDidFinishLaunching() 并将 scenePhase 设置为 .inactive;</li><li>创建 App 的初始 scene 和 rootView;</li><li>调用 applicationWillEnterForeground();</li><li>调用 applicationDidBecomeActive() 并将 scenePhase 设置为 .active;</li><li>App 将出现在屏幕上,watchOS 将调用 rootView 的 onAppear(perform:)。</li></ul>
<blockquote><p>注意:从 watchOS 7 及更高版本开始,在使用 SwiftUI 时,scenePhase 环境变量的更新,要早于 WKExtensionDelegate 方法的调用。</p></blockquote>
<p class="maodian"></p><h3>App 到 Inactive 状态</h3>
<p>一旦用户放下手臂,watchOS 将变为 Inactive 状态。如前所述,该 App 仍在运行并执行我们的代码。Inactive 状态需要减少 App 对 Apple Watch 电池的影响,我们应暂停或取消任何不需要的电池密集型操作。例如,我们可以禁止当前正在运行的动画的展示。</p>
<p>我们还需要考虑是否需要保存一些数据,保存 Core Data Stack?向 UserDefaults 写入内容?其实,一旦用户再次抬起手臂,App 就会再次激活。因此,此时并不需要保存或保存太多内容,否则可能会对电量产生较大压力。</p>
<p>当我们的 App 转换到 Inactive 状态时,watchOS 会将 scenePhase 设置为 .inactive,然后调用 WKExtensionDelegate 的 applicationWillResignActive() 方法。</p>
<p class="maodian"></p><h3>App 到 Background 状态</h3>
<p>在转换到 Inactive 状态两分钟后,或者当用户切换到另一个 App 时,我们的 App 将转换到 Background 状态。通过系统也可以直接将 App 启动到 Background 状态,如后 Background session 和 Background task。</p>
<p>在 Suspended 状态之前,操作系统会为 Background 状态的 App 提供一小段不确定的时间。如果我们的 App 从 Inactive 状态转换到 Background 状态,我们需要快速执行必要的操作来处理 App。</p>
<p>我们可以使用 SwiftUI ScenePhase 或 WKExtensionDelegate 的 applicationDidEnterBackground 来确定我们的 App 何时到 Background 状态。当从 Inactive 状态转换到 Background 状态时,watchOS 会将scenePhase 设置为 .background,然后调用applicationDidEnterBackground()。如果 App 需要太多资源,watchOS 将暂停该 App。</p>
<p class="maodian"></p><h4>返回表盘</h4>
<p>在 watchOS 7 之前,我们可以在 App 转换到 Background 后请求 8 分钟。在 watchOS 7 及后续版本中,用户可以通过 Apple Watch 上的 设置 ▸ 通用 ▸ 返回表盘 来进行超时配置。用户可以选择三个选项:&ldquo;始终&rdquo;、&ldquo;2 分钟后&rdquo;或&ldquo;1 小时后&rdquo;。默认情况下,所选设置会应用到所有 App,但用户也可以为每个 App 选取自定时间。默认值为两分钟。 根据功能需要,我们可能希望告诉用户如何将 App 的设置更改为一小时。</p>
<p class="maodian"></p><h4>额外的 Background 执行时间</h4>
<p>如果在转换到 Background 时,App 需要执行的工作量比 watchOS 为我们的App 提供时间要长,那么我们需要重新考虑 App 进行的操作,例如删除网络调用。如果我们已经进行了所有可以进行的优化,但仍然需要更多的处理时间,我们可以调用 ProcessInfo 类的 performExpiringActivity(withReason:using:) 方法。如果在 App 处于前台时调用,将获得 30 秒。如果在后台调用,将获得 10 秒。</p>
<p>系统将异步尝试执行我们提供给 using 参数的代码块,它将返回一个布尔值,让我们知道 App 是否即将暂停。如果我们收到 false 值,那么我们可以继续,并尽快执行我们的操作。如果我们收到一个 true 值,系统不会给我们额外的时间,App 需要立即停止。</p>
<p>请注意,仅仅是系统允许我们开始额外的工作,并不意味着它会给我们足够的时间来完成它。如果我们的代码块仍在运行,并且操作系统需要暂停我们的 App ,那么我们的代码块将被使用 true 参数再次调用。我们的代码应能够处理此取消请求。</p>
<p>例如,我们可以在每个操作之前检查 watchOS 是否告诉我们停止工作。假设我们有一个布尔实例属性 cancel,我们会执行以下操作:</p>
<div class="jb51code"><pre class="brush:cpp;">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")
}
</pre></div>
<p>在代码中:</p>
<ul><li><p>立即检查我们是否被允许运行 如果系统告诉你暂停,那么我们将取消属性设置为 true。</p></li><li><p>在尝试保存我们的 CoreDataModel 之前,请确保未设置 cancel。另一个线程可能已经调用了相同的方法并请求挂起。</p></li><li><p>在保存到 UserDefaults 之前,请检查操作系统是否告诉我们停止。</p></li></ul>
<p>每次检查取消可能看起来有点奇怪,但这样做可以确保我们遵守操作系统的指示。 在示例中,我们只需在被告知时停止操作,而时间情况下,我们可能需要快速执行其他操作来标记我们无法完成的操作。</p>
<p class="maodian"></p><h3>App 到 Active 状态</h3>
<p>如果用户在 App 处于 Background 状态时与其交互,watchOS 将通过以下过程将其转换回 Active 状态:</p>
<ul><li>以 .background 状态重新启动应用程序;</li><li>调用 applicationWillEnterForeground();</li><li>将 scenePhase 设置为 .active;</li><li>调用 applicationDidBecomeActive()。</li></ul>
<p>我们可能会对用户在后台状态下如何与应用交互感到困惑。这是用户使用了 App 提供的复杂功能。</p>
<p class="maodian"></p><h3>App 到 Suspended 状态</h3>
<p>当我们的 App 最终转换到 Suspended 状态时,所有代码执行都会停止。 App 仍在内存中,但不会处理事件。</p>
<p>当我们的 App 处于 Background 状态并且没有任何待处理的任务要完成时,系统会将我们的 App 转换为 Suspended 状态。</p>
<p>一旦我们的 App 进入 Suspended 状态,它就有资格被清除。 如果操作系统需要更多内存,它可能会在不通知的情况下从内存中清除任何处于 Suspended 状态的 App。</p>
<p>系统将尽最大努力不清除最近执行的 App、Dock 中的任何 App 以及在当前表盘上有复杂功能的任何 App。 如果系统必须清除上述 App 之一,它将在内存可用时重新启动该 App。</p>
<p class="maodian"></p><h3>始终显示 Always on</h3>
<p>在 watchOS 6 之前,当用户最近没有与之交互时,Apple Watch 会息屏。 Always On 改变了这一点,使手表继续显示时间。但是,watchOS 会模糊当前运行的 App,并在显示屏上显示时间。</p>
<p>而现在,在默认情况下,会显示我们的 App 的用户界面,而不是时间。只要它是最前面的 App 或运行 Background session,watchOS 就不会模糊它。处于 Always On 时,手表屏幕会变暗,并且 UI 更新速度会变慢,从而延长电池使用时长。</p>
<p>如果用户与我们的 App 交互,系统将返回其 Active 状态。 Always On 的一个显着优势与日期和时间有关。如果 App 显示计时器或相对日期等,则 UI 将继续更新为正确的值。</p>
<p>如果你希望为我们的 App 禁用 Always On,只需在 Info.plist 中将 WKSupportsAlwaysOnDisplay 键设置为 false。用户还可以通过&ldquo;设置&rdquo;▸&ldquo;显示和亮度&rdquo;▸&ldquo;始终显示&rdquo;来为某些 App 或整个设备禁用 Always on。</p>
<p class="maodian"></p><h2>状态变化示例</h2>
<p>创建一个新项目:</p>
<p style="text-align:center"><img alt="" src="https://img.jbzj.com/file_images/article/202210/20221031084343016.png" /></p>
<p style="text-align:center"><img alt="" src="https://img.jbzj.com/file_images/article/202210/20221031084343017.png" /></p>
<p>新增 ExtensionDelegate.swift 文件:</p>
<div class="jb51code"><pre class="brush:cpp;">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)
    }
}
</pre></div>
<p>修改 LifecycleApp.swift 代码:</p>
<div class="jb51code"><pre class="brush:cpp;">import SwiftUI
@mainstruct 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)")
      }
    }
}
</pre></div>
<p>我们可以在物理设备上运行该项目以观察状态变化。当我们抬起和放下手腕时,会看到状态在 Active 和 Inactive 之间变化。如果我们让应用程序处于 Inactive 状态两分钟,它会切换到 Background 模式。</p>
<p style="text-align:center"><img alt="" src="https://img.jbzj.com/file_images/article/202210/20221031084343018.png" /></p>
<p class="maodian"></p><h2>WKExtendedRuntimeSession</h2>
<p>有四种特定的类型,可以让我们的 App 保持运行,甚至在后台运行。</p>
<p class="maodian"></p><h3>Self care</h3>
<p>专注于用户情绪健康或健康的 App 将在前台运行,即使在手表屏幕未打开。 watchOS 将为 App 序提供 10 分钟的 Session,该 Session 将持续到用户切换到另一个 App 或 App 使 Session 无效。</p>
<p class="maodian"></p><h3>Mindfulness</h3>
<p>冥想已越来越成为一种流行的,Mindfulness - 正念 App 将保持在前台。不过,这是一个耗时的过程,所以 watchOS 会给 App 一个 1 小时的 Session。</p>
<p class="maodian"></p><h3>Physical therapy</h3>
<p>伸展等锻炼非常适合物理治疗课程。 与最后两种 Session 类型不同,物理治疗 Session 在后台运行。 后台 Session 将一直运行,直到到时间限制或 App 使 Session 无效,即使用户启动另一个 App 也是如此。物理治疗课程可长达 1 小时。</p>
<p class="maodian"></p><h3>Smart alarm</h3>
<p>当我们需要安排时间检查用户的心率和运动时,智能提醒是一个不错的选择。 App 将获得一个 30 分钟的 Sesion。</p>
<p>与其他三种会话类型不同,我们必须安排智提醒钟在未来某一个时刻开始。 我们需要在接下来的 36 小时内启动会话,并在我们的 App 处于 WKApplicationState.active 状态时安排它。我们的Aoo 能会暂停或终止,但 Sesion 将继续。</p>
<p>当需要处理 Session 时,watchOS 将调用 App 的 WKExtensionDelegate 的 handle(_:)。</p>
<blockquote><p>注意:我们必须在 App 退出之前设置会话的委托,否则 Session 将终止。</p></blockquote>
<p>一旦 Session 运行,我们必须通过调用会话的 notifyUser(hapticType:repeatHandler:) 来触发提醒。 如果我们忘记了,watchOS 将显示警告并提议禁用 Session。</p>
<p class="maodian"></p><h2>刷牙提醒 Demo - Dentisit</h2>
<p class="maodian"></p><h3>搭建项目框架</h3>
<p>我们将实现一个刷牙时,提醒用户刷牙时间的 App Dentisit,首先创建项目:</p>
<p style="text-align:center"><img alt="" src="https://img.jbzj.com/file_images/article/202210/20221031084343019.png" /></p>
<p style="text-align:center"><img alt="" src="https://img.jbzj.com/file_images/article/202210/20221031084343020.png" /></p>
<p>修改 ContentView.swift,这样能让我们更好的看到 App 的状态:</p>
<div class="jb51code"><pre class="brush:cpp;">struct ContentView: View {
    @Environment(.scenePhase) private var scenePhase
    var body: some View {
      Text("Hello, World!")
      .onChange(of: scenePhase) { print($0) }
    }
}
</pre></div>
<p>新增 GetReadyView.swift,它将实现一个准备视图:</p>
<div class="jb51code"><pre class="brush:cpp;">import SwiftUI
struct GetReadyView: View {
private let color: Color // 色环颜色
@State private var stage: Int // 倒计时秒数,屏幕上展示的值
private let onComplete: (() -&gt; 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: (() -&gt; 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 &gt; 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()
}
}
</pre></div>
<p>具体代码内容,已经添加注释以辅助阅读,可以在 ContentView 中添加 GetReadyView() 来查看效果:</p>
<div class="jb51code"><pre class="brush:cpp;">struct ContentView: View {
    @Environment(.scenePhase) private var scenePhase
    var body: some View {
//      Text("Hello, World!")
      GetReadyView()
      .onChange(of: scenePhase) { print($0) }
    }
}
</pre></div>
<p>最终效果如下:</p>
<p style="text-align:center"><img alt="" src="https://img.jbzj.com/file_images/article/202210/20221031084343021.gif" /></p>
<p class="maodian"></p><h3>选择 Session 类型</h3>
<p>刷牙属于 Self care 类型,按照下图中的步骤添加新功能。 首先,从 Project Navigator 菜单中选择 Dentisit。 然后选择 Dentisit Watch App,选择 Signing &amp; Capabilities,然后按 + Capability 选项。 出现提示时,从功能列表中选择 Background Modes,并修改 Session Type:</p>
<p style="text-align:center"><img alt="" src="https://img.jbzj.com/file_images/article/202210/20221031084343022.png" /></p>
<p style="text-align:center"><img alt="" src="https://img.jbzj.com/file_images/article/202210/20221031084343023.png" /></p>
<p class="maodian"></p><h3>添加 ContentModel</h3>
<p>创建一个名为 ContentModel.swift 的新文件:</p>
<div class="jb51code"><pre class="brush:cpp;">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!
}
</pre></div>
<p>在代码中,ContentModel 需要符合 ObservableObject 以便模型可以更新 ContentView。我们还需要继承 NSObject,这是 WKExtendedRuntimeSessionDelegate 的要求。</p>
<p>前三个属性用 @Published 包装,我们将使用它们来跟踪用户还需要刷几轮、刷多久。</p>
<p>最后,我们需要一种方法来知道时间到了,并控制会话。</p>
<p>用户开始刷牙后,我们需要创建会话并更新表盘按钮上显示的文本。将此添加到 ContentModel:</p>
<div class="jb51code"><pre class="brush:cpp;">func startBrushing() {
    session = WKExtendedRuntimeSession()
    session.delegate = self
    session.start()
}
</pre></div>
<p>将以下代码添加到文件末尾实现 WKExtendedRuntimeSessionDelegate:</p>
<div class="jb51code"><pre class="brush:cpp;">extension ContentModel: WKExtendedRuntimeSessionDelegate {
    func extendedRuntimeSessionDidStart(
      _ extendedRuntimeSession: WKExtendedRuntimeSession
    ) {
    }
    func extendedRuntimeSessionWillExpire(
      _ extendedRuntimeSession: WKExtendedRuntimeSession
    ) {
    }
    func extendedRuntimeSession(
      _ extendedRuntimeSession: WKExtendedRuntimeSession,
      didInvalidateWith reason: WKExtendedRuntimeSessionInvalidationReason,
      error: Error?
    ) {
    }
}
</pre></div>
<p>遵守协议非常简单:</p>
<ul><li>一旦 Session 开始运行,系统就会调用 extendedRuntimeSessionDidStart(_:)。</li><li>如果 App 即将超过 Session的时间限制,watchOS 将在强制使会话过期之前调用 extendedRuntimeSessionWillExpire(_:)。</li><li>无论出于何种原因,当会话完成时,watchOS 都会调用 extendedRuntimeSession(_:didInvalidateWith:error:)。</li></ul>
<p>继续在 在extendedRuntimeSessionDidStart(_:) 中添加:</p>
<div class="jb51code"><pre class="brush:cpp;">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)
</pre></div>
<p>我们不关心实际的日期或时间:我们只需要特定的秒数。当 Session 开始时,让手表快速振动是很好的用户体验。</p>
<p>现在我们知道每轮刷牙需要多长时间,继续设置一个计时器。 添加以下代码以完成该方法:</p>
<div class="jb51code"><pre class="brush:cpp;">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)
</pre></div>
<p>我们生成一个计时器,该计时器在当前刷牙 Round 结束时开始,并每隔 secondsPerRound 秒重复一次。如果仍有几轮要执行,则更新一轮结束的时间,以便更新视图的显示。 让手表振动让用户知道是时候切换到他们嘴巴的新部分了。如果最后一轮完成,我们可以进行两次振动提醒用户。最后,将计时器安排到 run loop 中。</p>
<p>extendedRuntimeSession(_:didInvalidateWith:error:) 是禁用计时器的地方:</p>
<div class="jb51code"><pre class="brush:cpp;">func extendedRuntimeSession(
    _ extendedRuntimeSession: WKExtendedRuntimeSession,
    didInvalidateWith reason: WKExtendedRuntimeSessionInvalidationReason,
    error: Error?
) {
    timer.invalidate()
    timer = nil   
    endOfRound = nil   
    endOfBrushing = nil   
    roundsLeft = 0
}
</pre></div>
<p class="maodian"></p><h3>更新 UI</h3>
<p>修改 ContentView:</p>
<div class="jb51code"><pre class="brush:cpp;">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()
    }
}
</pre></div>
<p>添加开始按钮、时间展示,并运行项目,我们的 Dentisit 就开始工作了:</p>
<p style="text-align:center"><img alt="" src="https://img.jbzj.com/file_images/article/202210/20221031084343024.gif" /></p>
<p><strong>附件</strong></p>
<p>你可以在这里获得文章项目:github.com/LLLLLayer/A&hellip;</p>
<p>以上就是Apple Watch App Lifecycle应用开发的详细内容,更多关于Apple Watch App Lifecycle的资料请关注琼殿技术社区其它相关文章!</p>
                           
                            <div class="art_xg">
                              <b>您可能感兴趣的文章:</b><ul><li>浅谈IOS如何对app进行安全加固</li><li>iOS整个APP实现灰色主题的示例代码</li><li>iOS APP实现微信H5支付示例总结</li></ul>
                            </div>

                        </div>
                        <!--endmain-->
頁: [1]
查看完整版本: Apple Watch App Lifecycle应用开发