punkbit
punkbit

Reputation: 7707

How to keep the animation running on TabView View change?

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:

enter image description here

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

Answers (2)

punkbit
punkbit

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

user832
user832

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

Related Questions