从问题出发:研究环境变量scenePhase应用生命周期
从问题出发:研究环境变量scenePhase应用生命周期

从问题出发:研究环境变量scenePhase应用生命周期

问题前引

本篇文章的写作初衷在于,想要解决Core Haptics在将应用切换到后台模式后,重新打开应用时(从后台切换到前台,没有退出应用),CoreHaptics振动效果失效。

import SwiftUI
import CoreHaptics

class HapticManager {
    private var hapticEngine: CHHapticEngine?
    
    init() {
        createEngine()
    }
    
    private func createEngine() {
        do {
            hapticEngine = try CHHapticEngine()
            try hapticEngine?.start()
        } catch {
            print("Failed to create the haptic engine: \(error)")
        }
    }
    
    func playSimpleHaptic() {
        guard CHHapticEngine.capabilitiesForHardware().supportsHaptics else { return }
        
        let hapticEvent = CHHapticEvent(eventType: .hapticTransient, // 瞬时触觉事件
                                        parameters: [],
                                        relativeTime: 0)
        
        do {
            let pattern = try CHHapticPattern(events: [hapticEvent], parameters: [])
            let player = try hapticEngine?.makePlayer(with: pattern)
            try player?.start(atTime: 0)
        } catch {
            print("Failed to play haptic: \(error)")
        }
    }
}

struct Touch: View {
    let hapticManager = HapticManager()
    
    var body: some View {
        VStack(spacing: 20) {     
            Button("Click me") {
                hapticManager.playSimpleHaptic()
            }
        }
        .padding()
    }
}

切换至后台并重新打开后,振动效果消失的原因可能与CHHapticEngine的行为有关。当应用切换到后台时,CHHapticEngine可能会被暂停或停止,因此需要在应用返回到前台时重新启动它。

解决方案为:监听应用的状态变化,并根据状态控制CHHapticEngine的启动和停止,确保CHHapticEngine在应用返回前台时恢复正常工作,所以我们可以考虑使用UIApplication的系统通知来处理。

下面是需要新增的代码部分:

