SwiftUI: Stop an Animation that Repeats Forever

After going through many things, I found out something that works for me. At the least for the time being and till I have time to figure out a better way.

struct WiggleAnimation<Content: View>: View {
    var content: Content
    @Binding var animate: Bool
    @State private var wave = true

    var body: some View {
        ZStack {
            content
            if animate {
                Image(systemName: "minus.circle.fill")
                    .foregroundColor(Color(.systemGray))
                    .offset(x: -25, y: -25)
            }
        }
        .id(animate) //THIS IS THE MAGIC
        .onChange(of: animate) { newValue in
            if newValue {
                let baseAnimation = Animation.linear(duration: 0.15)
                withAnimation(baseAnimation.repeatForever(autoreverses: true)) {
                    wave.toggle()
                }
            }
        }
        .rotationEffect(.degrees(animate ? (wave ? 2.5 : -2.5) : 0.0),
                        anchor: .center)
    }

    init(animate: Binding<Bool>,
         @ViewBuilder content: @escaping () -> Content) {
        self.content = content()
        self._animate = animate
    }
}

Use

@State private var editMode = false

WiggleAnimation(animate: $editMode) {
    VStack {
        Image(systemName: image)
            .resizable()
            .frame(width: UIScreen.screenWidth * 0.1,
                   height: UIScreen.screenWidth * 0.1)
            .padding()
            .foregroundColor(.white)
            .background(.gray)
        
        Text(text)
            .multilineTextAlignment(.center)
            .font(KMFont.tiny)
            .foregroundColor(.black)
    }
}

How does it work? .id(animate) modifier here does not refresh the view but just replaces it with a new one, so it is back to its original state.

Again this might not be the best solution but it works for my case.


I figured it out!

An animation using .repeatForever() will not stop if you replace the animation with nil. It WILL stop if you replace it with the same animation but without .repeatForever(). ( Or alternatively with any other animation that comes to a stop, so you could use a linear animation with a duration of 0 to get a IMMEDIATE stop)

In other words, this will NOT work: .animation(active ? Animation.default.repeatForever() : nil)

But this DOES work: .animation(active ? Animation.default.repeatForever() : Animation.default)

In order to make this more readable and easy to use, I put it into an extension that you can use like this: .animation(Animation.default.repeat(while: active))

Here is an interactive example using my extension you can use with live previews to test it out:

import SwiftUI

extension Animation {
    func `repeat`(while expression: Bool, autoreverses: Bool = true) -> Animation {
        if expression {
            return self.repeatForever(autoreverses: autoreverses)
        } else {
            return self
        }
    }
}

struct TheSolution: View {
    @State var active: Bool = false
    var body: some View {
        Circle()
            .scaleEffect( active ? 1.08: 1)
            .animation(Animation.default.repeat(while: active))
            .frame(width: 100, height: 100)
            .onTapGesture {
                self.active.toggle()
            }
    }
}

struct TheSolution_Previews: PreviewProvider {
    static var previews: some View {
        TheSolution()
    }
}

As far as I have been able to tell, once you assign the animation, it will not ever go away until your View comes to a complete stop. So if you have a .default animation that is set to repeat forever and auto reverse and then you assign a linear animation with a duration of 4, you will notice that the default repeating animation is still going, but it's movements are getting slower until it stops completely at the end of our 4 seconds. So we are animating our default animation to a stop through a linear animation.