Here’s what I have so far based for an alarm view animation that I’m working on
struct AlarmView: View {
@State var alarmSet = false
@State var animateBell = false
@State var alarmText = "Remind Me"
@State var shouldAnimate = false
@State private var bellID: UUID = UUID()
@State private var timeSinceLastTap = Date.now
var body: some View {
ZStack {
Color.black
VStack {
Button(action: {
// animate only if there is some time elapsed between taps
let duration = min(0.3, abs(timeSinceLastTap.timeIntervalSinceNow))
shouldAnimate = duration > 0.2
alarmText = !alarmSet ? "Reminder Set" : "Remind Me"
if shouldAnimate {
if alarmSet == true { // turn OFF alarm
alarmSet.toggle()
} else { // animate the bell, then turn ON alarm
animateBell.toggle()
DispatchQueue.main.asyncAfter(deadline: .now() + 0.5) {
alarmSet.toggle()
}
}
} else {
// to cancel animation
bellID = UUID()
alarmSet.toggle()
}
timeSinceLastTap = Date.now
}, label: {
VStack {
VStack {
if alarmSet {
Image(systemName: "checkmark")
.font(.system(size: 150))
.foregroundColor(.white)
} else {
Image(systemName: "bell.fill")
.font(.system(size: 150))
.foregroundColor(.white)
.id(bellID)
.keyframeAnimator(initialValue: Angle.zero, trigger: animateBell) { content, value in
content.rotationEffect(value, anchor: .top)
} keyframes: { _ in
KeyframeTrack {
let spring = Spring(mass: 0.5, stiffness: 170, damping: 2.5)
SpringKeyframe(Angle.degrees(10), duration: 0.05, spring: spring)
SpringKeyframe(Angle.degrees(-10), duration: 0.1, spring: spring)
SpringKeyframe(Angle.degrees(10), duration: 0.1, spring: spring)
SpringKeyframe(Angle.degrees(-10), duration: 0.1, spring: spring)
SpringKeyframe(Angle.degrees(10), duration: 0.1, spring: spring)
SpringKeyframe(Angle.zero, duration: 0.05, spring: spring, startVelocity: Angle.zero)
}
}
}
}
.frame(width: 150, height: 150)
Text(alarmText)
.font(.largeTitle)
.foregroundStyle(.gray)
}
})
.buttonStyle(NoAnimationStyle())
}
}
.ignoresSafeArea()
}
}
struct NoAnimationStyle: PrimitiveButtonStyle {
func makeBody(configuration: Configuration) -> some View {
configuration.label
.contentShape(Rectangle())
.onTapGesture(perform: configuration.trigger)
}
}
This works well for the most part except for the following scenario. When tapping the button while in its default state (with the bell showing and text saying “Remind me”), if I tap again while the bell is animating/wiggling it momentarily shows the checkmark before showing the bell again.
However the text continues to say “Reminder Set” instead of defaulting to “Remind Me”. The effect I’m trying to replicate is the reminder animation in the Netflix app in their ‘New & Hot’ screen. From what I checked, the Netflix animation is buggy on iOS where quick taps cause it to animate horribly even after the taps have ended. On Android however, the animation is perfect wherein if user taps twice quickly, just the text changes while the icon stays unchanged and does not switch at all between quick taps.
In my case ideally I’d like for the bell animation to cancel even if mid animation & not show the checkmark at all and for the text to be set appropriately. How can I achieve this? Any help is appreciated.