关于 axiom-haptics
axiom-haptics 是面向 iOS 开发者的触觉与音频反馈实现指南,涵盖 UIFeedbackGenerator、Core Haptics 和 AHAP。内容还包括反馈设计原则、与动画及音频同步,以及真机测试和调试。
主要功能
- 01使用 UIFeedbackGenerator 提供基础触觉反馈
- 02通过 Core Haptics 创建自定义模式
- 03加载 AHAP 触觉与音频模式
- 04同步触觉、音频和动画
- 05诊断设备限制并测试触觉
适用场景
- 01为按钮、选择器和表单结果添加触觉
- 02为滑块或持续动作设计反馈
- 03制作与动画同步的触觉和音频效果
- 04在实体设备上排查触觉问题
安装
复制下面的命令,在终端中运行。
npx skills add https:/ / github.com/ charleswiltgen/ axiom --skill axiom-haptics安装后在 Claude Code 中输入/axiom-haptics确认已加载。
详细介绍
触觉与音频反馈
iOS 触觉反馈实现综合指南。每款 Apple 设计大奖获奖应用都采用了出色的触觉反馈——相机、地图和天气应用都巧妙运用触觉,打造令人愉悦、响应灵敏的体验。
概述
触觉反馈为用户操作和系统事件提供触觉确认。使用因果性、和谐性与实用性框架进行周密设计时,axiom-haptics 能让界面从实用变得令人愉悦。
本技能涵盖简单触觉反馈(UIFeedbackGenerator)和高级自定义模式(Core Haptics),并提供真实场景示例和音频与触觉同步技巧。
何时使用本技能
- 为用户交互添加触觉反馈
- 在 UIFeedbackGenerator 和 Core Haptics 之间进行选择
- 设计协调统一的音频与触觉体验
- 使用 AHAP 文件创建自定义触觉模式
- 将触觉反馈与动画和音频同步
- 调试触觉问题(模拟器与设备的差异)
- 优化触觉性能和电池影响
系统要求
- iOS 10+,适用于 UIFeedbackGenerator
- iOS 13+,适用于 Core Haptics(CHHapticEngine)
- iPhone 8+,支持 Core Haptics 硬件
- 需要实体设备——在 Simulator 中无法感受到触觉反馈
第 1 部分:设计原则(WWDC 2021/10278)
Apple 的音频和触觉设计团队确立了多模态反馈的三项核心原则:
因果性——让用户清楚反馈由什么触发
问题:用户无法判断是什么触发了触觉反馈 解决方案:触觉反馈的时机必须与视觉或交互发生的时刻一致
WWDC 示例:
- ✅ 球撞上墙壁 → 在碰撞时刻触发触觉反馈
- ❌ 球撞上墙壁 → 100ms 后才触发触觉反馈(令人困惑)
代码模式:
// ✅ 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 示例:
- 小球 → 轻柔的触觉反馈 + 高音调声音
- 大球 → 强烈的触觉反馈 + 低音调声音
- 护盾变形 → 连续的触觉反馈 + 渐进式音频
关键见解:大型物体在触感上应显得沉重,在声音上应低沉而浑厚,在视觉上应显得厚重。三种感官共同强化同一种体验。
实用性——提供明确价值
问题:因为“能做到”,就在所有地方都使用触觉反馈 解决方案:将触觉反馈留给能为用户带来益处的重要时刻
适合使用触觉反馈的场景:
- ✅ 确认重要操作(支付完成)
- ✅ 提醒用户注意关键事件(电量低)
- ✅ 提供持续反馈(滑动滑块)
- ✅ 增添愉悦感(应用启动时的点缀效果)
不适合使用触觉反馈的场景:
- ❌ 每一次轻点(令人不堪重负)
- ❌ 浏览长列表时(消耗电量)
- ❌ 用户无法看到的后台事件(令人困惑)
- ❌ 装饰性动画(没有价值)
第 2 部分:UIFeedbackGenerator(简单触觉反馈)
对于大多数应用,UIFeedbackGenerator 提供 3 种简单的触觉反馈类型,无需自定义模式。
UIImpactFeedbackGenerator
模拟物理碰撞或冲击的触感。
样式(从轻到重排列):
.light- 轻微、细腻的点按感.medium- 标准点按感(最常用).heavy- 强烈、扎实的冲击感.rigid- 坚实、精准的点按感.soft- 柔和、有缓冲的点按感
使用模式:
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()
}
}
强度变化(iOS 13+):
// intensity: 0.0 (lightest) to 1.0 (strongest)
impactGenerator.impactOccurred(intensity: 0.5)
常见使用场景:
- 按钮点按(
.medium) - 切换开关(
.light) - 删除项目(
.heavy) - 确认选择(
.rigid)
UISelectionFeedbackGenerator
用于离散的选项变更(选择器滚轮、分段控件)。
用法:
class PickerViewController: UIViewController {
let selectionGenerator = UISelectionFeedbackGenerator()
func pickerView(_ picker: UIPickerView, didSelectRow row: Int,
inComponent component: Int) {
selectionGenerator.selectionChanged()
}
}
触感类似于:带有卡位的实体滚轮发出的咔哒声
常见使用场景:
- 选择器滚轮
- 分段控件
- 页面指示器
- 逐步切换界面
UINotificationFeedbackGenerator
系统级成功/警告/错误反馈。
类型:
.success- 任务成功完成.warning- 需要注意,但并不严重.error- 发生严重错误
用法:
let notificationGenerator = UINotificationFeedbackGenerator()
func submitForm() {
// Validate form
if isValid {
notificationGenerator.notificationOccurred(.success)
saveData()
} else {
notificationGenerator.notificationOccurred(.error)
showValidationErrors()
}
}
最佳实践:根据用户操作结果匹配触觉反馈类型
- ✅ 支付成功 →
.success - ✅ 表单验证失败 →
.error - ✅ 存储空间即将达到上限 →
.warning
性能:prepare()
在触觉反馈前调用 prepare() 以降低延迟:
// ✅ 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() 后,系统会让引擎保持就绪约 1 秒。
第 3 部分:Core Haptics(自定义触觉反馈)
对于需要自定义模式的应用,Core Haptics 可全面控制触觉波形。
四个基本元素
- 引擎(
CHHapticEngine)- 连接到手机的执行器 - 播放器(
CHHapticPatternPlayer)- 控制播放 - 模式(
CHHapticPattern)- 随时间变化的一组事件 - 事件(
CHHapticEvent)- 定义体验的构建模块
CHHapticEngine 生命周期
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)")
}
}
}
重要:务必设置 stoppedHandler 和 resetHandler,以处理系统中断。
CHHapticEvent 类型
瞬态事件
短促、离散的反馈(如轻敲)。
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
)
参数:
hapticIntensity:强度(0.0 = 几乎感觉不到,1.0 = 最大)hapticSharpness:特征(0.0 = 沉闷的撞击声,1.0 = 清脆的弹响)
连续事件
持续一段时间的反馈(如振动马达)。
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
)
适用场景:
- 物体移动时的滚动质感
- 马达运行
- 充电进度
- 长按反馈
创建和播放模式
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)")
}
}
CHHapticAdvancedPatternPlayer - 循环播放
对于连续反馈(滚动质感、马达),请使用高级播放器:
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)
}
主要区别:CHHapticPatternPlayer 播放一次;CHHapticAdvancedPatternPlayer 支持循环播放和动态参数更新。
第 4 部分:AHAP 文件(Apple Haptic Audio Pattern)
AHAP(Apple Haptic Audio Pattern)文件是结合触觉事件和音频的 JSON 文件。
AHAP 基本结构
{
"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
}
]
}
}
]
}
向 AHAP 添加音频
{
"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
}
]
}
}
]
}
加载 AHAP 文件
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)
}
设计工作流(WWDC 示例)
- 创建视觉动画(例如盾牌变形,500ms)
- 设计音频(传达能量增强和坚固感)
- 设计触觉反馈(感受变形)
- 测试协调性 - 三种感官体验是否相互配合?
- 迭代 - 替换 AHAP 资源,直到效果协调一致
- 实现 - 更新代码以使用最终资源
迭代示例:盾牌最初使用 3 次瞬态脉冲(触觉反馈)+ 逐渐增强的连续声音(音频)→ 不协调。解决方案:改用连续触觉反馈 + ShieldA.wav 音频 → 形成统一体验。
第 5 部分:音频与触觉反馈同步
匹配动画时序
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)
}
}
}
重要:在视觉变化发生的确切时刻触发触觉反馈,而不是提前或延后。
协调音频
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)
}
}
}
第 6 部分:常见模式
按钮轻触
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()
}
}
滑块拖动
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
}
}
}
下拉刷新
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()
}
}
}
成功/错误反馈
func handleServerResponse(_ result: Result<Data, Error>) {
let notificationGenerator = UINotificationFeedbackGenerator()
switch result {
case .success:
notificationGenerator.notificationOccurred(.success)
showSuccessMessage()
case .failure:
notificationGenerator.notificationOccurred(.error)
showErrorAlert()
}
}
第 7 部分:测试与调试
模拟器限制
触觉反馈在模拟器中不起作用。你会看到:
- 没有触觉反馈
- 没有警告或错误
- 代码正常运行
解决方案:务必在实体设备上测试(iPhone 8 或更新机型)。
设备测试清单
- 在“设置”→“声音与触感”中关闭触感后进行测试
- 在启用低电量模式时进行测试
- 在来电期间进行测试(引擎可能会停止)
- 在后台播放音频时进行测试
- 使用不同的强度/锐度值进行测试
- 验证对电池的影响(Instruments 能耗日志)
调试日志
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
}
}
故障排除
引擎无法启动
症状:CHHapticEngine.start() 抛出错误
原因:
- 设备不支持 Core Haptics(早于 iPhone 8)
- “设置”中已关闭触感
- 已启用低电量模式
解决方案:
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()
}
}
未感受到触觉反馈
症状:代码正常运行,但设备上没有触觉反馈
调试步骤:
- 检查“设置” → “声音与触感” → “系统触感”是否已开启
- 检查是否已关闭低电量模式
- 确认设备为 iPhone 8 或更新机型
- 检查强度是否大于 0.3(低于此值可能过于微弱)
- 使用 UIFeedbackGenerator 进行测试,以区分 Core Haptics 问题与系统问题
音频与触觉反馈不同步
症状:音频播放正常,但触觉反馈延迟,或反之
原因:
- 触觉反馈前未调用
prepare() - 音频和触觉反馈的启动时间不同
- 主线程繁忙,阻塞播放
解决方案:
// ✅ Synchronized start
func playCoordinated() {
impactGenerator.prepare() // Reduce latency
// Start both simultaneously
audioPlayer.play()
impactGenerator.impactOccurred()
}
AHAP 的音频文件错误
症状:AHAP 模式无法加载或播放
原因:音频文件大于 4.2 MB 或时长超过 23 秒
解决方案:保持音频文件小巧、简短。使用压缩格式(AAC),并将音频裁剪至必要时长。
资源
WWDC:2021-10278、2019-520、2019-223
文档:/corehaptics、/corehaptics/chhapticengine
技能:axiom-swiftui-animation-ref、axiom-ui-testing、axiom-accessibility-diag