LAST CHANCE: Save 50% on all my Swift books and bundles! >>

Adding haptic effects

Paul Hudson    @twostraws   

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.

Hacking with Swift is sponsored by Essential Developer.

SPONSORED Join a FREE crash course for mid/senior iOS devs who want to achieve an expert level of technical and practical skills – it’s the fast track to being a complete senior developer! Hurry up because it'll be available only until July 28th.

Click to save your free spot now

Sponsor Hacking with Swift and reach the world's largest Swift community!

BUY OUR BOOKS
Buy Pro Swift Buy Pro SwiftUI Buy Swift Design Patterns Buy Testing Swift Buy Hacking with iOS Buy Swift Coding Challenges Buy Swift on Sundays Volume One Buy Server-Side Swift Buy Advanced iOS Volume One Buy Advanced iOS Volume Two Buy Advanced iOS Volume Three Buy Hacking with watchOS Buy Hacking with tvOS Buy Hacking with macOS Buy Dive Into SpriteKit Buy Swift in Sixty Seconds Buy Objective-C for Swift Developers Buy Beyond Code

Was this page useful? Let us know!

Average rating: 4.9/5

 
Unknown user

You are not logged in

Log in or create account
 

Link copied to your pasteboard.