Haptic 触觉反馈

SwiftUI 内置了对简单触觉效果的支持,它使用 Apple 的 Taptic Engine 使手机以各种方式振动。它只有在真机上才有效,这些触觉效果仅适用于实体 iPhone,Mac 和 iPad 等其他设备不会振动。


sensoryFeedback 修饰符

参数:trigger 触发器

参数 trigger 要绑定一个状态参数,只要该状态参数发生变化,就会触发震动。

Button("Tap Count: \\(counter)") { 
		counter += 1 
}
.sensoryFeedback(.increase, trigger: counter)

参数:内置触觉反馈实例

内置的触觉反馈实例,每一种都有不同的感觉。使用时请考虑一下这对于依赖触觉传达信息的盲人用户来说可能会造成什么困惑,不能乱用。比如应用程序遇到错误,但您选择使用 .success ,这可能会引起混乱。一般通过实例的命名即可推测其大致使用场景。

Button("Tap Count: \\(counter)") { 
		counter += 1 
}
.sensoryFeedback(.increase, trigger: counter)

常见种类:

参数:impact 自定义反馈

如果想要多点自定义控制,可以使用一种名为 .impact() 的替代方案,它有两种变体:

// 方式一:指定反馈的 flexible 属性以及强度 intensity
// 例如,我们可以请求“软”对象,强度为 0.5:
.sensoryFeedback(.impact(flexibility: .soft, intensity: 0.5), trigger: counter)
// 方式二: 指定具体重量,以及强度 intensity
.sensoryFeedback(.impact(weight: .heavy, intensity: 1), trigger: counter)

闭包:根据状态参数选择反馈

// 例如该例子中 trigger 参数绑定的是布尔值,在闭包中用 oldValue, newValue 可以做判断
.sensoryFeedback(trigger: issue.completed) { oldValue, newValue in
		// 如果要改变的新值是 true ,那执行 success 反馈
		if newValue {
				.success
		} 
		// 如果要改变的新值是 false ,那不执行反馈
		else {
				nil
		}
}

CoreHaptics 框架

Apple 提供了名为 Core Haptics 的完整框架。Core Haptics 能够通过组合敲击、连续振动、参数曲线等来创建高度可定制的触觉。

1. 创建和启动引擎 CHHapticEngine

// 1. 首先在文件顶部导入 CoreHaptics 框架
import CoreHaptics

// 2. 需要创建一个 CHHapticEngine 引擎实例作为属性(这是负责创建振动的实际对象,在启动具体触觉效果之前必须创建它)
@State private var engine: CHHapticEngine?

// 3. 创建一个 触觉预准备 的方法(该方法主要启动引擎)
func prepareHaptics() {
		// 确保手机支持震动,不支持就直接返回
    guard CHHapticEngine.capabilitiesForHardware().supportsHaptics else { return }
    do {
    
				// 支持的话,生成一个新 CHHapticEngine 实例,赋予之前的状态属性;
        engine = try CHHapticEngine()
        
				// 并且调用该实例的 start 方法,启动引擎
        try engine?.start()
        
    } catch {
				// 有错的时候报错
        print("There was an error creating the engine: \\(error.localizedDescription)")
    }
}

2. 自定义触觉方法 CHHapticEvent

我们可以配置参数来控制触觉的强度( .hapticIntensity )以及触觉的“锐度”( .hapticSharpness ),然后将它们放入触觉中具有相对时间偏移的事件。hapticSharpness是一个奇怪的术语(与值 1 相比,锐度值 0 确实感觉迟钝)。至于相对时间,这让我们可以创建很多单个序列中的触觉事件。

