Is it possible to add multiple haptics to play one after another in Swift?

I've been trying to add multiple haptic events based on when each view I have animates in, however when testing it only plays the first haptic and stops there. I've tried searching online if it's even possible to play multiple haptics in a short space of time (would the Taptic Engine be ready to fire off multiple times that quickly?) but couldn't find anything.

Anyone know if this is even possible and if so why mine might not be working?

I've tried 2 different methods of adding multiple haptics in:

Here's my code for the first method, where I'm initialising a new instance first and then playing the same haptic within each animation block with a 0.25 delay between animations:
Code Block
func animateSunriseAndSunsetViews() {
let generator = UIImpactFeedbackGenerator(style: .medium)
generator.prepare()
sunriseView.alpha = 0
morningGoldenHourView.alpha = 0
morningWeatherLabel.alpha = 0
morningGoldenHourWeatherView.alpha = 0
eveningGoldenHourView.alpha = 0
sunsetView.alpha = 0
eveningWeatherLabel.alpha = 0
eveningGoldenHourWeatherView.alpha = 0
nextGoldenHourLabel.alpha = 0
UIView.animate(withDuration: 1, delay: 0, options: .curveEaseInOut) {
self.sunriseView.alpha = 100
generator.impactOccurred()
}
UIView.animate(withDuration: 1, delay: 0.25, options: .curveEaseInOut) {
self.morningGoldenHourView.alpha = 100
generator.impactOccurred()
}
UIView.animate(withDuration: 1, delay: 0.5, options: .curveEaseInOut) {
self.eveningGoldenHourView.alpha = 100
generator.impactOccurred()
}
UIView.animate(withDuration: 1, delay: 0.75, options: .curveEaseInOut) {
self.sunsetView.alpha = 100
generator.impactOccurred()
}
UIView.animate(withDuration: 1, delay: 1, options: .curveEaseInOut) {
self.morningWeatherLabel.alpha = 100
generator.impactOccurred()
}
UIView.animate(withDuration: 1, delay: 1.25, options: .curveEaseInOut) {
self.morningGoldenHourWeatherView.alpha = 100
generator.impactOccurred()
}
UIView.animate(withDuration: 1, delay: 1.5, options: .curveEaseInOut) {
self.eveningWeatherLabel.alpha = 100
generator.impactOccurred()
}
UIView.animate(withDuration: 1, delay: 1.75, options: .curveEaseInOut) {
self.eveningGoldenHourWeatherView.alpha = 100
generator.impactOccurred()
}
UIView.animate(withDuration: 1, delay: 2, options: .curveEaseInOut) {
self.nextGoldenHourLabel.alpha = 100
generator.impactOccurred()
}
}

Here's the 2nd method I've tried, where I initialise and play a new haptic within each animation block:
Code Block
func animateSunsetViews() {
eveningGoldenHourView.alpha = 0
sunsetView.alpha = 0
eveningWeatherLabel.alpha = 0
eveningGoldenHourWeatherView.alpha = 0
nextGoldenHourLabel.alpha = 0
UIView.animate(withDuration: 1, delay: 0, options: .curveEaseInOut) {
self.eveningGoldenHourView.alpha = 100
let generator = UINotificationFeedbackGenerator()
generator.notificationOccurred(.success)
}
UIView.animate(withDuration: 1, delay: 0.25, options: .curveEaseInOut) {
self.sunsetView.alpha = 100
let generator = UINotificationFeedbackGenerator()
generator.notificationOccurred(.success)
}
UIView.animate(withDuration: 1, delay: 0.5, options: .curveEaseInOut) {
self.eveningWeatherLabel.alpha = 100
let generator = UINotificationFeedbackGenerator()
generator.notificationOccurred(.success)
}
UIView.animate(withDuration: 1, delay: 0.75, options: .curveEaseInOut) {
self.eveningGoldenHourWeatherView.alpha = 100
let generator = UINotificationFeedbackGenerator()
generator.notificationOccurred(.success)
}
UIView.animate(withDuration: 1, delay: 1, options: .curveEaseInOut) {
self.nextGoldenHourLabel.alpha = 100
let generator = UINotificationFeedbackGenerator()
generator.notificationOccurred(.success)
}
}

So far both methods only play the first haptic and stop right there. Any ideas if this works? Thanks.
Accepted Answer
When you use UIView.animate(withDuration:, delay:, options:, animations:, completion:), your animations closure is generally going to execute synchronously (immediately) when you call that method, even when you specify a nonzero delay.

When you provide a delay, the way that works is that it gets associated with the actual animations created for UIView/CALayer properties (e.g. when you change the view alpha, a CABasicAnimation will be created for the view's layer opacity under the hood). So your code is setting the new alpha immediately, but due to the animation being delayed you won't see the effect of it until the animation actually starts and runs (unless something removes the animation).

This is why in both cases, you're seeing all the feedback happen at once: all of your feedback generator usage is happening back-to-back immediately as each of the animations gets set up.

In order to actually delay the feedback and haptics, you need to delay the calls into the UIFeedbackGenerator. A simple way to do this would be to use DispatchQueue.main.asyncAfter to delay the usage:

Code Block swift
DispatchQueue.main.asyncAfter(deadline: .now() + 0.5) {
// Use UIFeedbackGenerator here for a simple delay
}


However, note that this simple approach of using DispatchQueue.main.asyncAfter does not guarantee an extremely precise delay, and if you're trying to closely synchronize your feedback with animations or other UI changes, you'll likely want to use a different approach. For UIFeedbackGenerator, it's best to trigger feedback directly in response to actual UI events occurring, e.g. in the completion of animations or other actions. For example, instead of setting up a chain of multiple animations up front with a delay on each of them, consider triggering them one-by-one with callbacks into your code between each animation finishing and the next one starting, so that you can trigger feedback and then set up the next animation each time.

If you need more advanced and precise control over the timing of haptics, e.g. to synchronize haptics with audio or other events, consider using Core Haptics directly which provides a lower-level interface for you to schedule haptic events to play at specific times in the future.
Thank you so much for that clear and precise explanation, it really helps me understand how animation blocks actually work and why my UIFeedbackGenerator wasn't doing what is was supposed to do! Thanks!
Is it possible to add multiple haptics to play one after another in Swift?
 
 
Q