An independent review of the audit's own fixes found defects the fixes had introduced or left behind, and the hand-written docs pages — which restate many of the same claims and are not generated from the skills — had not followed the corrections. Fixes that were wrong or incomplete: - The paused-player elapsed fix was correct only where it was measured. `playerTime(forNodeTime:)` returns nil whenever the player is not playing — paused, stopped, or never started — and falling back to the lock screen's dictionary republished the *previous item's* elapsed on a track change. It now keeps an app-owned value, reset when a new item loads. - "Other request types, hand-pose among them, still succeed" is false: measured across eleven Vision requests, only hand-pose and text recognition run. Barcodes, animal, body pose, human rectangles, saliency and feature print all fail to build an inference context, and an order-reversed control on a second device showed the failure is request-specific, not context exhaustion. - `teardownCommands()` was left defined and called from nowhere. Two `isEnabled = true` writes contradicted the file's own rule that registering a target enables a command by default. The Bluetooth option legend that produced a recording-recipe straddle still omitted that A2DP is output-only routing. - Two citations pointed at the wrong guide page, one range omitted the page holding its section's last step, and one quoted a sentence the guide does not contain. The docs sweep found fifteen stale claims across the pages mirroring this suite, including the automatic-passthrough mechanism the audit disproved, a fabricated `prepare()` timing figure, and a fabricated set of diagnostic percentages. `photo-library`'s PNG/JPEG/HEIC split is now measured rather than asserted: `Image.importedContentTypes()` and `exportedContentTypes()` are both `["public.jpeg", "public.png"]`, with HEIC and HEIF absent. Cursor, Codex, inlined-auditor and MCP distributions regenerated. Verified: npm test (static validation clean).
5.9 KiB
name, description, skill_type, version
| name | description | skill_type | version |
|---|---|---|---|
| haptics | Use when implementing haptic feedback, Core Haptics patterns, audio-haptic synchronization, or debugging haptic issues - covers UIFeedbackGenerator, CHHapticEngine, AHAP patterns, and Apple's Causality-Harmony-Utility design principles | reference | 1.0 |
Haptics & Audio Feedback
Comprehensive guide to implementing haptic feedback on iOS with Core Haptics and UIFeedbackGenerator. Based on WWDC 2021 session 10278 (Practice audio haptic design).
Overview
This reference covers haptic feedback implementation from simple patterns to advanced audio-haptic synchronization:
- Design Principles – Causality, Harmony, Utility framework from WWDC 2021
- UIFeedbackGenerator – Simple haptic feedback for common interactions (iOS 10+)
- Core Haptics – Custom haptic patterns and audio-haptic synchronization (iOS 13+)
- AHAP Files – Apple Haptic Audio Pattern JSON format
- Testing & Debugging – Simulator limitations and device-specific behavior
System Requirements
- iOS 10+ for UIFeedbackGenerator (basic haptics)
- iOS 13+ for Core Haptics (CHHapticEngine)
- iPhone 8+ for Core Haptics hardware support
Simulator Limitation: Haptics only work on physical devices. Always test on hardware.
Design Principles (WWDC 2021/10278)
Causality — What caused the feedback?
Haptic feedback should have a clear relationship to what triggered it.
Good: Button tap generates haptic when finger touches screen Poor: Haptic triggered half a second after interaction
Harmony — Senses work together
Combine visual, audio, and haptic feedback that reinforce each other.
Good: Sound + haptic for success confirmation Poor: Haptic alone with no visual/audio context
Utility — Provide clear value
Haptics should enhance understanding or provide information the user needs.
Good: Different haptic patterns for success vs error Poor: Same haptic for everything
Quick Start
Simple Haptics with UIFeedbackGenerator
For basic interactions, use UIFeedbackGenerator:
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()
}
}
Custom Haptics with Core Haptics
For complex patterns and audio-haptic synchronization:
import CoreHaptics
var engine: CHHapticEngine?
func initializeHaptics() {
guard CHHapticEngine.capabilitiesForHardware().supportsHaptics else {
return
}
do {
engine = try CHHapticEngine()
try engine?.start()
} catch {
print("Failed to create haptic engine: \(error)")
}
}
Common Patterns
Button Tap
let impactGenerator = UIImpactFeedbackGenerator(style: .medium)
impactGenerator.impactOccurred()
Selection Change (Picker, Segmented Control)
let selectionGenerator = UISelectionFeedbackGenerator()
selectionGenerator.selectionChanged()
Success/Error/Warning
let notificationGenerator = UINotificationFeedbackGenerator()
notificationGenerator.notificationOccurred(.success) // or .error, .warning
When to Use Core Haptics
Use Core Haptics when you need:
- Custom haptic patterns beyond basic impact/selection/notification
- Audio-haptic synchronization for games or creative apps
- Looping haptic patterns for continuous feedback
- Fine control over intensity, sharpness, and timing
Otherwise, stick with UIFeedbackGenerator for simplicity.
Troubleshooting
Haptics not working
Check: Are you testing on a physical device (iPhone 8+)?
- Simulator doesn't support haptics
- Some older devices don't have Taptic Engine
Check: Is the ringer/silent switch on?
- Device must not be in silent mode (check Settings → Sounds & Haptics → System Haptics)
Engine fails to start
Solution: Handle engine stopped/reset events:
engine?.stoppedHandler = { reason in
switch reason {
case .audioSessionInterrupt, .applicationSuspended:
// The cause is still in effect, so a restart would fail —
// restart with the next user-initiated playback instead
print("Waiting for user-initiated playback to restart")
case .idleTimeout, .systemError:
self.restartEngine()
default:
break
}
}
engine?.resetHandler = {
print("Engine reset")
self.restartEngine()
}
Haptics feel weak or inconsistent
Check: Did you call prepare() before triggering?
- Call
prepare()shortly before the expected use — the Taptic Engine stays prepared for a short period (typically seconds), so call it again if more feedback is imminent - Reduces latency and ensures consistent response
WWDC Sessions
- Practice audio haptic design (2021/10278) – Causality-Harmony-Utility principles
- Introducing Core Haptics (2019/520) – CHHapticEngine architecture
- Expanding the Sensory Experience (2019/223) – Audio-haptic design
Related Skills
- haptics – Complete reference with AHAP patterns, advanced Core Haptics, audio synchronization