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
You are currently viewing page 4 of 4 of this article. Click here to view the first page.

Controlling Intensity With a Parameter Curve

Since you’re getting creative with these haptic experiences, why not improve the snip haptic pattern for a more satisfying *SsssNIP* feel? A haptic pattern can also accept parameters that apply to the pattern as a whole.

First, update the slice event property like so:

let slice = CHHapticEvent(
  eventType: .hapticContinuous, 
  parameters: [
    CHHapticEventParameter(parameterID: .hapticIntensity, value: 0.6),
    CHHapticEventParameter(parameterID: .hapticSharpness, value: 0.8)
  relativeTime: 0, 
  duration: 0.5)

This increases the intensity, sharpness and duration.

Next, create a new type of parameter — a CHHapticParameterCurve — after the two slice and snip events in slicePattern():

let curve = CHHapticParameterCurve(
  parameterID: .hapticIntensityControl, 
  controlPoints: [
    .init(relativeTime: 0, value: 0.2),
    .init(relativeTime: 0.08, value: 1.0),
    .init(relativeTime: 0.24, value: 0.2),
    .init(relativeTime: 0.34, value: 0.6),
    .init(relativeTime: 0.5, value: 0)
  relativeTime: 0)

A parameter curve is similar to an animation curve, and the control points are like animation keyframes.

This parameter curve has the ID .hapticIntensityControl, which acts as a multiplier to all event intensity values across the pattern. Because it’s a curve, the parameter interpolates smoothly between the control points as the pattern plays.

For example, the first control point is at time 0 with value 0.2, which means that it multiplies all event intensity values by 0.2 at the start. By 0.08 seconds, it will have ramped up smoothly to a multiplier of 1.0. By 0.24 seconds, it will have ramped smoothly back down to 0.2, and so on.

Here’s how it looks:

Slice curve timing graph

To use the parameter curve, you need to initialize the pattern object using CHHapticPattern(events:parameterCurves:).

Still in Haptics.swift, replace the return statement in slicePattern() with the following:

return try CHHapticPattern(events: events, parameterCurves: [curve])

This creates the haptic pattern using the curve you specified.

Build and run to experience your new dynamic haptic experience.

Updating Pattern Parameters in Real Time

If you think dynamic parameter curves are cool, wait until you see Core Haptics’ heavy hitter: CHHapticAdvancedPatternPlayer. This is a pattern player that you can control while it’s playing.

There’s something important missing in your game. When the player swishes a finger across the screen, you can see the particle effects, but where’s the feel of the blade slicing through the air? With a CHHapticAdvancedPatternPlayer, you can even control the intensity in real time so it ramps up stronger the faster the player’s finger moves.

Player slicing a vine in the game

First, add a property to HapticManager to hold a reference to your new player:

var swishPlayer: CHHapticAdvancedPatternPlayer?

Next, add a method to create the player:

func createSwishPlayer() {
  let swish = CHHapticEvent(
    eventType: .hapticContinuous, 
    parameters: [
      CHHapticEventParameter(parameterID: .hapticIntensity, value: 0.5),
      CHHapticEventParameter(parameterID: .hapticSharpness, value: 1.0)
    relativeTime: 0,
    duration: 60)
  do {
    let pattern = try CHHapticPattern(events: [swish], parameters: [])
    swishPlayer = try hapticEngine.makeAdvancedPlayer(with: pattern)
  } catch let error {
    print("Swish player error: \(error)")

It’s a simple pattern: a single continuous event with a long duration. You create the player by calling makeAdvancedPlayer(with:).

Next, in HapticManager, add the following line to setupResources():


By doing this, you create the swish player whenever you call setupResources(), both in initializer and the haptic engine reset handler. Player references also reset when the engine resets.

Next, you need to add a method to start the player. Add the following at the end of HapticManager:

func startSwishPlayer() {
  do {
    try hapticEngine.start()
    try swishPlayer?.start(atTime: CHHapticTimeImmediate)
  } catch {
    print("Swish player start error: \(error)")

startSwishPlayer() first calls hapticEngine.start() just in case the engine has stopped. Then, it calls the pattern player’s start(atTime:) with CHHapticTimeImmediate, so the player starts immediately.

You’ll also need to add a method to stop the player. Add this at the end of HapticManager as well:

func stopSwishPlayer() {
  do {
    try swishPlayer?.stop(atTime: CHHapticTimeImmediate)
  } catch {
    print("Swish player stop error: \(error)")

Here you try to stop the pattern player as soon as you can, almost immediately, by passing CHHapticTimeImmediate.

Return to GameScene.swift, find touchesBegan(_:with:) and add the following line to start the pattern player when the player begins swiping:


Next, find touchesEnded(_:with:) and add the following line to stop the pattern player when the player’s swipe ends:


Build and run and you should experience the player starting and stopping as you move your finger around the screen.

Now, it’s time to add the magic!

Making the Player Dynamic

Next, you’ll make the swish’s intensity depend on the user’s movements. Add the following method to HapticManager:

// 1
func updateSwishPlayer(intensity: Float) {
  // 2
  let intensity = CHHapticDynamicParameter(
    parameterID: .hapticIntensityControl, 
    value: intensity, 
    relativeTime: 0)
  do {
    // 3
    try swishPlayer?.sendParameters([intensity], atTime: CHHapticTimeImmediate)
  } catch let error {
    print("Swish player dynamic update error: \(error)")
  1. Your new updateSwishPlayer(intensity:) takes a single float argument: a value between 0 and 1.
  2. Use that value to create a CHHapticDynamicParameter with the ID .hapticIntensityControl. This parameter functions much like the previous parameter curve you created, acting as a multiplier to all the event intensity values in the pattern. Unlike the curve, this is a one-time change.
  3. Send the dynamic parameter to the player for it to apply immediately to the pattern that’s playing.

Return to GameScene.swift and add the following to touchesMoved(_:with:):

let distance = CGVector(
  dx: abs(startPoint.x - endPoint.x),
  dy: abs(startPoint.y - endPoint.y))
let distanceRatio = CGVector(
  dx: distance.dx / size.width,
  dy: distance.dy / size.height)
let intensity = Float(max(distanceRatio.dx, distanceRatio.dy)) * 100
hapticManager?.updateSwishPlayer(intensity: intensity)

Every time the system calls touchesMoved(_:with:), you update your dynamic player’s intensity control value. You calculate the intensity using a simple algorithm: The more you move from the previous touch, the higher the intensity value will be.

Build and run. Snipping the vine should now feel like you’re a Jedi knight wielding a light saber!

Where to Go From Here?

You can download the completed project using the Download Materials button at the top or bottom of this tutorial.

You’ve transformed Snip The Vine from an amusing distraction to a whole new immersive experience! In the field of physics-based-cutting-things-down-so-they-drop-on-animals games on the App Store, it’ll beat them all, hands down.

If you can believe it, you’ve only touched on what Core Haptics can achieve. There’s so much more to explore.

Watch the Apple sessions from WWDC 2019:

Have a look through the Core Haptics documentation. There are a few sample Xcode projects in there to download as well.

Don’t forget about the Apple Human Interface Guidelines page on Haptics and the tips in Designing with Haptics.

You may also want to read about Apple Haptic and Audio Pattern (AHAP) file format.

I hope you enjoyed this Core Haptics tutorial. If you have any questions or comments, please join the forum discussion below.