Getting Started With Core Haptics
In this Core Haptics tutorial, you’ll learn how to create and play haptic patterns, synchronize audio with haptic events and create dynamic haptic patterns that respond to external stimuli. By Andrew Tetlaw.
Sign up/Sign in
With a free Kodeco account you can download source code, track your progress, bookmark, personalise your learner profile and more!
Create accountAlready a member of Kodeco? Sign in
Sign up/Sign in
With a free Kodeco account you can download source code, track your progress, bookmark, personalise your learner profile and more!
Create accountAlready a member of Kodeco? Sign in
Sign up/Sign in
With a free Kodeco account you can download source code, track your progress, bookmark, personalise your learner profile and more!
Create accountAlready a member of Kodeco? Sign in
Contents
Getting Started With Core Haptics
35 mins
- Getting Started
- Adding Your First Haptic Experience
- Exploring the Events That Make up the Pattern
- Managing Energy Usage
- Designing a Haptic Experience
- Feeding the Crocodile
- Playing Different Patterns
- Syncing Audio Events
- Setting a Reset Handler
- Ramping Intensity Up and Down — Pineapple Splashdown
- Controlling Intensity With a Parameter Curve
- Updating Pattern Parameters in Real Time
- Making the Player Dynamic
- Where to Go From Here?
Managing Energy Usage
Core Haptics uses the Taptic Engine hardware in your iOS device. Like all hardware components on an iOS device, you’ll want to activate it only when necessary to avoid wasting energy. When to run the engine depends on the needs of your app, but here are some guidelines:
- If your pattern plays a single transient event immediately, call the engine
start()
, play the pattern then callstop(completionHandler:)
. This is the most energy-efficient way, as described in Apple’s haptic pattern documentation. - If you’re playing a longer or more complex pattern, call
notifyWhenPlayersFinished(finishedHandler:)
and return.stopEngine
, as you’ve done already. - However, if your app plays multiple patterns in succession, or that overlap, as Snip The Vine does, you need the engine to always run.
Now, you’ll find out how to make sure the engine keeps running.
First, remove the following call to notifyWhenPlayersFinished(finishedHandler:)
from playSlice()
:
hapticEngine.notifyWhenPlayersFinished { error in
return .stopEngine
}
Next, add this to the end of init?()
:
do {
try hapticEngine.start()
} catch let error {
print("Haptic failed to start Error: \(error)")
}
This makes sure the haptic engine is ready when the game scene loads.
Still in init?()
, add the following line to the end:
hapticEngine.isAutoShutdownEnabled = true
This enables the automatic idle shutdown of the haptic engine. That’s responsible energy management on your part. However, this means iOS can shut the haptic engine down at any time, and you can’t assume it will still be running when you need it.
Designing a Haptic Experience
Why did you use those specific events, properties and timings? How did the experience feel to you? Did you get an impression of a blade slicing through the air and a sharp *snip*? These are the questions you need to ask when you design a haptic experience for your users.
A well-designed haptic experience enhances an app in subtle ways; a poorly-designed experience annoys and distracts.
A well-designed haptic experience also takes time. You need to test many subtle variations, but it’s a hoot to be able to say to friends that you just spent the afternoon designing the perfect haptic experience for a crocodile eating a pineapple. :]
Every budding haptic designer should read the Apple Human Interface Guidelines page on Haptics. The section on Designing with Haptics has a great list of design tips.
If you need something a little different, UIFeedbackGenerator
is available for notification, impact or selection effects.
If you truly need to get creative, then Core Haptics is there for you. But the jump in complexity from UIFeedbackGenerator
to Core Haptics is large.
If you need something a little different, UIFeedbackGenerator
is available for notification, impact or selection effects.
If you truly need to get creative, then Core Haptics is there for you. But the jump in complexity from UIFeedbackGenerator
to Core Haptics is large.
Now, it’s time to look at how to get the perfect sensation when the croc bites into that pineapple!
Feeding the Crocodile
So how do you start designing a haptic experience?
Build and run and take a look at your crocodile, happily munching his pineapple as it falls.
There are some cues you can draw from: sound and animation. Look in Resources/Sounds and open NomNom.caf in GarageBand or your audio editing app of choice. You can see the waveform of that sound effect:
The waveform has two distinct high points: An initial *crunch*, then a smaller *munch* at the end.
Open Haptics.swift and add a new method after slicePattern()
:
private func nomNomPattern() throws -> CHHapticPattern {
let rumble1 = CHHapticEvent(
eventType: .hapticContinuous,
parameters: [
CHHapticEventParameter(parameterID: .hapticIntensity, value: 1.0),
CHHapticEventParameter(parameterID: .hapticSharpness, value: 0.3)
],
relativeTime: 0,
duration: 0.15)
let rumble2 = CHHapticEvent(
eventType: .hapticContinuous,
parameters: [
CHHapticEventParameter(parameterID: .hapticIntensity, value: 0.4),
CHHapticEventParameter(parameterID: .hapticSharpness, value: 0.1)
],
relativeTime: 0.3,
duration: 0.3)
return try CHHapticPattern(events: [rumble1, rumble2], parameters: [])
}
This is the start of your crocodile’s *nom-nom* pattern — two continuous events to correspond to the two sounds in the nom nom sound effect.
Playing Different Patterns
Before playing your new pattern, first write a generic pattern-playing method to avoid code duplication. In Haptics.swift, add the following method above playHapticSlice()
:
private func playHapticFromPattern(_ pattern: CHHapticPattern) throws {
try hapticEngine.start()
let player = try hapticEngine.makePlayer(with: pattern)
try player.start(atTime: CHHapticTimeImmediate)
}
This method will play any pattern you pass to it. It still must try to start the haptic engine, because iOS may shut the engine down at any time. So, you can now simplify playSlice()
:
func playSlice() {
do {
let pattern = try slicePattern()
try playHapticFromPattern(pattern)
} catch {
print("Failed to play slice: \(error)")
}
}
You can also add a method to play your new effect under playSlice()
:
func playNomNom() {
do {
let pattern = try nomNomPattern()
try playHapticFromPattern(pattern)
} catch {
print("Failed to play nomNom: \(error)")
}
}
In GameScene.swift, find didBegin(_:)
, which runs nomNomSoundAction
, and add this line above it:
hapticManager?.playNomNom()
Build and run. Of course, you’ll need enough skill to feed the crocodile to test your new effect. :]
Look at runNomNomAnimation(withDelay:)
in GameScene.swift. It’s called with a value of 0.15
when the croc catches his treat. The animation runs like this:
- Close mouth.
- Wait 0.15 seconds.
- Open mouth.
- Wait 0.15 seconds.
- Close mouth.
It’d be great to add a couple of strong beats to match those snapping jaws. To do this, add two more events to nomNomPattern()
. Replace the line return try CHHapticPattern ...
with:
let crunch1 = CHHapticEvent(
eventType: .hapticTransient,
parameters: [
CHHapticEventParameter(parameterID: .hapticIntensity, value: 1.0),
CHHapticEventParameter(parameterID: .hapticSharpness, value: 1.0)
],
relativeTime: 0)
let crunch2 = CHHapticEvent(
eventType: .hapticTransient,
parameters: [
CHHapticEventParameter(parameterID: .hapticIntensity, value: 1.0),
CHHapticEventParameter(parameterID: .hapticSharpness, value: 0.3)
],
relativeTime: 0.3)
return try CHHapticPattern(
events: [rumble1, rumble2, crunch1, crunch2],
parameters: [])
As mentioned earlier, a transient event is instantaneous, like a single drum beat. .hapticIntensity
represents the strength of the sensation, and .hapticSharpness
represents the physical quality. Higher values create stronger and more prominent haptic events. Feel free to tweak the numbers to your liking. Here, you add two transient events to match those snapping jaws.
Build and run. Pretty *snappy*, right? You want to give the user a rewarding feeling, since this is the success result for the game screen. And that satisfying *snap* *snap* is perfect: