iOS developers
Syncing haptics with audio and animation
Put sound and touch on a shared timeline where possible, prepare playback early, and test the visual alignment on a real device.
Choose the moment that matters
Start by naming the event you want someone to perceive: a ball hits the floor, a latch closes, a countdown reaches zero. Put a marker at that event in the sound and the animation. “Start everything when the button is pressed” is insufficient if the sound’s impact occurs partway through the file.
Keep three times distinct: the app’s decision to begin, the scheduled event time, and the moment the output reaches the person. A resource load can delay the first. A badly chosen offset can misplace the second. Output hardware can affect the third.
Use one engine for authored sound and touch
If the sound belongs to the effect, putting it in the same Core Haptics pattern gives the audio and tactile events a shared timeline. Apple’s collision example uses this approach. It removes the need to coordinate two independent player start calls. Collision-based haptic patterns
This factory registers your audio file and puts a tactile impact at an offset you supply. For a sound whose impact begins 0.12 seconds into the recording, pass 0.12. Use a local audio file supported by Core Haptics.
import Foundation
import CoreHaptics
func makeImpactCue(
engine: CHHapticEngine,
audioURL: URL,
impactOffset: TimeInterval
) throws -> any CHHapticPatternPlayer {
let audioID = try engine.registerAudioResource(audioURL, options: [:])
let audio = CHHapticEvent(
audioResourceID: audioID,
parameters: [],
relativeTime: 0
)
let impact = CHHapticEvent(
eventType: .hapticTransient,
parameters: [
CHHapticEventParameter(parameterID: .hapticIntensity, value: 0.7),
CHHapticEventParameter(parameterID: .hapticSharpness, value: 0.6)
],
relativeTime: max(0, impactOffset)
)
let pattern = try CHHapticPattern(events: [audio, impact], parameters: [])
return try engine.makePlayer(with: pattern)
}
Call this while preparing the effect, retain the returned player and its engine, then start the player when appropriate. The function is a resource-preparation example, not a complete playback service. Your service still owns audio-session configuration, engine startup, interruption handling and cleanup. Reuse the prepared resources; do not register a fresh copy of the sound for every collision. Registering custom audio
An AHAP file can also reference bundled audio. Its waveform path is relative to the app bundle’s root; do not assume it resolves beside any AHAP you downloaded. AHAP audio paths
Separate players have separate clocks
A music or video app may already have an audio playback system. In that case, do not pass its current time directly to a Core Haptics player. Apple explicitly says CHHapticEngine.currentTime does not correlate with the clocks used by other media playback frameworks. Engine time
Define an app-level event timeline and track how each subsystem starts, pauses and seeks relative to it. Two adjacent “play now” calls are a useful prototype, not a guarantee of synchronized output. Measure the result with the output routes you support before adding compensation. A fixed offset that helps one route may hurt another.
When Core Haptics should produce only touch, configure playsHapticsOnly before starting the engine. Changing it while the engine runs takes effect only after a stop and restart. Haptics-only playback
Prepare before the critical frame
Load the file, construct the pattern and create the player ahead of the interaction when latency matters. Starting a stopped engine inside playPattern(from:) can delay playback. The synchronous start() also blocks its calling thread while startup completes, so it is not work to hide inside a rendering callback. File playback, engine startup
For animation, drive both effects from the same semantic event. A scripted sequence can share authored offsets. An interactive collision should trigger from the collision itself, rather than an earlier prediction that can become wrong when the person changes direction.
Do not use a UI timer as evidence of precise output timing. First review normal-speed playback on the device, then inspect a recording if you need to locate a visible or audible mismatch. A screen recording can help diagnose scheduling; it cannot record what the person felt.
Treat pause and reset as part of the design
If the person seeks to the middle of a clip, decide whether a sustained effect should resume and whether earlier impacts must be skipped. The advanced pattern player supplies seek, pause and resume operations; the rest of your timeline must follow the same decision. Advanced playback
After a media-server reset, recreate players and register custom audio again. After an interruption, wait until playback is appropriate; do not automatically resume a cue for a completed action. Engine recovery
Check a small matrix
| Test | What to look for |
|---|---|
| First play and repeated play | A cold-start delay or duplicated cue |
| Sound on, then off | Touch still corresponds to the visible action |
| Pause and resume | All parts resume from the intended position |
| Seek past an impact | The old impact does not fire late |
| Background and interruption | No stale playback on return |
| Each supported audio route | Alignment remains acceptable for that route |
Record the device, OS and output route with your findings. The goal is a coherent interaction you have actually checked, not an unmeasured latency promise.
More to read
Design haptics in Haptrix
Free, for Mac, iPad and iPhone: the editor, export and publishing included.
New articles by email
Email updates are not switched on yet.