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
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
}
}
}
}
}
This creates a timer publisher which asks the timer to fire every
0.1
seconds. It says the timer should run on themain
thread, It should run on thecommon
run loop, which is the one you’ll want to use most of the time. (Run loops letsiOS
handle running code while the user is actively doing something, such as scrolling in a list or tapping a button.)When you tap the circle, the timer is automatically cancelled.
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
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
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
Swift Sorting Array of Structs by String Double
Swift Issue in Passing Variable Number of Parameters
Raw Value of Enumeration, Default Value of a Class/Structure, What's the Different
How to Handle Two Possible Date Formats
Why Can't I Use 'Type' as the Name of an Enum Embedded in a Struct
Unexpectedly Found Nil While Unwrapping an Optional Value While Reading from Ds with Fromcstring
Is There a Neat Way to Represent a Fraction as an Attributed String
When Two Optionals Are Assigned to an If Let Statement, Which One Gets Unwrapped? Swift Language
Swift How to Sort Dict Keys by Byte Value and Not Alphabetically
Setting Observer for Swift Objects/Properties
Swift Tuple Has Unexpected Print Result
Getting the Time Remaining in the Time Interval of a Timer in Swift
How to Change the Order of Functions Triggered
How to Decode Partially Double Serialized JSON String Using 'Codable' Protocol
Swift - How to Set a Singleton to Nil
Accessing Struct from One Class to Another
Getting Data Out of Completionhandler in Swift in Nsurlconnection