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.

Leave a rating/review
Download materials
Save for later
Share
You are currently viewing page 2 of 4 of this article. Click here to view the first page.

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 call stop(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.

When to use Core Haptics: The general rule is that if you use UIKit in your app, you can take advantage of Apple-designed Haptics for free if you use standard UIKit controls.

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:

Nom nom sound wave

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.

Diagram showing the two separate 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:

  1. Close mouth.
  2. Wait 0.15 seconds.
  3. Open mouth.
  4. Wait 0.15 seconds.
  5. Close mouth.

Croc eating swinging pineapple

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:

How the haptics align with the nom nom sound effects