Adding haptic feedback to your app with CoreHaptics
Published on: November 27, 2019One of iOS 13's more subtle, yet amazingly fun and powerful frameworks is CoreHaptics
. With this framework, you can add tactile feedback to your app. When implemented correctly, this kind of feedback will delight and amaze your users. It will make your app feel alive like it's physically responsive to the touch input from your user. If you want to see what haptic feedback feels like in a real app, check out the CocoaHub app. It uses haptic feedback when you switch tabs in the app and in my opinion, this feels really nice.
In this blog post you will learn about the following:
- Understanding the different types of haptic feedback
- Adding haptic feedback to your app
- Using AHAP files to define your haptics
By the end of this post, you should be able to add beautiful, elegant haptic experiences to your app that will feel natural and exciting to your users. Because when implemented correctly, haptics can really take any touch to the next level.
Understanding the different types of haptic feedback
Ever since the iPhone 6s, we have been able to add haptics to apps. Haptic feedback is perceived as tiny little nudges or taps that you get from your phone when something happens. It's almost like a very short vibration, except it seems to have a little bit more force to it. The same type of technology that's found in the iPhone 6s and up can be found in the Apple watch and in Apple's trackpads. If you have a MacBook (pro) and you click the trackpad, nothing actually clicks. That's the taptic engine giving you haptic feedback.
The haptics that exist on iOS 12, and in your MacBook's trackpad are fairly limited in what you can with them. With iOS 13 Apple has made a huge change to the haptic capabilities we have. Instead of very simple haptics that can't express much more than that clicking feeling, we can almost make the user feel sound through haptics. This is all enabled through the CoreHaptics
framework.
In this framework, we have two key types of haptic feedback, continuous and transient. Transient haptic feedback is like a tap, or a nudge and is typically used when you tap a button, when you need the user's attention or if you want to enrich the visual experience of two objects bumping into each other with some nice haptic feedback. For this kind of haptic feedback, you can configure its intensity to specify how intense the tap should be, and its sharpness. A sharp haptic will feel more urgent to the user, while a less sharp haptic will feel very calm. If this sounds abstract, or strange, I recommend that you come back to this section after playing with haptics a bit. You'll realize that sharp and intense are actually very clever ways to describe what certain haptic feedback feels like.
Continuous haptic feedback is more like a vibration pattern. It still has that typical haptic click feeling, except there are many of them in rapid succession. For a continuous event, you can specify exactly the same parameters that are available for transient feedback, except you can also set the event's duration to determine how long the haptic feedback should keep going for.
It's also possible to define how certain characteristics of a haptic event should change over time. For example, to create an increasing and then decreasing continuous haptic, you would define a single haptic event and apply dynamic parameters or a parameter curve to have the haptic fade in and out over time. I will show an example of both dynamic parameters and parameter curves in the next section.
In addition to tactile feedback, CoreHaptics
also has special audio haptic event types. Audio haptic can either be a custom audio file that is played or a generated waveform that is configured similar to a haptic event, except it's played as audio rather than a haptic pulse. You can configure parameters like the audio's pitch, volume and decay to create some very interesting audio effects to accompany your haptic experiences.
Adding haptics to your app
All haptic feedback by CoreHaptics
is played through an instance of CHHapticEngine
. You can create an instance of the haptic engine as follows:
let engine = try CHHapticEngine()
Note that you should define the haptic engine somewhere where the instance will stay around for as long as you need it. You should avoid creating an engine every time you want to play haptic feedback. Instead, make it a property on the object that will end up managing your haptics implementation so it stays around for a longer time.
You should also set the engine's stoppedHandler
and its resetHandler
. The stoppedHandler
is called in case the haptic engine is stopped due to external factors or if it's finished playing your haptic events. The resetHandler
is called if something went wrong and the engine has to be reset. In the reset handler, you will typically want immediately start the engine again.
Before you can play haptic feedback, you must start the haptic engine by calling its start
method:
do {
try engine.start()
} catch {
// failed to start the engine
}
Once the engine is ready to play haptics, we can define our haptic pattern, ask the haptic engine to create an instance of CHHapticPatternPlayer
and then play the haptics. Let's create a simple transient haptic event:
let event = CHHapticEvent(eventType: .hapticTransient, parameters: [
CHHapticEventParameter(parameterID: .hapticSharpness, value: 0.5),
CHHapticEventParameter(parameterID: .hapticIntensity, value: 1)
], relativeTime: 0)
do {
let pattern = try CHHapticPattern(events: [event], parameters: [])
let player = try engine.makePlayer(with: pattern)
try player?.start(atTime: CHHapticTimeImmediate)
} catch {
// something went wrong
}
The code above defines a haptic event. Haptic events are the building blocks that can be used to define a haptic pattern. In this case, we have a pattern that only contains a single haptic event. It is possible to, for example, play a transient haptic event first, then play a continuous haptic for a second, and then play another transient haptic event. You can use the relativeTime
parameter on the CHHapticEvent
initializer to specify an offset for each event. So if you want to play several haptic taps in succession, you would use the relativeTime
to make sure each haptic starts when needed rather than playing the entire pattern all at once.
Once the pattern is defined, we can create an instance of CHHapticPattern
. The parameters
property in the pattern's initializer is an array of CHHapticDynamicParameter
items that define how the haptic pattern's intensity, sharpness, and other characteristics should be manipulated over time. I'll show you an example shortly.
Once the pattern is created, we can ask the haptic engine for a player, and we can then play our haptic pattern on the device. You can specify a delay for the player's start time if needed. We use CHHapticTimeImmediate
in this example, but you can specify any TimeInterval
(or Double
) you want. Fairly straightforward, right? In the following examples, I will only show the steps to create the CHHapticPattern
. All steps involving obtaining and starting a player are identical for all haptic patterns.
Let's see an example of building a haptic pattern as described earlier where we have a transient event, then a continuous one, and then another transient one:
let events = [
CHHapticEvent(eventType: .hapticTransient, parameters: [
CHHapticEventParameter(parameterID: .hapticSharpness, value: 0.5),
CHHapticEventParameter(parameterID: .hapticIntensity, value: 1)
], relativeTime: 0),
CHHapticEvent(eventType: .hapticContinuous, parameters: [
CHHapticEventParameter(parameterID: .hapticSharpness, value: 1),
CHHapticEventParameter(parameterID: .hapticIntensity, value: 0.7),
], relativeTime: 0.1, duration: 0.5),
CHHapticEvent(eventType: .hapticTransient, parameters: [
CHHapticEventParameter(parameterID: .hapticSharpness, value: 0.5),
CHHapticEventParameter(parameterID: .hapticIntensity, value: 1)
], relativeTime: 0.7)
]
let pattern = try CHHapticPattern(events: events, parameters: [])
Note that the second haptic event, the continuous one, defines its duration while the transient events don't. This is because a transient haptic event is a single event, that doesn't have a duration. If you try running this pattern on a device, you will feel a short tap, then a sharp buzz, followed by another short tap. Cool, right!
Let's see how we can use parameters to create an interesting continuous haptic pattern.
let events = [
CHHapticEvent(eventType: .hapticContinuous, parameters: [
CHHapticEventParameter(parameterID: .hapticSharpness, value: 1),
CHHapticEventParameter(parameterID: .hapticIntensity, value: 0.8),
], relativeTime: 0.1, duration: 3)
]
let parameters = [
CHHapticDynamicParameter(parameterID: .hapticIntensityControl, value: 0.3, relativeTime: 0),
CHHapticDynamicParameter(parameterID: .hapticIntensityControl, value: 1, relativeTime: 1),
CHHapticDynamicParameter(parameterID: .hapticIntensityControl, value: 0.5, relativeTime: 2),
CHHapticDynamicParameter(parameterID: .hapticIntensityControl, value: 1, relativeTime: 2.5)]
let pattern = try CHHapticPattern(events: events, parameters: parameters)
Note that the value
property of a dynamic parameter is used as a multiplier for the value
specified on the parameter it manipulates. So in this case setting a value of 1 on the dynamic pattern equals an intensity of 0.8. A value of 0.5 equals an intensity of 0.4, etc.
If you play this you will notice that the changes for the dynamic parameters are very abrupt. After one second we jump to a higher intensity. One second later, the pattern jumps to a lower intensity and so forth. If you want to apply a more streamlined transition, we can use CHHapticParameterCurve
instead of CHHapticDynamicParameter
. Let's see how:
let events = [
CHHapticEvent(eventType: .hapticContinuous, parameters: [
CHHapticEventParameter(parameterID: .hapticSharpness, value: 1),
CHHapticEventParameter(parameterID: .hapticIntensity, value: 0.8),
], relativeTime: 0.1, duration: 3)
]
let dynamicParameters = [
CHHapticParameterCurve(parameterID: .hapticIntensityControl,
controlPoints: [.init(relativeTime: 0, value: 0),
.init(relativeTime: 1, value: 1),
.init(relativeTime: 2, value: 0.5),
.init(relativeTime: 3, value: 1)],
relativeTime: 0)]
let pattern = try CHHapticPattern(events: events, parameterCurves: dynamicParameters)
Everything looks very similar except we define an array of CHHapticParameterCurve
instead of CHHapticDynamicParameter
. Note that there is a controlPoints
argument on the CHHapticParameterCurve
initializer. This is where we specify how we want our pattern to change over time. We create instances of CHHapticParameterCurve.ControlPoint
to define how the values change over time. Note that I used .init
as a shorthand because the real type name is a bit long to type. If you run this pattern on a device, you should notice that the animations are much smoother.
Before we take a look at using AHAP files for haptic, let's also quickly define a custom audio haptic:
let events = [
CHHapticEvent(eventType: .audioContinuous, parameters: [
CHHapticEventParameter(parameterID: .audioPitch, value: 0.3),
CHHapticEventParameter(parameterID: .audioVolume, value: 0.6),
CHHapticEventParameter(parameterID: .decayTime, value: 0.1),
CHHapticEventParameter(parameterID: .sustained, value: 0)
], relativeTime: 0)
]
let pattern = try CHHapticPattern(events: events, parameters: [])
The process of creating an audio haptic is very similar to creating a tactile haptic. Try running this on your device and you should hear a short beep-like sound. Combining this with a sharp haptic with medium intensity makes for a very interesting effect. Try playing around with combining audio and tactile haptic to create feedback that delights your users.
Using AHAP files to define your haptics
One last thing I want to cover in this already fairly long Quick Tip is using AHAP files to define your haptic in files. Let's look at a very brief example of what the contents of an AHAP file look like:
{
"Version": 1.0,
"Metadata": {
"Project" : "Sample",
"Created" : "12-11-2019",
"Description" : "Quick haptic sample"
},
"Pattern": [
{ "Event":
{
"Time": 0.0,
"EventType": "HapticTransient",
"EventParameters": [
{ "ParameterID": "HapticIntensity", "ParameterValue": 1 },
{ "ParameterID": "HapticSharpness", "ParameterValue": 0.2 }]
}
}
]
}
You might notice that the pattern itself looks very similar to the patterns we defined in code. The big difference is that we now have a JSON-like file called an AHAP file that we can edit and update outside of our app. We could even fetch these AHAP files from a remote server to dynamically update the haptics in an app. Let's see how you can play an AHAP file in your app:
guard let path = Bundle.main.path(forResource: "myAhapFile", ofType: "ahap") else {
return
}
try engine.playPattern(from: URL(fileURLWithPath: path))
All that we need to do to play an AHAP file is load it from the bundle, and pass it to the haptic engine. It will then read the file, create the player and play the pattern specified in your file.
In Summary
First, my apologies for making yet another Quick Tip that's way longer than I intended. There are just so many cool things you can do with haptics and I wanted to make sure that you have all the tools needed to get started with haptics in your own apps. If you need some more inspiration, be sure to check out this WWDC video from 2019 it has many beautiful examples of how haptics improve the user experience.
Also, make sure to check out the documentation for the CoreHaptics framework. There is a lot of good information available, and even a couple of sample apps that will help you to explore haptics and hopefully inspire you to integrate awesome haptic experiences in your app.
If you have questions, feedback or suggestions, don't hesitate to reach out on Twitter.