//例如:创建以下方法
func complexSuccess() {
    //确保手机支持震动,不支持就直接返回
    guard CHHapticEngine.capabilitiesForHardware().supportsHaptics else { return }

		//创建了一个 CHHapticEvent 事件数组
    var events = [CHHapticEvent]()

    // create one intense, sharp tap
    let intensity = CHHapticEventParameter(parameterID: .hapticIntensity, value: 1)
    let sharpness = CHHapticEventParameter(parameterID: .hapticSharpness, value: 1)
    let event0 = CHHapticEvent(eventType: .hapticTransient, parameters: [intensity, sharpness], relativeTime: 0)
		let event1 = CHHapticEvent(eventType: .hapticTransient, parameters: [intensity, sharpness], relativeTime: 1)
		let event2 = CHHapticEvent(eventType: .hapticTransient, parameters: [intensity, sharpness], relativeTime: 2)
		
		//这里可以创建多个 event
    events.append(event0)
		events.append(event1)
		events.append(event2)

    // convert those events into a pattern and play it immediately
    do {
        let pattern = try CHHapticPattern(events: events, parameters: [])
        let player = try engine?.makePlayer(with: pattern)
        try player?.start(atTime: 0)
    } catch {
        print("Failed to play pattern: \\(error.localizedDescription).")
    }
}

中间创建触觉效果的部分还可以这样:

//用循环创建一个逐渐递减的效果
for i in stride(from: 0, to: 1, by: 0.1) {
    let intensity = CHHapticEventParameter(parameterID: .hapticIntensity, value: Float(1 - i))
    let sharpness = CHHapticEventParameter(parameterID: .hapticSharpness, value: Float(1 - i))
    let event = CHHapticEvent(eventType: .hapticTransient, parameters: [intensity, sharpness], relativeTime: 1 + i)
    events.append(event)
}

3. 调用自定义触觉

// 调用自定义的触觉方法时,请务必先启动触觉引擎
// 前面将 “引擎启动” 是封装到 prepareHaptics 方法中的
// 同时将 “自定义触觉效果“ 是封装到 complexSuccess 方法中的

// 所以一种方法是添加 onAppear() 先执行 prepareHaptics,确保触觉系统启动;然后点击手势再执行 complexSuccess
Button("Tap Me", action: complexSuccess)
    .onAppear(perform: prepareHaptics)

4. 将 “预准备” 和 “执行” 合二为一

当然也可以把两个方法合成一个,只要记得每次都先启动引擎,再启动模式即可。

func toggleCompleted() {
        do {
		        // 先启动引擎
            try engine?.start()
            let sharpness = CHHapticEventParameter(parameterID: .hapticSharpness, value: 0)
            let intensity = CHHapticEventParameter(parameterID: .hapticIntensity, value: 1)
            let event1 = CHHapticEvent(
                eventType: .hapticTransient,
                parameters: [intensity, sharpness],
                relativeTime: 0
            )
            let pattern = try CHHapticPattern(events: [event1])
            let player = try engine?.makePlayer(with: pattern)
            // 再启动具体播放器
            try player?.start(atTime: 0)
        } catch {
        }
}

在这段代码中,try engine?.start()try player?.start(atTime: 0) 的区别在于:

try engine?.start

这行代码用于启动 haptic 引擎,,使其进入工作状态。

try player?.start

这行代码用于启动 haptic player,是在使用 haptic 引擎创建的 player 对象播放特定的 haptic 模式。

<aside> 💡 以上两者分别启动引擎和启动播放器,如果引擎没有成功启动,播放器就无法工作。这两步操作共同完成 haptic 反馈的生成和播放。

</aside>


CHHapticEvent 的主要参数

1. 事件类型 eventType

eventType 是在创建 CHHapticEvent 对象时指定的事件类型。在 Core Haptics 中,主要有两种主要的事件类型:

hapticTransient 瞬态事件

let sharpness = CHHapticEventParameter(parameterID: .hapticSharpness, value: 0)
let intensity = CHHapticEventParameter(parameterID: .hapticIntensity, value: 1)
let transientEvent = CHHapticEvent(
    eventType: .hapticTransient,
    parameters: [intensity, sharpness],
    relativeTime: 0
)

hapticContinuous 连续事件

let sharpness = CHHapticEventParameter(parameterID: .hapticSharpness, value: 0)
let intensity = CHHapticEventParameter(parameterID: .hapticIntensity, value: 1)
let continuousEvent = CHHapticEvent(
    eventType: .hapticContinuous,
    parameters: [intensity, sharpness],
    relativeTime: 0,
    duration: 1.0
)

