问题前引
本篇文章的写作初衷在于,想要解决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 值执行对应的逻辑,例如:
- 当应用变为 .active 时,可以在控制台输出日志、更新UI等。
- 当应用进入 .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框架》。