axiom-haptics
Compare original and translation side by side
🇺🇸
Original
English🇨🇳
Translation
ChineseHaptics & Audio Feedback
触觉与音频反馈
Comprehensive guide to implementing haptic feedback on iOS. Every Apple Design Award winner uses excellent haptic feedback - Camera, Maps, Weather all use haptics masterfully to create delightful, responsive experiences.
这是一份在iOS上实现触觉反馈的综合指南。每一款获得苹果设计奖的应用都运用了出色的触觉反馈——相机、地图、天气应用都熟练地利用触觉打造了愉悦、响应迅速的体验。
Overview
概述
Haptic feedback provides tactile confirmation of user actions and system events. When designed thoughtfully using the Causality-Harmony-Utility framework, axiom-haptics transform interfaces from functional to delightful.
This skill covers both simple haptics () and advanced custom patterns (), with real-world examples and audio-haptic synchronization techniques.
UIFeedbackGeneratorCore Haptics触觉反馈为用户操作和系统事件提供触觉确认。当运用因果性-协调性-实用性框架进行精心设计时,触觉反馈能将界面从功能性提升至令人愉悦的体验。
本指南涵盖了简单触觉()和高级自定义模式(),并提供了真实场景示例和音触觉同步技术。
UIFeedbackGeneratorCore HapticsWhen to Use This Skill
适用场景
- Adding haptic feedback to user interactions
- Choosing between UIFeedbackGenerator and Core Haptics
- Designing audio-haptic experiences that feel unified
- Creating custom haptic patterns with AHAP files
- Synchronizing haptics with animations and audio
- Debugging haptic issues (simulator vs device)
- Optimizing haptic performance and battery impact
- 为用户交互添加触觉反馈
- 在UIFeedbackGenerator和Core Haptics之间做选择
- 设计统一的音触觉体验
- 使用AHAP文件创建自定义触觉模式
- 同步触觉与动画、音频
- 调试触觉问题(模拟器与真机差异)
- 优化触觉性能和电池消耗
System Requirements
系统要求
- iOS 10+ for UIFeedbackGenerator
- iOS 13+ for Core Haptics (CHHapticEngine)
- iPhone 8+ for Core Haptics hardware support
- Physical device required - haptics cannot be felt in Simulator
- iOS 10+ 支持UIFeedbackGenerator
- iOS 13+ 支持Core Haptics(CHHapticEngine)
- iPhone 8+ 具备Core Haptics硬件支持
- 需使用真机 - 模拟器无法体验触觉反馈
Part 1: Design Principles (WWDC 2021/10278)
第一部分:设计原则(WWDC 2021/10278)
Apple's audio and haptic design teams established three core principles for multimodal feedback:
苹果的音频和触觉设计团队为多模态反馈确立了三大核心原则:
Causality - Make it obvious what caused the feedback
因果性 - 明确反馈的触发源
Problem: User can't tell what triggered the haptic
Solution: Haptic timing must match the visual/interaction moment
Example from WWDC:
- ✅ Ball hits wall → haptic fires at collision moment
- ❌ Ball hits wall → haptic fires 100ms later (confusing)
Code pattern:
swift
// ✅ Immediate feedback on touch
@objc func buttonTapped() {
let generator = UIImpactFeedbackGenerator(style: .medium)
generator.impactOccurred() // Fire immediately
performAction()
}
// ❌ Delayed feedback loses causality
@objc func buttonTapped() {
performAction()
DispatchQueue.main.asyncAfter(deadline: .now() + 0.2) {
let generator = UIImpactFeedbackGenerator(style: .medium)
generator.impactOccurred() // Too late!
}
}问题:用户无法判断是什么触发了触觉反馈
解决方案:触觉反馈的时机必须与视觉/交互时刻完全匹配
WWDC示例:
- ✅ 球碰到墙 → 碰撞瞬间触发触觉
- ❌ 球碰到墙 → 100ms后触发触觉(易造成困惑)
代码示例:
swift
// ✅ 触摸时立即反馈
@objc func buttonTapped() {
let generator = UIImpactFeedbackGenerator(style: .medium)
generator.impactOccurred() // 立即触发
performAction()
}
// ❌ 延迟反馈丢失因果性
@objc func buttonTapped() {
performAction()
DispatchQueue.main.asyncAfter(deadline: .now() + 0.2) {
let generator = UIImpactFeedbackGenerator(style: .medium)
generator.impactOccurred() // 太晚了!
}
}Harmony - Senses work best when coherent
协调性 - 多感官协同工作
Problem: Visual, audio, and haptic don't match
Solution: All three senses should feel like a unified experience
Example from WWDC:
- Small ball → light haptic + high-pitched sound
- Large ball → heavy haptic + low-pitched sound
- Shield transformation → continuous haptic + progressive audio
Key insight: A large object should feel heavy, sound low and resonant, and look substantial. All three senses reinforce the same experience.
问题:视觉、音频和触觉反馈不匹配
解决方案:三种感官应形成统一的体验
WWDC示例:
- 小球 → 轻柔触觉 + 高音调声音
- 大球 → 厚重触觉 + 低音调声音
- 护盾变形 → 持续触觉 + 渐进式音频
核心要点:大型物体应该摸起来厚重、听起来低沉共鸣、看起来有分量。三种感官共同强化同一种体验。
Utility - Provide clear value
实用性 - 提供明确价值
Problem: Haptics used everywhere "just because we can"
Solution: Reserve haptics for significant moments that benefit the user
When to use haptics:
- ✅ Confirming an important action (payment completed)
- ✅ Alerting to critical events (low battery)
- ✅ Providing continuous feedback (scrubbing slider)
- ✅ Enhancing delight (app launch flourish)
When NOT to use haptics:
- ❌ Every single tap (overwhelming)
- ❌ Scrolling through long lists (battery drain)
- ❌ Background events user can't see (confusing)
- ❌ Decorative animations (no value)
问题:滥用触觉反馈“只是因为我们能做到”
解决方案:仅在对用户有益的重要时刻使用触觉反馈
适合使用触觉的场景:
- ✅ 确认重要操作(支付完成)
- ✅ 提醒关键事件(低电量)
- ✅ 提供持续反馈(滑动滑块)
- ✅ 提升愉悦感(应用启动动画)
不适合使用触觉的场景:
- ❌ 每一次点击(过于频繁)
- ❌ 滚动长列表(消耗电池)
- ❌ 用户不可见的后台事件(易造成困惑)
- ❌ 装饰性动画(无实际价值)
Part 2: UIFeedbackGenerator (Simple Haptics)
第二部分:UIFeedbackGenerator(简单触觉)
For most apps, provides 3 simple haptic types without custom patterns.
UIFeedbackGenerator对于大多数应用,提供3种无需自定义模式的简单触觉类型。
UIFeedbackGeneratorUIImpactFeedbackGenerator
UIImpactFeedbackGenerator
Physical collision or impact sensation.
Styles (ordered light → heavy):
- - Small, delicate tap
.light - - Standard tap (most common)
.medium - - Strong, solid impact
.heavy - - Firm, precise tap
.rigid - - Gentle, cushioned tap
.soft
Usage pattern:
swift
class MyViewController: UIViewController {
let impactGenerator = UIImpactFeedbackGenerator(style: .medium)
override func viewDidLoad() {
super.viewDidLoad()
// Prepare reduces latency for next impact
impactGenerator.prepare()
}
@objc func userDidTap() {
impactGenerator.impactOccurred()
}
}Intensity variation (iOS 13+):
swift
// intensity: 0.0 (lightest) to 1.0 (strongest)
impactGenerator.impactOccurred(intensity: 0.5)Common use cases:
- Button taps ()
.medium - Toggle switches ()
.light - Deleting items ()
.heavy - Confirming selections ()
.rigid
模拟物理碰撞或冲击的触感。
样式(从轻到重排序):
- - 轻微、细腻的点击
.light - - 标准点击(最常用)
.medium - - 强烈、扎实的冲击
.heavy - - 坚定、精准的点击
.rigid - - 轻柔、缓冲的点击
.soft
使用示例:
swift
class MyViewController: UIViewController {
let impactGenerator = UIImpactFeedbackGenerator(style: .medium)
override func viewDidLoad() {
super.viewDidLoad()
// 提前准备以减少下次触发的延迟
impactGenerator.prepare()
}
@objc func userDidTap() {
impactGenerator.impactOccurred()
}
}强度调节(iOS 13+):
swift
// intensity: 0.0(最轻)到1.0(最强)
impactGenerator.impactOccurred(intensity: 0.5)常见使用场景:
- 按钮点击()
.medium - 开关切换()
.light - 删除项目()
.heavy - 确认选择()
.rigid
UISelectionFeedbackGenerator
UISelectionFeedbackGenerator
Discrete selection changes (picker wheels, segmented controls).
Usage:
swift
class PickerViewController: UIViewController {
let selectionGenerator = UISelectionFeedbackGenerator()
func pickerView(_ picker: UIPickerView, didSelectRow row: Int,
inComponent component: Int) {
selectionGenerator.selectionChanged()
}
}Feels like: Clicking a physical wheel with detents
Common use cases:
- Picker wheels
- Segmented controls
- Page indicators
- Step-through interfaces
适用于离散的选择变化(选择器滚轮、分段控件)。
使用示例:
swift
class PickerViewController: UIViewController {
let selectionGenerator = UISelectionFeedbackGenerator()
func pickerView(_ picker: UIPickerView, didSelectRow row: Int,
inComponent component: Int) {
selectionGenerator.selectionChanged()
}
}触感:类似点击带定位的物理滚轮
常见使用场景:
- 选择器滚轮
- 分段控件
- 页面指示器
- 分步操作界面
UINotificationFeedbackGenerator
UINotificationFeedbackGenerator
System-level success/warning/error feedback.
Types:
- - Task completed successfully
.success - - Attention needed, but not critical
.warning - - Critical error occurred
.error
Usage:
swift
let notificationGenerator = UINotificationFeedbackGenerator()
func submitForm() {
// Validate form
if isValid {
notificationGenerator.notificationOccurred(.success)
saveData()
} else {
notificationGenerator.notificationOccurred(.error)
showValidationErrors()
}
}Best practice: Match haptic type to user outcome
- ✅ Payment succeeds →
.success - ✅ Form validation fails →
.error - ✅ Approaching storage limit →
.warning
系统级的成功/警告/错误反馈。
类型:
- - 任务成功完成
.success - - 需要注意但不紧急
.warning - - 发生严重错误
.error
使用示例:
swift
let notificationGenerator = UINotificationFeedbackGenerator()
func submitForm() {
// 验证表单
if isValid {
notificationGenerator.notificationOccurred(.success)
saveData()
} else {
notificationGenerator.notificationOccurred(.error)
showValidationErrors()
}
}最佳实践:将触觉类型与用户操作结果匹配
- ✅ 支付成功 →
.success - ✅ 表单验证失败 →
.error - ✅ 存储容量即将不足 →
.warning
Performance: prepare()
性能优化:prepare()
Call before the haptic to reduce latency:
prepare()swift
// ✅ Good - prepare before user action
@IBAction func buttonTouchDown(_ sender: UIButton) {
impactGenerator.prepare() // User's finger is down
}
@IBAction func buttonTouchUpInside(_ sender: UIButton) {
impactGenerator.impactOccurred() // Immediate haptic
}
// ❌ Bad - unprepared haptic may lag
@IBAction func buttonTapped(_ sender: UIButton) {
let generator = UIImpactFeedbackGenerator()
generator.impactOccurred() // May have 10-20ms delay
}Prepare timing: System keeps engine ready for ~1 second after .
prepare()在触发触觉前调用以减少延迟:
prepare()swift
// ✅ 良好实践 - 在用户操作前准备
@IBAction func buttonTouchDown(_ sender: UIButton) {
impactGenerator.prepare() // 用户手指已按下
}
@IBAction func buttonTouchUpInside(_ sender: UIButton) {
impactGenerator.impactOccurred() // 立即触发触觉
}
// ❌ 不良实践 - 未准备的触觉可能延迟
@IBAction func buttonTapped(_ sender: UIButton) {
let generator = UIImpactFeedbackGenerator()
generator.impactOccurred() // 可能有10-20ms延迟
}准备时机:调用后,系统会将引擎保持就绪状态约1秒。
prepare()Part 3: Core Haptics (Custom Haptics)
第三部分:Core Haptics(自定义触觉)
For apps needing custom patterns, provides full control over haptic waveforms.
Core Haptics对于需要自定义模式的应用,提供了对触觉波形的完全控制。
Core HapticsFour Fundamental Elements
四个基本元素
- Engine () - Link to the phone's actuator
CHHapticEngine - Player () - Playback control
CHHapticPatternPlayer - Pattern () - Collection of events over time
CHHapticPattern - Events () - Building blocks specifying the experience
CHHapticEvent
- 引擎()- 与手机执行器的连接
CHHapticEngine - 播放器()- 播放控制
CHHapticPatternPlayer - 模式()- 随时间变化的事件集合
CHHapticPattern - 事件()- 定义体验的基本构建块
CHHapticEvent
CHHapticEngine Lifecycle
CHHapticEngine生命周期
swift
import CoreHaptics
class HapticManager {
var engine: CHHapticEngine?
func initializeHaptics() {
// Check device support
guard CHHapticEngine.capabilitiesForHardware().supportsHaptics else {
print("Device doesn't support haptics")
return
}
do {
// Create engine
engine = try CHHapticEngine()
// Handle interruptions (calls, Siri, etc.)
engine?.stoppedHandler = { reason in
print("Engine stopped: \(reason)")
self.restartEngine()
}
// Handle reset (audio session changes)
engine?.resetHandler = {
print("Engine reset")
self.restartEngine()
}
// Start engine
try engine?.start()
} catch {
print("Failed to create haptic engine: \(error)")
}
}
func restartEngine() {
do {
try engine?.start()
} catch {
print("Failed to restart engine: \(error)")
}
}
}Critical: Always set and to handle system interruptions.
stoppedHandlerresetHandlerswift
import CoreHaptics
class HapticManager {
var engine: CHHapticEngine?
func initializeHaptics() {
// 检查设备支持
guard CHHapticEngine.capabilitiesForHardware().supportsHaptics else {
print("Device doesn't support haptics")
return
}
do {
// 创建引擎
engine = try CHHapticEngine()
// 处理中断(来电、Siri等)
engine?.stoppedHandler = { reason in
print("Engine stopped: \(reason)")
self.restartEngine()
}
// 处理重置(音频会话变化)
engine?.resetHandler = {
print("Engine reset")
self.restartEngine()
}
// 启动引擎
try engine?.start()
} catch {
print("Failed to create haptic engine: \(error)")
}
}
func restartEngine() {
do {
try engine?.start()
} catch {
print("Failed to restart engine: \(error)")
}
}
}关键提示:务必设置和以处理系统中断。
stoppedHandlerresetHandlerCHHapticEvent Types
CHHapticEvent类型
Transient Events
瞬时事件
Short, discrete feedback (like a tap).
swift
let intensity = CHHapticEventParameter(
parameterID: .hapticIntensity,
value: 1.0 // 0.0 to 1.0
)
let sharpness = CHHapticEventParameter(
parameterID: .hapticSharpness,
value: 0.5 // 0.0 (dull) to 1.0 (sharp)
)
let event = CHHapticEvent(
eventType: .hapticTransient,
parameters: [intensity, sharpness],
relativeTime: 0.0 // Seconds from pattern start
)Parameters:
- : Strength (0.0 = barely felt, 1.0 = maximum)
hapticIntensity - : Character (0.0 = dull thud, 1.0 = crisp snap)
hapticSharpness
短暂、离散的反馈(如点击)。
swift
let intensity = CHHapticEventParameter(
parameterID: .hapticIntensity,
value: 1.0 // 0.0到1.0
)
let sharpness = CHHapticEventParameter(
parameterID: .hapticSharpness,
value: 0.5 // 0.0(沉闷)到1.0(清脆)
)
let event = CHHapticEvent(
eventType: .hapticTransient,
parameters: [intensity, sharpness],
relativeTime: 0.0 // 相对于模式开始的秒数
)参数说明:
- :强度(0.0 = 几乎感觉不到,1.0 = 最大强度)
hapticIntensity - :触感特性(0.0 = 沉闷撞击,1.0 = 清脆点击)
hapticSharpness
Continuous Events
持续事件
Sustained feedback over time (like a vibration motor).
swift
let intensity = CHHapticEventParameter(
parameterID: .hapticIntensity,
value: 0.8
)
let sharpness = CHHapticEventParameter(
parameterID: .hapticSharpness,
value: 0.3
)
let event = CHHapticEvent(
eventType: .hapticContinuous,
parameters: [intensity, sharpness],
relativeTime: 0.0,
duration: 2.0 // Seconds
)Use cases:
- Rolling texture as object moves
- Motor running
- Charging progress
- Long press feedback
随时间持续的反馈(如振动马达)。
swift
let intensity = CHHapticEventParameter(
parameterID: .hapticIntensity,
value: 0.8
)
let sharpness = CHHapticEventParameter(
parameterID: .hapticSharpness,
value: 0.3
)
let event = CHHapticEvent(
eventType: .hapticContinuous,
parameters: [intensity, sharpness],
relativeTime: 0.0,
duration: 2.0 // 持续秒数
)使用场景:
- 物体移动时的滚动纹理
- 马达运行
- 充电进度
- 长按反馈
Creating and Playing Patterns
创建并播放模式
swift
func playCustomPattern() {
// Create events
let tap1 = CHHapticEvent(
eventType: .hapticTransient,
parameters: [
CHHapticEventParameter(parameterID: .hapticIntensity, value: 0.5),
CHHapticEventParameter(parameterID: .hapticSharpness, value: 0.5)
],
relativeTime: 0.0
)
let tap2 = CHHapticEvent(
eventType: .hapticTransient,
parameters: [
CHHapticEventParameter(parameterID: .hapticIntensity, value: 0.7),
CHHapticEventParameter(parameterID: .hapticSharpness, value: 0.7)
],
relativeTime: 0.3
)
let tap3 = CHHapticEvent(
eventType: .hapticTransient,
parameters: [
CHHapticEventParameter(parameterID: .hapticIntensity, value: 1.0),
CHHapticEventParameter(parameterID: .hapticSharpness, value: 1.0)
],
relativeTime: 0.6
)
do {
// Create pattern from events
let pattern = try CHHapticPattern(
events: [tap1, tap2, tap3],
parameters: []
)
// Create player
let player = try engine?.makePlayer(with: pattern)
// Play
try player?.start(atTime: CHHapticTimeImmediate)
} catch {
print("Failed to play pattern: \(error)")
}
}swift
func playCustomPattern() {
// 创建事件
let tap1 = CHHapticEvent(
eventType: .hapticTransient,
parameters: [
CHHapticEventParameter(parameterID: .hapticIntensity, value: 0.5),
CHHapticEventParameter(parameterID: .hapticSharpness, value: 0.5)
],
relativeTime: 0.0
)
let tap2 = CHHapticEvent(
eventType: .hapticTransient,
parameters: [
CHHapticEventParameter(parameterID: .hapticIntensity, value: 0.7),
CHHapticEventParameter(parameterID: .hapticSharpness, value: 0.7)
],
relativeTime: 0.3
)
let tap3 = CHHapticEvent(
eventType: .hapticTransient,
parameters: [
CHHapticEventParameter(parameterID: .hapticIntensity, value: 1.0),
CHHapticEventParameter(parameterID: .hapticSharpness, value: 1.0)
],
relativeTime: 0.6
)
do {
// 从事件创建模式
let pattern = try CHHapticPattern(
events: [tap1, tap2, tap3],
parameters: []
)
// 创建播放器
let player = try engine?.makePlayer(with: pattern)
// 播放
try player?.start(atTime: CHHapticTimeImmediate)
} catch {
print("Failed to play pattern: \(error)")
}
}CHHapticAdvancedPatternPlayer - Looping
CHHapticAdvancedPatternPlayer - 循环播放
For continuous feedback (rolling textures, motors), use advanced player:
swift
func startRollingTexture() {
let event = CHHapticEvent(
eventType: .hapticContinuous,
parameters: [
CHHapticEventParameter(parameterID: .hapticIntensity, value: 0.4),
CHHapticEventParameter(parameterID: .hapticSharpness, value: 0.2)
],
relativeTime: 0.0,
duration: 0.5
)
do {
let pattern = try CHHapticPattern(events: [event], parameters: [])
// Use advanced player for looping
let player = try engine?.makeAdvancedPlayer(with: pattern)
// Enable looping
try player?.loopEnabled = true
// Start
try player?.start(atTime: CHHapticTimeImmediate)
// Update intensity dynamically based on ball speed
updateTextureIntensity(player: player)
} catch {
print("Failed to start texture: \(error)")
}
}
func updateTextureIntensity(player: CHHapticAdvancedPatternPlayer?) {
let newIntensity = calculateIntensityFromBallSpeed()
let intensityParam = CHHapticDynamicParameter(
parameterID: .hapticIntensityControl,
value: newIntensity,
relativeTime: 0
)
try? player?.sendParameters([intensityParam], atTime: CHHapticTimeImmediate)
}Key difference: plays once, supports looping and dynamic parameter updates.
CHHapticPatternPlayerCHHapticAdvancedPatternPlayer对于持续反馈(滚动纹理、马达运行),使用高级播放器:
swift
func startRollingTexture() {
let event = CHHapticEvent(
eventType: .hapticContinuous,
parameters: [
CHHapticEventParameter(parameterID: .hapticIntensity, value: 0.4),
CHHapticEventParameter(parameterID: .hapticSharpness, value: 0.2)
],
relativeTime: 0.0,
duration: 0.5
)
do {
let pattern = try CHHapticPattern(events: [event], parameters: [])
// 使用高级播放器实现循环
let player = try engine?.makeAdvancedPlayer(with: pattern)
// 启用循环
try player?.loopEnabled = true
// 开始播放
try player?.start(atTime: CHHapticTimeImmediate)
// 根据球的速度动态更新强度
updateTextureIntensity(player: player)
} catch {
print("Failed to start texture: \(error)")
}
}
func updateTextureIntensity(player: CHHapticAdvancedPatternPlayer?) {
let newIntensity = calculateIntensityFromBallSpeed()
let intensityParam = CHHapticDynamicParameter(
parameterID: .hapticIntensityControl,
value: newIntensity,
relativeTime: 0
)
try? player?.sendParameters([intensityParam], atTime: CHHapticTimeImmediate)
}核心差异:仅播放一次,支持循环和动态参数更新。
CHHapticPatternPlayerCHHapticAdvancedPatternPlayerPart 4: AHAP Files (Apple Haptic Audio Pattern)
第四部分:AHAP文件(苹果触觉音频模式)
AHAP (Apple Haptic Audio Pattern) files are JSON files combining haptic events and audio.
AHAP(Apple Haptic Audio Pattern)文件是结合触觉事件和音频的JSON文件。
Basic AHAP Structure
基础AHAP结构
json
{
"Version": 1.0,
"Metadata": {
"Project": "My App",
"Created": "2024-01-15"
},
"Pattern": [
{
"Event": {
"Time": 0.0,
"EventType": "HapticTransient",
"EventParameters": [
{
"ParameterID": "HapticIntensity",
"ParameterValue": 1.0
},
{
"ParameterID": "HapticSharpness",
"ParameterValue": 0.5
}
]
}
}
]
}json
{
"Version": 1.0,
"Metadata": {
"Project": "My App",
"Created": "2024-01-15"
},
"Pattern": [
{
"Event": {
"Time": 0.0,
"EventType": "HapticTransient",
"EventParameters": [
{
"ParameterID": "HapticIntensity",
"ParameterValue": 1.0
},
{
"ParameterID": "HapticSharpness",
"ParameterValue": 0.5
}
]
}
}
]
}Adding Audio to AHAP
在AHAP中添加音频
json
{
"Version": 1.0,
"Pattern": [
{
"Event": {
"Time": 0.0,
"EventType": "AudioCustom",
"EventParameters": [
{
"ParameterID": "AudioVolume",
"ParameterValue": 0.8
}
],
"EventWaveformPath": "ShieldA.wav"
}
},
{
"Event": {
"Time": 0.0,
"EventType": "HapticContinuous",
"EventDuration": 0.5,
"EventParameters": [
{
"ParameterID": "HapticIntensity",
"ParameterValue": 0.6
}
]
}
}
]
}json
{
"Version": 1.0,
"Pattern": [
{
"Event": {
"Time": 0.0,
"EventType": "AudioCustom",
"EventParameters": [
{
"ParameterID": "AudioVolume",
"ParameterValue": 0.8
}
],
"EventWaveformPath": "ShieldA.wav"
}
},
{
"Event": {
"Time": 0.0,
"EventType": "HapticContinuous",
"EventDuration": 0.5,
"EventParameters": [
{
"ParameterID": "HapticIntensity",
"ParameterValue": 0.6
}
]
}
}
]
}Loading AHAP Files
加载AHAP文件
swift
func loadAHAPPattern(named name: String) -> CHHapticPattern? {
guard let url = Bundle.main.url(forResource: name, withExtension: "ahap") else {
print("AHAP file not found")
return nil
}
do {
return try CHHapticPattern(contentsOf: url)
} catch {
print("Failed to load AHAP: \(error)")
return nil
}
}
// Usage
if let pattern = loadAHAPPattern(named: "ShieldTransient") {
let player = try? engine?.makePlayer(with: pattern)
try? player?.start(atTime: CHHapticTimeImmediate)
}swift
func loadAHAPPattern(named name: String) -> CHHapticPattern? {
guard let url = Bundle.main.url(forResource: name, withExtension: "ahap") else {
print("AHAP file not found")
return nil
}
do {
return try CHHapticPattern(contentsOf: url)
} catch {
print("Failed to load AHAP: \(error)")
return nil
}
}
// 使用示例
if let pattern = loadAHAPPattern(named: "ShieldTransient") {
let player = try? engine?.makePlayer(with: pattern)
try? player?.start(atTime: CHHapticTimeImmediate)
}Design Workflow (WWDC Example)
设计流程(WWDC示例)
- Create visual animation (e.g., shield transformation, 500ms)
- Design audio (convey energy gain and robustness)
- Design haptic (feel the transformation)
- Test harmony - Do all three senses work together?
- Iterate - Swap AHAP assets until coherent
- Implement - Update code to use final assets
Example iteration: Shield initially used 3 transient pulses (haptic) + progressive continuous sound (audio) → no harmony. Solution: Switch to continuous haptic + ShieldA.wav audio → unified experience.
- 创建视觉动画(如护盾变形,时长500ms)
- 设计音频(传递能量提升和坚固感)
- 设计触觉(模拟变形的触感)
- 测试协调性 - 三种感官是否协同工作?
- 迭代优化 - 更换AHAP资源直到体验统一
- 实现集成 - 更新代码使用最终资源
示例迭代:护盾最初使用3次瞬时脉冲(触觉)+ 渐进式持续声音(音频)→ 缺乏协调性。解决方案:切换为持续触觉 + ShieldA.wav音频 → 形成统一体验。
Part 5: Audio-Haptic Synchronization
第五部分:音触觉同步
Matching Animation Timing
匹配动画时机
swift
class ViewController: UIViewController {
let animationDuration: TimeInterval = 0.5
func performShieldTransformation() {
// Start haptic/audio simultaneously with animation
playShieldPattern()
UIView.animate(withDuration: animationDuration) {
self.shieldView.transform = CGAffineTransform(scaleX: 1.2, y: 1.2)
self.shieldView.alpha = 0.8
}
}
func playShieldPattern() {
if let pattern = loadAHAPPattern(named: "ShieldContinuous") {
let player = try? engine?.makePlayer(with: pattern)
try? player?.start(atTime: CHHapticTimeImmediate)
}
}
}Critical: Fire haptic at the exact moment the visual change occurs, not before or after.
swift
class ViewController: UIViewController {
let animationDuration: TimeInterval = 0.5
func performShieldTransformation() {
// 与动画同时启动触觉/音频
playShieldPattern()
UIView.animate(withDuration: animationDuration) {
self.shieldView.transform = CGAffineTransform(scaleX: 1.2, y: 1.2)
self.shieldView.alpha = 0.8
}
}
func playShieldPattern() {
if let pattern = loadAHAPPattern(named: "ShieldContinuous") {
let player = try? engine?.makePlayer(with: pattern)
try? player?.start(atTime: CHHapticTimeImmediate)
}
}
}关键提示:在视觉变化发生的精确时刻触发触觉,不要提前或延迟。
Coordinating with Audio
与音频协同
swift
import AVFoundation
class AudioHapticCoordinator {
let audioPlayer: AVAudioPlayer
let hapticEngine: CHHapticEngine
func playCoordinatedExperience() {
// Prepare both systems
hapticEngine.notifyWhenPlayersFinished { _ in
return .stopEngine
}
// Start at exact same moment
let startTime = CACurrentMediaTime() + 0.05 // Small delay for sync
// Start audio
audioPlayer.play(atTime: startTime)
// Start haptic
if let pattern = loadAHAPPattern(named: "CoordinatedPattern") {
let player = try? hapticEngine.makePlayer(with: pattern)
try? player?.start(atTime: CHHapticTimeImmediate)
}
}
}swift
import AVFoundation
class AudioHapticCoordinator {
let audioPlayer: AVAudioPlayer
let hapticEngine: CHHapticEngine
func playCoordinatedExperience() {
// 准备两个系统
hapticEngine.notifyWhenPlayersFinished { _ in
return .stopEngine
}
// 同时启动
let startTime = CACurrentMediaTime() + 0.05 // 微小延迟以确保同步
// 启动音频
audioPlayer.play(atTime: startTime)
// 启动触觉
if let pattern = loadAHAPPattern(named: "CoordinatedPattern") {
let player = try? hapticEngine.makePlayer(with: pattern)
try? player?.start(atTime: CHHapticTimeImmediate)
}
}
}Part 6: Common Patterns
第六部分:常见模式
Button Tap
按钮点击
swift
class HapticButton: UIButton {
let impactGenerator = UIImpactFeedbackGenerator(style: .medium)
override func touchesBegan(_ touches: Set<UITouch>, with event: UIEvent?) {
super.touchesBegan(touches, with: event)
impactGenerator.prepare()
}
override func touchesEnded(_ touches: Set<UITouch>, with event: UIEvent?) {
super.touchesEnded(touches, with: event)
impactGenerator.impactOccurred()
}
}swift
class HapticButton: UIButton {
let impactGenerator = UIImpactFeedbackGenerator(style: .medium)
override func touchesBegan(_ touches: Set<UITouch>, with event: UIEvent?) {
super.touchesBegan(touches, with: event)
impactGenerator.prepare()
}
override func touchesEnded(_ touches: Set<UITouch>, with event: UIEvent?) {
super.touchesEnded(touches, with: event)
impactGenerator.impactOccurred()
}
}Slider Scrubbing
滑块拖动
swift
class HapticSlider: UISlider {
let selectionGenerator = UISelectionFeedbackGenerator()
var lastValue: Float = 0
@objc func valueChanged() {
let threshold: Float = 0.1
if abs(value - lastValue) >= threshold {
selectionGenerator.selectionChanged()
lastValue = value
}
}
}swift
class HapticSlider: UISlider {
let selectionGenerator = UISelectionFeedbackGenerator()
var lastValue: Float = 0
@objc func valueChanged() {
let threshold: Float = 0.1
if abs(value - lastValue) >= threshold {
selectionGenerator.selectionChanged()
lastValue = value
}
}
}Pull-to-Refresh
下拉刷新
swift
class PullToRefreshController: UIViewController {
let impactGenerator = UIImpactFeedbackGenerator(style: .medium)
var isRefreshing = false
func scrollViewDidScroll(_ scrollView: UIScrollView) {
let threshold: CGFloat = -100
let offset = scrollView.contentOffset.y
if offset <= threshold && !isRefreshing {
impactGenerator.impactOccurred()
isRefreshing = true
beginRefresh()
}
}
}swift
class PullToRefreshController: UIViewController {
let impactGenerator = UIImpactFeedbackGenerator(style: .medium)
var isRefreshing = false
func scrollViewDidScroll(_ scrollView: UIScrollView) {
let threshold: CGFloat = -100
let offset = scrollView.contentOffset.y
if offset <= threshold && !isRefreshing {
impactGenerator.impactOccurred()
isRefreshing = true
beginRefresh()
}
}
}Success/Error Feedback
成功/错误反馈
swift
func handleServerResponse(_ result: Result<Data, Error>) {
let notificationGenerator = UINotificationFeedbackGenerator()
switch result {
case .success:
notificationGenerator.notificationOccurred(.success)
showSuccessMessage()
case .failure:
notificationGenerator.notificationOccurred(.error)
showErrorAlert()
}
}swift
func handleServerResponse(_ result: Result<Data, Error>) {
let notificationGenerator = UINotificationFeedbackGenerator()
switch result {
case .success:
notificationGenerator.notificationOccurred(.success)
showSuccessMessage()
case .failure:
notificationGenerator.notificationOccurred(.error)
showErrorAlert()
}
}Part 7: Testing & Debugging
第七部分:测试与调试
Simulator Limitations
模拟器限制
Haptics DO NOT work in Simulator. You will see:
- No haptic feedback
- No warnings or errors
- Code runs normally
Solution: Always test on physical device (iPhone 8 or newer).
触觉反馈在模拟器中无法工作。你会看到:
- 无触觉反馈
- 无警告或错误
- 代码正常运行
解决方案:始终在真机上测试(iPhone 8或更新机型)。
Device Testing Checklist
真机测试清单
- Test with Haptics disabled in Settings → Sounds & Haptics
- Test with Low Power Mode enabled
- Test during incoming call (engine may stop)
- Test with audio playing in background
- Test with different intensity/sharpness values
- Verify battery impact (Instruments Energy Log)
- 在“设置”→“声音与触觉”中关闭触觉后测试
- 开启低电量模式后测试
- 来电时测试(引擎可能停止)
- 后台播放音频时测试
- 测试不同强度/锐利度的值
- 验证电池消耗(使用Instruments能源日志)
Debug Logging
调试日志
swift
func playHaptic() {
#if DEBUG
print("🔔 Playing haptic - Engine running: \(engine?.currentTime ?? -1)")
#endif
do {
let player = try engine?.makePlayer(with: pattern)
try player?.start(atTime: CHHapticTimeImmediate)
#if DEBUG
print("✅ Haptic started successfully")
#endif
} catch {
#if DEBUG
print("❌ Haptic failed: \(error.localizedDescription)")
#endif
}
}swift
func playHaptic() {
#if DEBUG
print("🔔 Playing haptic - Engine running: \(engine?.currentTime ?? -1)")
#endif
do {
let player = try engine?.makePlayer(with: pattern)
try player?.start(atTime: CHHapticTimeImmediate)
#if DEBUG
print("✅ Haptic started successfully")
#endif
} catch {
#if DEBUG
print("❌ Haptic failed: \(error.localizedDescription)")
#endif
}
}Troubleshooting
故障排除
Engine fails to start
引擎启动失败
Symptom: throws error
CHHapticEngine.start()Causes:
- Device doesn't support Core Haptics (< iPhone 8)
- Haptics disabled in Settings
- Low Power Mode enabled
Solution:
swift
func safelyStartEngine() {
guard CHHapticEngine.capabilitiesForHardware().supportsHaptics else {
print("Device doesn't support haptics")
return
}
do {
try engine?.start()
} catch {
print("Engine start failed: \(error)")
// Fall back to UIFeedbackGenerator
useFallbackHaptics()
}
}症状:抛出错误
CHHapticEngine.start()原因:
- 设备不支持Core Haptics(早于iPhone 8)
- 设置中关闭了触觉
- 开启了低电量模式
解决方案:
swift
func safelyStartEngine() {
guard CHHapticEngine.capabilitiesForHardware().supportsHaptics else {
print("Device doesn't support haptics")
return
}
do {
try engine?.start()
} catch {
print("Engine start failed: \(error)")
// 回退到UIFeedbackGenerator
useFallbackHaptics()
}
}Haptics not felt
无触觉反馈
Symptom: Code runs but no haptic felt on device
Debug steps:
- Check Settings → Sounds & Haptics → System Haptics is ON
- Check Low Power Mode is OFF
- Verify device is iPhone 8 or newer
- Check intensity > 0.3 (values below may be too subtle)
- Test with UIFeedbackGenerator to isolate Core Haptics vs system issue
症状:代码正常运行但真机上无触觉
调试步骤:
- 检查“设置”→“声音与触觉”→“系统触觉”是否开启
- 检查是否开启了低电量模式
- 验证设备是否为iPhone 8或更新机型
- 检查强度是否大于0.3(低于此值可能过于微弱)
- 使用UIFeedbackGenerator测试以排查是Core Haptics问题还是系统问题
Audio out of sync with haptics
音频与触觉不同步
Symptom: Audio plays but haptic delayed or vice versa
Causes:
- Not calling before haptic
prepare() - Audio/haptic started at different times
- Heavy main thread work blocking playback
Solution:
swift
// ✅ Synchronized start
func playCoordinated() {
impactGenerator.prepare() // Reduce latency
// Start both simultaneously
audioPlayer.play()
impactGenerator.impactOccurred()
}症状:音频播放但触觉延迟,反之亦然
原因:
- 触发触觉前未调用
prepare() - 音频/触觉启动时间不同
- 主线程负载过重阻塞播放
解决方案:
swift
// ✅ 同步启动
func playCoordinated() {
impactGenerator.prepare() // 减少延迟
// 同时启动两者
audioPlayer.play()
impactGenerator.impactOccurred()
}Audio file errors with AHAP
AHAP音频文件错误
Symptom: AHAP pattern fails to load or play
Cause: Audio file > 4.2 MB or > 23 seconds
Solution: Keep audio files small and short. Use compressed formats (AAC) and trim to essential duration.
症状:AHAP模式加载或播放失败
原因:音频文件大于4.2MB或时长超过23秒
解决方案:保持音频文件短小。使用压缩格式(AAC)并裁剪到必要时长。
Resources
资源
WWDC: 2021-10278, 2019-520, 2019-223
Docs: /corehaptics, /corehaptics/chhapticengine
Skills: axiom-swiftui-animation-ref, axiom-ui-testing, axiom-accessibility-diag
WWDC:2021-10278, 2019-520, 2019-223
文档:/corehaptics, /corehaptics/chhapticengine
相关技能:axiom-swiftui-animation-ref, axiom-ui-testing, axiom-accessibility-diag