创建一个包含瞬态和连续事件的模式

通过这种方式,可以根据具体需求组合瞬态和连续事件,以提供丰富的触觉反馈体验。

let sharpness = CHHapticEventParameter(parameterID: .hapticSharpness, value: 0)
let intensity = CHHapticEventParameter(parameterID: .hapticIntensity, value: 1)

// 创建瞬态事件
let transientEvent = CHHapticEvent(
    eventType: .hapticTransient,
    parameters: [intensity, sharpness],
    relativeTime: 0
)

// 创建连续事件
let continuousEvent = CHHapticEvent(
    eventType: .hapticContinuous,
    parameters: [intensity, sharpness],
    relativeTime: 0.5,
    duration: 1.0
)

do {
    // 创建 haptic 模式
    let pattern = try CHHapticPattern(events: [transientEvent, continuousEvent], parameters: [])
    let player = try engine?.makePlayer(with: pattern)
    try player?.start(atTime: 0)
} catch {
    print("Failed to create haptic pattern: \\\\(error.localizedDescription)")
}

2. 曲线参数 CHHapticParameterCurve

还可以使用 CHHapticParameterCurve 曲线参数,制造触觉反馈逐渐消失的效果。

func toggleCompleted() {
		do {
                try engine?.start()
                // 创建两个参数给后面的事件使用
                let sharpness = CHHapticEventParameter(parameterID: .hapticSharpness, value: 0)
                let intensity = CHHapticEventParameter(parameterID: .hapticIntensity, value: 1)
                
                // 添加两个控制点:它们本身不做任何事情,因为它们只是孤立的点
                let start = CHHapticParameterCurve.ControlPoint(relativeTime: 0, value: 1)
                let end = CHHapticParameterCurve.ControlPoint(relativeTime: 1, value: 0)
                
                // 这一行会将两个点组合成一条曲线,该曲线从 start 开始,到 end 结束,用于控制 haptic 的强度
                let parameter = CHHapticParameterCurve(
                    parameterID: .hapticIntensityControl,
                    controlPoints: [start, end],
                    relativeTime: 0
                )
                
                // 事件1: 瞬态的快速点击,强烈而沉闷,并立即开始
                let event1 = CHHapticEvent(
                    eventType: .hapticTransient,
                    parameters: [intensity, sharpness],
                    relativeTime: 0
                )
                // 事件2: 连续的嗡嗡声,强烈而沉闷,持续1秒钟,在 1/8 秒后开始,感觉与刚刚进行的快速点击是分开的
                let event2 = CHHapticEvent(
                    eventType: .hapticContinuous,
                    parameters: [sharpness, intensity],
                    relativeTime: 0.125,
                    duration: 1
                )
                
                // 结合这两个事件作为一个序列发生,同时附加制作的参数曲线,以便连续触觉的强度逐渐消失
                // 这是通过将我们的事件包装在一个模式 pattern 中,并将它们作为一个数组来完成的
                let pattern = try CHHapticPattern(events: [event1, event2], parameterCurves: [parameter])
                
                // 使用该 pattern 模式,声明一个 player 属性
                let player = try engine?.makePlayer(with: pattern)
                // player 开始播放触觉反馈
                try player?.start(atTime: 0)
            } catch {
                // 如果由于某种原因触觉效果失败,它实际上对程序功能没有影响,所以可以不处理
		}
}

3. 参数标识符

在上一段代码中,parameterID 指的是 CHHapticParameterCurve 的参数标识符,用于指定参数曲线控制的具体触觉参数类型。CHHapticParameterCurve 可以控制触觉反馈的某些属性(如强度、锐度)随时间变化的曲线。

parameterID 的类型是 CHHapticDynamicParameter.ID。除了 .hapticIntensityControl,还有其他类型的参数标识符,可以用于控制不同的触觉反馈属性。以下是一些常用的参数标识符:

.hapticIntensityControl

.hapticSharpnessControl

.hapticAttackTimeControl

.hapticDecayTimeControl

.hapticReleaseTimeControl