iOS comes with a number of options for generating haptic feedback, and they are all available for us to use in SwiftUI. In its simplest form, this is as simple as creating an instance of one of the subclasses of UIFeedbackGenerator
then calling its play()
method, but for more precise control over feedback you should first call its prepare()
method to give the Taptic Engine chance to warm up.
Important: Warming up the Taptic Engine helps reduce the latency between us calling play()
and the effect actually happening, but it also has a battery impact so the system will only stay ready for a second or two after you call prepare()
.
There are a few different subclasses of UIFeedbackGenerator
we could use, but the one we’ll use here is UINotificationFeedbackGenerator
because it provides success and failure haptics that are common across iOS. Now, we could add one central instance of UINotificationFeedbackGenerator
to every ContentView
, but that causes a problem: ContentView
gets notified whenever a card has been removed, but isn’t notified when a drag is in progress, which means we don’t have the opportunity to warm up the Taptic Engine.
So, instead we’re going to give each CardView
its own instance of UINotificationFeedbackGenerator
so they can prepare and play them as needed. The system will take care of making sure the haptics are all neatly arranged, so there’s no chance of them somehow getting confused.
Add this new property to CardView
:
@State private var feedback = UINotificationFeedbackGenerator()
Now find the self.removal?()
line in the drag gesture of CardView
, and change that whole closure to this:
if self.offset.width > 0 {
self.feedback.notificationOccurred(.success)
} else {
self.feedback.notificationOccurred(.error)
}
self.removal?()
That alone is enough to get haptics in our app, but there is always the risk that the haptic will be delayed because the Taptic Engine wasn’t ready. In this case the haptic will still play, but it could be up to maybe half a second late – enough to feel just that little bit disconnected from our user interface.
To improve this we need to call prepare()
on our haptic a little before calling play()
. It is not enough to call prepare()
immediately before play()
: doing so does not give the Taptic Engine enough time to warm up, so you won’t see any reduction in latency. Instead, you should call prepare()
as soon as you know the haptic might be needed.
Now, there are two helpful implementation details that you should be aware of.
First, it’s OK to call prepare()
then never call play()
– the system will keep the Taptic Engine ready for a few seconds then just power it down again. If you repeatedly call prepare()
and never call play()
the system might start ignoring your prepare()
calls until at least one play()
has happened.
Second, it’s perfectly allowable to call prepare()
many times before calling play()
once – prepare()
doesn’t pause your app while the Taptic Engine warms up, and also doesn’t have any real performance cost when the system is already prepared.
Putting these two together, we’re going to update our drag gesture so that prepare()
is called whenever the gesture changes. This means it could be called a hundred times before play()
is finally called, because it will get triggered every time the user moves their finger.
So, modify your onChanged()
closure to this:
.onChanged { offset in
self.offset = offset.translation
self.feedback.prepare()
}
Now go ahead and give the app a try and see what you think – you should be able to feel two very different haptics depending on which direction you swipe.
Before we wrap up with haptics, there’s one thing I want you to consider. Years ago PepsiCo challenged mall shoppers to the “Pepsi Challenge”: drink a sip of one cola drink and a sip of another, and see which you prefer. The results found that more Americans preferred Pepsi than Coca Cola, despite Coke having a much bigger market share. However, there was a problem: people seemed to pick Pepsi in the test because Pepsi had a sweeter taste, and while that worked well in sip-size amounts it worked less well in the sizes of cans and bottles, where people actually preferred Coke.
The reason I’m saying this is because we added two haptic notifications to our app that will get played a lot. And while you’re testing out in small doses these haptics probably feel great – you’re making your phone buzz, and it can be really delightful. However, if you’re a serious user of this app then our haptics might hit two problems:
So, now you’ve tried it for yourself I want you to think about how they should be used. If this were my app I would probably keep the failure haptic, but I think the success haptic could go – that one is likely to be triggered the most often, and it means when the failure haptic plays it feels a little more special.
SPONSORED Building and maintaining in-app subscription infrastructure is hard. Luckily there's a better way. With RevenueCat, you can implement subscriptions for your app in hours, not months, so you can get back to building your app.
Sponsor Hacking with Swift and reach the world's largest Swift community!
Link copied to your pasteboard.