alexis

software engineer, nyc

Byte of the Day: Haptics in SwiftUI

Haptic feedback adds a layer of polish to your app. It can also serve as an accessibility feature, providing a physical confirmation when a task fails or completes.

In UIKit, you'd typically use one of the UIFeedbackGenerator subclasses. But in SwiftUI there is no native support for these.

One solution is to create an object that wraps this functionality and then expose it to views in the SwiftUI environment. Let's create this type:

struct Haptics {
    /// Performs a success haptic feedback.
    func success() {
        let feedbackGenerator = UINotificationFeedbackGenerator()
        feedbackGenerator.prepare()
        feedbackGenerator.notificationOccurred(.success)
    }
}

Now, let's expose it to views. In SwiftUI, the environment is a collection of values that are propagated down the view hierarchy. It is a good place to provide objects that pertain to the device, your app or your application stack.

This collection is keyed by types that conform to the EnvironmentKey protocol, that requires you to provide a default value. In this case, we'll instantiate a Haptics instance. We use an enum since it's a namespace-like structure and we don't need an init for it.

enum HapticsEnvironmentKey: EnvironmentKey {
    static let defaultValue = Haptics()
}

The last step is to make this key available in the environment values collection, so that it can be used with the @Environment property wrapper in views. This is done by creating a computed property on EnvironmentValues.

extension EnvironmentValues {
    var haptics: Haptics {
        get { self[HapticsEnvironmentKey.self] }
        set { self[HapticsEnvironmentKey.self} = newValue }
    }
}

You can now use the haptics property on the environment values in your views by using it as a key-path with @Environment:

struct ContentView: View {
    @Environment(\.haptics) private var haptics
    
    var body: some View {
        Button(action: buttonTapped) {
            Text("Tap Me!")
        }
    }
    
    private func buttonTapped() {
        haptics.success()
    }
}

Next Steps

  • You can modify the Haptics type to include more feedback types, such as impact or selection.
  • You can create a publisher in your view model that emits Void when a task completes, and use the onReceive(_:perform:) method to perform haptics.

The source code of the example is available on GitHub. Don't hesitate to reach out with questions!