axiom-haptics

Compare original and translation side by side

🇺🇸

Original

English
🇨🇳

Translation

Chinese

Haptics & 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 (
UIFeedbackGenerator
) and advanced custom patterns (
Core Haptics
), with real-world examples and audio-haptic synchronization techniques.
触觉反馈为用户操作和系统事件提供触觉确认。当运用因果性-协调性-实用性框架进行精心设计时,触觉反馈能将界面从功能性提升至令人愉悦的体验。
本指南涵盖了简单触觉(
UIFeedbackGenerator
)和高级自定义模式(
Core Haptics
),并提供了真实场景示例和音触觉同步技术。

When 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,
UIFeedbackGenerator
provides 3 simple haptic types without custom patterns.
对于大多数应用,
UIFeedbackGenerator
提供3种无需自定义模式的简单触觉类型。

UIImpactFeedbackGenerator

UIImpactFeedbackGenerator

Physical collision or impact sensation.
Styles (ordered light → heavy):
  • .light
    - Small, delicate tap
  • .medium
    - Standard tap (most common)
  • .heavy
    - Strong, solid impact
  • .rigid
    - Firm, precise tap
  • .soft
    - Gentle, cushioned tap
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:
  • .success
    - Task completed successfully
  • .warning
    - Attention needed, but not critical
  • .error
    - Critical error occurred
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
prepare()
before the haptic to reduce latency:
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延迟
}
准备时机:调用
prepare()
后,系统会将引擎保持就绪状态约1秒。

Part 3: Core Haptics (Custom Haptics)

第三部分:Core Haptics(自定义触觉)

For apps needing custom patterns,
Core Haptics
provides full control over haptic waveforms.
对于需要自定义模式的应用,
Core Haptics
提供了对触觉波形的完全控制。

Four Fundamental Elements

四个基本元素

  1. Engine (
    CHHapticEngine
    ) - Link to the phone's actuator
  2. Player (
    CHHapticPatternPlayer
    ) - Playback control
  3. Pattern (
    CHHapticPattern
    ) - Collection of events over time
  4. Events (
    CHHapticEvent
    ) - Building blocks specifying the experience
  1. 引擎
    CHHapticEngine
    )- 与手机执行器的连接
  2. 播放器
    CHHapticPatternPlayer
    )- 播放控制
  3. 模式
    CHHapticPattern
    )- 随时间变化的事件集合
  4. 事件
    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
stoppedHandler
and
resetHandler
to handle system interruptions.
swift
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)")
        }
    }
}
关键提示:务必设置
stoppedHandler
resetHandler
以处理系统中断。

CHHapticEvent 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:
  • hapticIntensity
    : Strength (0.0 = barely felt, 1.0 = maximum)
  • hapticSharpness
    : Character (0.0 = dull thud, 1.0 = crisp snap)
短暂、离散的反馈(如点击)。
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  // 相对于模式开始的秒数
)
参数说明
  • hapticIntensity
    :强度(0.0 = 几乎感觉不到,1.0 = 最大强度)
  • hapticSharpness
    :触感特性(0.0 = 沉闷撞击,1.0 = 清脆点击)

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:
CHHapticPatternPlayer
plays once,
CHHapticAdvancedPatternPlayer
supports looping and dynamic parameter updates.

对于持续反馈(滚动纹理、马达运行),使用高级播放器:
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)
}
核心差异
CHHapticPatternPlayer
仅播放一次,
CHHapticAdvancedPatternPlayer
支持循环和动态参数更新。

Part 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示例)

  1. Create visual animation (e.g., shield transformation, 500ms)
  2. Design audio (convey energy gain and robustness)
  3. Design haptic (feel the transformation)
  4. Test harmony - Do all three senses work together?
  5. Iterate - Swap AHAP assets until coherent
  6. 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.

  1. 创建视觉动画(如护盾变形,时长500ms)
  2. 设计音频(传递能量提升和坚固感)
  3. 设计触觉(模拟变形的触感)
  4. 测试协调性 - 三种感官是否协同工作?
  5. 迭代优化 - 更换AHAP资源直到体验统一
  6. 实现集成 - 更新代码使用最终资源
示例迭代:护盾最初使用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:
CHHapticEngine.start()
throws error
Causes:
  1. Device doesn't support Core Haptics (< iPhone 8)
  2. Haptics disabled in Settings
  3. 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()
抛出错误
原因
  1. 设备不支持Core Haptics(早于iPhone 8)
  2. 设置中关闭了触觉
  3. 开启了低电量模式
解决方案
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:
  1. Check Settings → Sounds & Haptics → System Haptics is ON
  2. Check Low Power Mode is OFF
  3. Verify device is iPhone 8 or newer
  4. Check intensity > 0.3 (values below may be too subtle)
  5. Test with UIFeedbackGenerator to isolate Core Haptics vs system issue
症状:代码正常运行但真机上无触觉
调试步骤
  1. 检查“设置”→“声音与触觉”→“系统触觉”是否开启
  2. 检查是否开启了低电量模式
  3. 验证设备是否为iPhone 8或更新机型
  4. 检查强度是否大于0.3(低于此值可能过于微弱)
  5. 使用UIFeedbackGenerator测试以排查是Core Haptics问题还是系统问题

Audio out of sync with haptics

音频与触觉不同步

Symptom: Audio plays but haptic delayed or vice versa
Causes:
  1. Not calling
    prepare()
    before haptic
  2. Audio/haptic started at different times
  3. Heavy main thread work blocking playback
Solution:
swift
// ✅ Synchronized start
func playCoordinated() {
    impactGenerator.prepare()  // Reduce latency

    // Start both simultaneously
    audioPlayer.play()
    impactGenerator.impactOccurred()
}
症状:音频播放但触觉延迟,反之亦然
原因
  1. 触发触觉前未调用
    prepare()
  2. 音频/触觉启动时间不同
  3. 主线程负载过重阻塞播放
解决方案
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