phaseAnimator: prevent opacity change?

I'm trying to get a view to move laterally, and when it has reached the end of that animation, snap (i.e. without animation) back to the starting position (seemingly instantly). At the same time it snaps back, the view should display something different (in this example that is just text).

That last part disrupts what seemed to be a straightforward application of phaseAnimator, however, when changing the displayed text, the view's opacity goes to zero, moves (invisibly and instantly) to the new position and then opacity goes back to one. My suspicion is that this has something to do with the way SwiftUI understands whether a view is the same or not via some kind of ID, but I'm not sure how to troubleshoot that.

Here is the code:

struct ContentView: View
{
    @State
    private var value: Int = 0
    
    var body: some View
    {
        VStack
        {
            GeometryReader { geometry in
                /// Replacing this with `Text("\(value)")` will cause the unintended effect
                Text("Static text")
                    .phaseAnimator([0, 1], trigger: value) { view, phase in
                        view
                            .offset(x: phase == 1 ? 100 : 0)
                    } animation: { phase in
                        switch phase
                        {
                            case 1: .linear(duration: 1)
                            default: nil
                        }
                    }
            }
            
            Button("Start Animation")
            {
                value += 1
            }
        }
    }
}

Any suggestions, or even solutions would be appreciated!

Answered by BabyJ in 788896022

Could you not use two separate state variables?

This code works:

struct ContentView: View {
    @State private var value: Int = 0
    @State private var trigger = 0

    var body: some View {
        VStack {
            GeometryReader { geometry in
                Text("\(value)")
                    .phaseAnimator([0, 1], trigger: trigger) { view, phase in
                        view
                            .offset(x: phase == 1 ? 100 : 0)
                    } animation: { phase in
                        switch phase {
                        case 1:
                            .linear(duration: 1)
                        default:
                            nil
                        }
                    }
            }

            Button("Start Animation") {
                withAnimation {
                    trigger += 1
                } completion: {
                    value += 1
                }
            }
        }
    }
}
  • The trigger property is for triggering the animation.
  • The value property only gets updated when the animation is complete, i.e. on the "snap back".

This way the trigger and what is shown in the view cannot interfere with each other. It also ensures the text value can be updated after the animation, not before, like you said you wanted.


If this solution is something you're not looking for, or there is more surrounding this problem, then I am still here to help. Just let me know.

Accepted Answer

Could you not use two separate state variables?

This code works:

struct ContentView: View {
    @State private var value: Int = 0
    @State private var trigger = 0

    var body: some View {
        VStack {
            GeometryReader { geometry in
                Text("\(value)")
                    .phaseAnimator([0, 1], trigger: trigger) { view, phase in
                        view
                            .offset(x: phase == 1 ? 100 : 0)
                    } animation: { phase in
                        switch phase {
                        case 1:
                            .linear(duration: 1)
                        default:
                            nil
                        }
                    }
            }

            Button("Start Animation") {
                withAnimation {
                    trigger += 1
                } completion: {
                    value += 1
                }
            }
        }
    }
}
  • The trigger property is for triggering the animation.
  • The value property only gets updated when the animation is complete, i.e. on the "snap back".

This way the trigger and what is shown in the view cannot interfere with each other. It also ensures the text value can be updated after the animation, not before, like you said you wanted.


If this solution is something you're not looking for, or there is more surrounding this problem, then I am still here to help. Just let me know.

phaseAnimator: prevent opacity change?
 
 
Q