private func addObservers() {
    NotificationCenter.default.addObserver(self, selector: #selector(appDidBecomeActive), name: UIApplication.didBecomeActiveNotification, object: nil)
    NotificationCenter.default.addObserver(self, selector: #selector(appWillResignActive), name: UIApplication.willResignActiveNotification, object: nil)
}
@objc private func appDidBecomeActive() {
    do {
        try hapticEngine?.start()
    } catch {
        print("Failed to restart the haptic engine: \(error)")
    }
}
@objc private func appWillResignActive() {
    hapticEngine?.stop(completionHandler: nil)
}

当我意识到NotificationCenter是基于Objective-C时,突然想到可以使用Swift UI的监听应用方法,由此转为本次关于scenePhase的学习文章。

我们可以通过使用@Environment属性包装器与scenePhase环境变量来监听应用的生命周期变化,而不是使用UIKit中的UIApplication通知功能。

scenePhase是什么?

@Environment(\.scenePhase) 是 SwiftUI 提供的一种属性包装器,用于检测应用的生命周期状态变化。scenePhase 可以帮助我们知道当前应用的状态,比如是否处于前台活动、后台、或者即将离开活跃状态。这对于像管理资源、节电模式、暂停操作等场景非常有用。

scenePhase 的三个主要状态

  • .active:表示应用正在前台,并且用户与应用正在交互。此时应用处于活跃状态。
  • .inactive:应用仍然在前台,但可能因为一些系统原因而变得不活跃,比如用户打开了通知中心或者控制中心。
  • .background:应用已经进入后台,不再显示在屏幕上。这通常是用户切换到其他应用或者按下了主屏幕按钮导致的。

如何使用scenePhase

以下是一个简单的示例,展示如何使用 @Environment(\.scenePhase) 在 SwiftUI 中监听应用的生命周期状态:

import SwiftUI

struct ContentView: View {
    @Environment(\.scenePhase) var scenePhase
    
    var body: some View {
        Text("Hello, World!")
            .onChange(of: scenePhase) { newPhase in
                switch newPhase {
                case .active:
                    print("App is active")
                    // 这里可以放置应用变为活跃状态时需要执行的代码
                case .inactive:
                    print("App is inactive")
                    // 这里可以放置应用变为非活跃状态时需要执行的代码
                case .background:
                    print("App is in background")
                    // 这里可以放置应用进入后台时需要执行的代码
                @unknown default:
                    break
                }
            }
    }
}

struct ContentView_Previews: PreviewProvider {
    static var previews: some View {
        ContentView()
    }
}

当应用在前台时处于active状态,当应用在前台下拉通知栏或者上滑查看后台应用时,scenePhase处于inactive状态,最后是将应该放在后台运行时,scenePhase则处于background的状态。

对应的输出为:

App is active
App is inactive
App is active
App is inactive
App is in background
App is inactive
…

代码分析

1、@Environment(\.scenePhase) var scenePhase:声明一个 scenePhase 属性,SwiftUI 会自动将应用的当前生命周期状态传递给这个属性。

@Environment(\.scenePhase) var scenePhase

2、.onChange(of: scenePhase) { newPhase in … }:监听 scenePhase 的值变化。当应用的状态改变时(如从后台进入前台),newPhase 将更新,并且会触发对应的代码块。

.onChange(of: scenePhase) { newPhase in }

3、状态的处理:使用 switch 语句来根据不同的 scenePhase 值执行对应的逻辑,例如:

  1. 当应用变为 .active 时,可以在控制台输出日志、更新UI等。
  2. 当应用进入 .background 时,可以保存数据、停止动画或释放不必要的资源。
switch newPhase {
    case .active:
        print("App is active")
        // 这里可以放置应用变为活跃状态时需要执行的代码
    case .inactive:
        print("App is inactive")
        // 这里可以放置应用变为非活跃状态时需要执行的代码
    case .background:
        print("App is in background")
        // 这里可以放置应用进入后台时需要执行的代码
    @unknown default:
        break
}

使用场景

节省资源:在应用进入后台时,可以停止某些动画或释放占用资源的操作。

恢复操作:当应用重新变为活跃状态时,重新启动需要的操作或重新加载数据。

自动保存:在应用即将进入后台时自动保存用户的数据。

实际运用

在前面提到的在Touch结构中,应用从后台切换至前台后,存在振动效果失效的情况。

因为我们需要在Touch结构中声明@Environment(\.scenePhase),设置点击按钮时,执行hapticManager.playCustomHaptic方法,然后通过onChange监听VStack,根据scenePhase的变化执行对应的startEngine()和stopEngine()。

视图文件代码

import SwiftUI

struct Touch: View {
    @Environment(\.scenePhase) var scenePhase
    let hapticManager = HapticManager()

    var body: some View {
        VStack(spacing: 20) {
            Button("Play Custom Haptic") {
                hapticManager.playCustomHaptic()
            }
        }
        .padding()
        .onChange(of: scenePhase) { newPhase in
            switch newPhase {
            case .active:
                hapticManager.startEngine()
            case .inactive, .background:
                hapticManager.stopEngine()
            @unknown default:
                break
            }
        }
    }
}

Core Haptics文件代码

将Core Haptics单独封装到HapticManager文件中:

import CoreHaptics

class HapticManager {
    private var hapticEngine: CHHapticEngine?
    
    init() {
        createEngine()
    }
    
    private func createEngine() {
        do {
            hapticEngine = try CHHapticEngine()
            try hapticEngine?.start()
        } catch {
            print("Failed to create the haptic engine: \(error)")
        }
    }
    
    func startEngine() {
        do {
            try hapticEngine?.start()
        } catch {
            print("Failed to restart the haptic engine: \(error)")
        }
    }
    
    func stopEngine() {
        hapticEngine?.stop(completionHandler: nil)
    }
    
    func playCustomHaptic() {
        guard CHHapticEngine.capabilitiesForHardware().supportsHaptics else { return }
        let intensity = CHHapticEventParameter(parameterID: .hapticIntensity, value: 1.0)
        let sharpness = CHHapticEventParameter(parameterID: .hapticSharpness, value: 0.5)
        
        let event = CHHapticEvent(eventType: .hapticContinuous,
                                  parameters: [intensity, sharpness],
                                  relativeTime: 0.1,
                                  duration: 1.0)
        
        do {
            let pattern = try CHHapticPattern(events: [event], parameters: [])
            let player = try hapticEngine?.makePlayer(with: pattern)
            try player?.start(atTime: 0)
        } catch {
            print("Failed to play custom haptic: \(error)")
        }
    }
}

总结

本篇文章主要涉及如何通过scenePhase应用生命周期,通过scenePhase的三种状态,来判断执行Core Haptic的启动或者暂停代码。

使用Swift UI的@Environment(\.scenePhase)和.onChange修饰符,可以更简洁地监听应用生命周期的变化,无需直接使用 NotificationCenter。这种方法在 SwiftUI 应用中更加优雅和符合 SwiftUI 的声明式编程风格。

如果不理解Core Haptic代码部分,可以查看一下刚写的Core Haptics文章《高度定制化触觉反馈体验:Core Haptics框架》。

如果您认为这篇文章给您带来了帮助,您可以在此通过支付宝或者微信打赏网站开放者。

发表回复

您的电子邮箱地址不会被公开。 必填项已用 * 标注