Swiftui: Stop an Animation That Repeats Forever

SwiftUI: Stop an Animation that Repeats Forever

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.

How to STOP Animation().repeatForever in SwiftUI

Update - retested with Xcode 13.4 / iOS 15.5

Sample Image

The following should work, moved animation into overlay-only and added conditional to .default (tested with Xcode 11.2 / iOS 13.2)

Button("Start", action: { self.start.toggle() })
.font(.largeTitle)
.padding()
.foregroundColor(.white)
.background(RoundedRectangle(cornerRadius: 10).fill(Color.green))
.overlay(
RoundedRectangle(cornerRadius: 10)
.stroke(Color.green, lineWidth: 4)
.scaleEffect(start ? 2 : 0.9)
.opacity(start ? 0 : 1)
.animation(start ? Animation.easeOut(duration: 0.6)
.delay(1) // Add 1 second between animations
.repeatForever(autoreverses: false) : .default, value: start)
)

On GitHub

How to stop animation in the current state in SwiftUI

As @Schottky stated above, the offset value has already been set, SwiftUI is just animating the changes.

The problem is that when you tap the circle, onAppear is not called again which means enabled is not checked at all and even if it was it wouldn’t stop the offset from animating.

To solve this, we can introduce a Timer and do some small calculations, iOS comes with a built-in Timer class that lets us run code on a regular basis. So the code would look like this:

