Reputation: 7707
I've recently found that when an animation is running forever and the View
is switched through the TabView
, the animation state is lost. While this is expected behaviour, I'd like to understand if there is a way to keep the animation running when back to the animated View, after the TabView View change?
Here's a quick example, showing the behaviour in SwiftUI:
import SwiftUI
class MyState: ObservableObject {
@Published var animate: Bool = false
}
struct ContentView: View {
var body: some View {
TabView {
AnimationView()
.tabItem {
Image(systemName: "eye")
}
Text("Some View")
.tabItem {
Image(systemName: "play")
}
}
}
}
struct AnimationView: View {
@EnvironmentObject var myState: MyState
var body: some View {
VStack {
Text(self.myState.animate ? "Animation running" : "Animation stopped")
.padding()
ZStack {
Circle()
.stroke(style: StrokeStyle(lineWidth: 12.0))
.foregroundColor(Color.black)
.opacity(0.3)
Circle()
.trim(
from: 0,
to: self.myState.animate ? 1.0 : 0.0
)
.stroke(
style: StrokeStyle(lineWidth: 12.0, lineCap: .round, lineJoin: .round)
)
.animation(
Animation
.linear(duration: self.myState.animate ? 1.0 : 0.0)
.repeatForever(autoreverses: false)
)
}.frame(width: 200, height: 200)
Button(action: {
self.myState.animate.toggle()
}) {
Text("Toggle animation")
.padding()
}
}
}
}
struct ContentView_Previews: PreviewProvider {
static var previews: some View {
ContentView()
.environmentObject(MyState())
}
}
Below you can see the behaviour:
What I'd expect to control, is get back to the View where the animation runs and see the state it'd be at the current time - this is, imagining that the animation is looping when switching Views and back to the View find it in the correct point in "running time" or "real-time".
Obs: The State is handled in MyState
an EnvironmentObject
and it's not local (@State) in the View
Upvotes: 1
Views: 962
Reputation: 7707
After some tests, the best solution I found this far is to follow a few rules I've stated below. Have in mind that this is only useful if you can track the exact animation position in time. So please check the process bellow:
1) Stop the animation .onDisappear
The reason is that state changes between views that have animated elements will cause unexpected behaviour, for example, the element positioning be completely off and so on.
self.myState.animate = false
2) Keep track of progress
My use-case is a long computation that starts from 0 and ends in X and MyState
has methods that provide me with the current position
in the computation. This value is then is calculated in the range* 0 to 1, so that it's possible to put it into the SwiftUI Animation
.
3) Jumpstart to a specific time
This part is not supported yet in SwiftUI Animation, but let's say that you have an animation that takes 5 seconds, you're at 50% playtime, so you'd jump to 2.5 seconds where you'd want to start play.
An implementation for this, if you've experienced Javascript and way before, ActionScript is GSAP doc for seek
(https://greensock.com/docs/v2/Core/Animation/seek())
4) Re-trigger the animation
We started by stopping the animation by changing the state, to make SwiftUI start the animation all we need to do at this stage is to change the state once again
self.myState.animate = true
There might be other ways to keep the animation running, but at the time of writing haven't found it documented, so there was a need to "start animation from any point".
Upvotes: 1
Reputation: 836
So I added an extra parameter to your MyState
to store the current animation status when the user switches between tabs. Below is the updated code. I have also added comments in the code for better understanding.
class MyState: ObservableObject {
@Published var animate: Bool = false
var isAnimationActive = false // Store animate status when user switches between tabs
}
struct AnimationView: View {
@EnvironmentObject var myState: MyState
var body: some View {
VStack {
Text(self.myState.animate ? "Animation running" : "Animation stopped")
.padding()
ZStack {
Circle()
.stroke(style: StrokeStyle(lineWidth: 12.0))
.foregroundColor(Color.black)
.opacity(0.3)
Circle()
.trim(
from: 0,
to: self.myState.animate ? 1.0 : 0.0
)
.stroke(
style: StrokeStyle(lineWidth: 12.0, lineCap: .round, lineJoin: .round)
)
.animation(
Animation
.linear(duration: self.myState.animate ? 1.0 : 0.0)
.repeatForever(autoreverses: false)
)
}.frame(width: 200, height: 200)
Button(action: {
self.myState.animate.toggle()
}) {
Text("Toggle animation")
.padding()
}
}
.onAppear(perform: {
if self.myState.isAnimationActive {
/*switching the animation inside withAnimation block is important*/
withAnimation {
self.myState.animate = true
}
}
})
.onDisappear {
self.myState.isAnimationActive = self.myState.animate
/*The property needs to be set to false as SwiftUI compare the
views before re-rendering them*/
self.myState.animate = false
}
}
}
Upvotes: 2