SwiftUI has built-in support for simple haptic effects, which use Apple's Taptic Engine to make the phone vibrate in various ways. We have two options for this in iOS, easy and complete – I'll show you both so you know what's possible, but I think it's fair to say you'll want to stick to the easy option unless you have very specific needs!
Important: These haptic effects only work on physical iPhones – Macs and other devices such as iPad won't vibrate.
Let's start with the easy option. Like sheets and alerts, we tell SwiftUI when to trigger the effect and it takes care of the rest for us.
First, we can write a simple view that adds 1 to a counter whenever a button is pressed:
struct ContentView: View {
@State private var counter = 0
var body: some View {
Button("Tap Count: \(counter)") {
counter += 1
}
}
}
That's all old code, so let's make it more interesting by adding a haptic effect that triggers whenever the button is pressed – add this modifier to the button:
.sensoryFeedback(.increase, trigger: counter)
Try running that on a real device, and you should feel gentle haptic taps whenever you press the button.
.increase
is one of the built-in haptic feedback variants, and is best used when you're increasing a value such as a counter. There are lots of others to choose from, including .success
, .warning
, .error
, .start
, .stop
, and more.
Each of these feedback variants has a different feel, and although it's tempting to go through them all and pick the ones you like the most, please think about how this might be confusing for blind users who rely on haptics to convey information – if your app hits an error but you play the success haptic because you like it a lot, it might cause confusion.
If you want a little more control over your haptic effects, there's an alternative called .impact()
, which has two variants: one where you specify how flexible your object is and how strong the effect should be, and one where you specify a weight and intensity.
For example, we could request a middling collision between two soft objects:
.sensoryFeedback(.impact(flexibility: .soft, intensity: 0.5), trigger: counter)
Or we could specify an intense collision between two heavy objects:
.sensoryFeedback(.impact(weight: .heavy, intensity: 1), trigger: counter)
For more advanced haptics, Apple provides us with a whole framework called Core Haptics. This thing feels like a real labor of love by the Apple team behind it, and I think it was one of the real hidden gems introduced in iOS 13 – certainly I pounced on it as soon as I saw the release notes!
Core Haptics lets us create hugely customizable haptics by combining taps, continuous vibrations, parameter curves, and more. I don’t want to go into too much depth here because it’s a bit off topic, but I do at least want to give you an example so you can try it for yourself.
First add this new import near the top of ContentView.swift:
import CoreHaptics
Next, we need to create an instance of CHHapticEngine
as a property – this is the actual object that’s responsible for creating vibrations, so we need to create it up front before we want haptic effects.
So, add this property to ContentView
:
@State private var engine: CHHapticEngine?
We’re going to create that as soon as our main view appears. When creating the engine you can attach handlers to help resume activity if it gets stopped, such as when the app moves to the background, but here we’re going to keep it simple: if the current device supports haptics we’ll start the engine, and print an error if it fails.
Add this method to ContentView
:
func prepareHaptics() {
guard CHHapticEngine.capabilitiesForHardware().supportsHaptics else { return }
do {
engine = try CHHapticEngine()
try engine?.start()
} catch {
print("There was an error creating the engine: \(error.localizedDescription)")
}
}
Now for the fun part: we can configure parameters that control how strong the haptic should be (.hapticIntensity
) and how “sharp” it is (.hapticSharpness
), then put those into a haptic event with a relative time offset. “Sharpness” is an odd term, but it will make more sense once you’ve tried it out – a sharpness value of 0 really does feel dull compared to a value of 1. As for the relative time, this lets us create lots of haptic events in a single sequence.
Add this method to ContentView
now:
func complexSuccess() {
// make sure that the device supports haptics
guard CHHapticEngine.capabilitiesForHardware().supportsHaptics else { return }
var events = [CHHapticEvent]()
// create one intense, sharp tap
let intensity = CHHapticEventParameter(parameterID: .hapticIntensity, value: 1)
let sharpness = CHHapticEventParameter(parameterID: .hapticSharpness, value: 1)
let event = CHHapticEvent(eventType: .hapticTransient, parameters: [intensity, sharpness], relativeTime: 0)
events.append(event)
// convert those events into a pattern and play it immediately
do {
let pattern = try CHHapticPattern(events: events, parameters: [])
let player = try engine?.makePlayer(with: pattern)
try player?.start(atTime: 0)
} catch {
print("Failed to play pattern: \(error.localizedDescription).")
}
}
To try out our custom haptics, modify the body
property of ContentView
to this:
Button("Tap Me", action: complexSuccess)
.onAppear(perform: prepareHaptics)
Adding onAppear()
makes sure the haptics system is started so the tap gesture works correctly.
If you want to experiment with haptics further, replace the let intensity
, let sharpness
, and let event
lines with whatever haptics you want. For example, if you replace those three lines with this next code you’ll get several taps of increasing then decreasing intensity and sharpness:
for i in stride(from: 0, to: 1, by: 0.1) {
let intensity = CHHapticEventParameter(parameterID: .hapticIntensity, value: Float(i))
let sharpness = CHHapticEventParameter(parameterID: .hapticSharpness, value: Float(i))
let event = CHHapticEvent(eventType: .hapticTransient, parameters: [intensity, sharpness], relativeTime: i)
events.append(event)
}
for i in stride(from: 0, to: 1, by: 0.1) {
let intensity = CHHapticEventParameter(parameterID: .hapticIntensity, value: Float(1 - i))
let sharpness = CHHapticEventParameter(parameterID: .hapticSharpness, value: Float(1 - i))
let event = CHHapticEvent(eventType: .hapticTransient, parameters: [intensity, sharpness], relativeTime: 1 + i)
events.append(event)
}
Core Haptics is great fun to experiment with, but given how much more work it takes I think you're likely to stick with the built-in effects as much as possible!
That brings us to the end of the overview for this project, so please put ContentView.swift back to its original state so we can begin building the main project.
SPONSORED Transform your career with the iOS Lead Essentials. This Black Friday, unlock over 40 hours of expert training, mentorship, and community support to secure your place among the best devs. Click for early access to this limited offer and a free crash course.
Sponsor Hacking with Swift and reach the world's largest Swift community!
Link copied to your pasteboard.