struct ContentView: View {
@State private var offset = 0.0
// 1
let timer = Timer.publish(every: 0.1, on: .main, in: .common).autoconnect()
@State var animationDuration: Double = 5.0
var body: some View {
Circle()
.frame(width: 100, height: 100)
.offset(y: CGFloat(self.offset))
.onTapGesture {
// 2
timer.upstream.connect().cancel()
}
.onReceive(timer) { _ in
// 3
animationDuration -= 0.1
if animationDuration <= 0.0 {
timer.upstream.connect().cancel()
} else {
withAnimation(.easeIn) {
self.offset += Double(-UIScreen.main.bounds.size.height + 300)/50
}
}
}
}
}
  1. This creates a timer publisher which asks the timer to fire every 0.1 seconds. It says the timer should run on the main thread, It should run on the common run loop, which is the one you’ll want to use most of the time. (Run loops lets iOS handle running code while the user is actively doing something, such as scrolling in a list or tapping a button.)

  2. When you tap the circle, the timer is automatically cancelled.

  3. We need to catch the announcements (publications) by hand using a new modifier called onReceive(). This accepts a publisher as its first parameter (in this came it's our timer) and a function to run as its second, and it will make sure that function is called whenever the publisher sends its change notification (in this case, every 0.1 seconds).

After every 0.1 seconds, we reduce the duration of our animation, when the duration is over (duration = 0.0), we stop the timer otherwise we keep decreasing the offset.

Check out triggering-events-repeatedly-using-a-timer to learn more about Timer

How to pause an animation in SwiftUI?

It looks like there is no control to stop animation.

As your requirement is to start and stop the draw in the middle of progress, one alternate solution is to use a Timer. The tricky point is to clear the arc based on the timer duration.

Here is the code I made a change in your MainView:

NOTE: Adjust the animation duration based on your choice.

struct MainView: View {
@State private var fillPoint = 1.0
@State private var animationDuration = 10.0
@State private var stopAnimation = true
@State private var countdownTimer: Timer?

private var ring: Ring {
let ring = Ring(fillPoint: self.fillPoint)
return ring
}

var body: some View {
VStack {
ring.stroke(Color.red, lineWidth: 15.0)
.frame(width: 200, height: 200)
.padding(40)
.animation(self.stopAnimation ? nil : .easeIn(duration: 0.1))
HStack {
Button(action: {
self.stopAnimation = false
self.countdownTimer = Timer.scheduledTimer(withTimeInterval: 0.1, repeats: true, block: { _ in
guard self.animationDuration > 0 else {
self.countdownTimer?.invalidate()
return
}
self.fillPoint = self.animationDuration/10
self.animationDuration -= 0.1
})
}) {
Text("Start")
}
Button(action: {
self.countdownTimer?.invalidate()
self.stopAnimation = true
}) {
Text("Stop")
}
}
}
}
}

How to stop a stroke repeat animation in SwiftUI

To stop repeating animation it we should replace it with default one, and line width can be just toggled.

Tested with Xcode 13.4 / iOS 15.4

demo

Here is main fixes:

Rectangle()
.stroke(Color.blue, style: StrokeStyle(lineWidth: isRepeatAnimation ? 0 : lineWidth))
.frame(width: 100, height: 100)
.animation(isRepeatAnimation ? repeatAnimation : Animation.default, value: isRepeatAnimation)

Complete test module in project

Restarting perpetual animation after stopping in SwiftUI

I made your project works, you can see the changed code // <<: Here!, the issue was there that you did not show the Animation the changed value! you showed just one time! and after that you keep it the same! if you see your code in your question you are repeating self.phase = .pi * 2 it makes no meaning to Animation! I just worked on your ContentView the all project needs refactor work, but that is not the issue here.

struct ContentView: View {

@State private var phase = 0.0
@State private var waveStrength: Double = 10.0
@State private var waveFrequency: Double = 10.0

@State var isAnimating: Bool = false
@State private var randNum: Int16 = 0
@State private var isNumberInteresting: Bool = false


@State private var timer = Timer.publish(every: 1, on: .main, in: .common).autoconnect()


@State private var stringOfText: String = String() // <<: Here!

func stopTimer() {

self.timer.upstream.connect().cancel()

phase = 0.0 // <<: Here!
}

func startTimer() {

self.timer = Timer.publish(every: 1, on: .main, in: .common).autoconnect()

DispatchQueue.main.asyncAfter(deadline: DispatchTime.now() + DispatchTimeInterval.milliseconds(500)) { phase = .pi * 2 } // <<: Here!

}


func checkNumber(num: Int16) -> Bool {
var isInteresting: Bool = false
if num % 2 == 0 {
isInteresting.toggle()
}
return isInteresting
}



var body: some View {

VStack {

Button(isAnimating ? "Stop" : "Start") { // <<: Here!

isAnimating.toggle() // <<: Here!

isAnimating ? startTimer() : stopTimer() // <<: Here!

}
.font(.title)
.foregroundColor(isAnimating ? Color.red : Color.blue) // <<: Here!


ZStack {

if isAnimating {

ForEach(0..<10) { waveIteration in
Wave(strength: waveStrength, frequency: waveFrequency, phase: phase)
.stroke(Color.blue.opacity(Double(waveIteration) / 3), lineWidth: 1.1)
.offset(y: CGFloat(waveIteration) * 10)
}

}
else {

Line().stroke(Color.blue)

}

}
.frame(height: UIScreen.main.bounds.height * 0.8)
.overlay(isAnimating ? Text(stringOfText) : nil, alignment: .top) // <<: Here!
.onReceive(timer) { _ in

if isAnimating { // <<: Here!

randNum = Int16.random(in: 0..<Int16.max)
isNumberInteresting = checkNumber(num: randNum)

stringOfText = "Random number: \(String(randNum)), interesting: \(String(isNumberInteresting))" // <<: Here!

if isNumberInteresting {
waveFrequency = 50.0
waveStrength = 50.0
} else {
waveFrequency = 10.0
waveStrength = 10.0
}

}
else {
stopTimer() // For safety! Killing Timer in case! // <<: Here!
}

}
.animation(nil, value: stringOfText) // <<: Here!
.animation(Animation.linear(duration: 1).repeatForever(autoreverses: false)) // <<: Here!
.id(isAnimating) // <<: Here!

}


}
}

SwiftUI Custom View repeat forever animation show as unexpected

This looks like a bug of NavigationView: without it animation works totally fine. And it wan't fixed in iOS15.

Working solution is waiting one layout cycle using DispatchQueue.main.async before string animation:

struct LoadingView: View {
@State private var isLoading = false
var body: some View {
Circle()
.trim(from: 0, to: 0.8)
.stroke(Color.red, lineWidth: 5)
.frame(width: 30, height: 30)
.rotationEffect(Angle(degrees: isLoading ? 360 : 0))
.onAppear {
DispatchQueue.main.async {
withAnimation(.linear(duration: 1).repeatForever(autoreverses: false)) {
self.isLoading.toggle()
}
}
}
}
}

Animation triggered using a Button stops a repeatForever animation added onAppear

SwiftUI animations are not added(cumulated), at least for now (SwiftUI 2.0). So here is possible workaround.

Tested with Xcode 12 / iOS 14

demo

struct WaterWaveView: View {
@State var progress: CGFloat = 0.1
@State var phase: CGFloat = 0.5

var body: some View {
VStack {
WaterWave(progress: self.progress, phase: self.phase)
.fill(Color.blue)
.clipShape(Circle())
.frame(width: 250, height: 250)
.animation(phase == 0 ? .default : Animation.linear(duration: 1).repeatForever(autoreverses: false), value: phase)
.animation(.easeOut(duration: 1), value: progress)
.onAppear {
self.phase = .pi * 2
}
Button("Add") {
self.phase = 0
self.progress += 0.1
DispatchQueue.main.asyncAfter(deadline: .now() + 0.2) {
self.phase = .pi * 2
}
}
Button("Reset") {
self.progress = 0.0
}
}
}
}

Repeating Action Continuously In SwiftUI

Animation.basic is deprecated. Basic animations are now named after their curve types: like linear, etc:

var foreverAnimation: Animation {
Animation.linear(duration: 0.3)
.repeatForever()
}

Source:
https://forums.swift.org/t/swiftui-animation-basic-duration-curve-deprecated/27076



Related Topics



Leave a reply



Submit