Why is .animation(_:,value:) affectes other properties' change in SwifftUI?

64 Views Asked by At

I have three properties. Two with different animations on change and one with no animation.

struct Stick: View {
    
    var color: Color
    
    var body: some View {
        GeometryReader { global in
            Path { path in
                path.move(to: CGPoint(x: global.size.width/2, y: 0))
                path.addLine(to: CGPoint(x:global.size.width/2,y: global.size.height))
            }
            .stroke(color,lineWidth: 200)
        }
    }
}

struct SuperStick: View {
    
    @Binding var progress: Float
    @State var offset: CGFloat = 0
    @State var color: Color = .blue.opacity(0.5)
    
    var body: some View {
        GeometryReader { global in
            ZStack(alignment: .bottom) {
                Stick(color: color)
                    .onAppear {
                        if offset >= 1 {
                            offset = 0
                        }
                        offset += 1
                        
                        color = .green.opacity(0.5)
                    }
            }
            .frame(width: global.size.width, height: global.size.height)
            // Animation 1
//            .animation(.linear(duration: 0.1).repeatForever(autoreverses: true), value: color)
            // Animation 2
            .position(x: global.size.width * (0.3+0.4*offset),y: global.size.height/2)
            .animation(.linear(duration: 1).repeatForever(autoreverses: false), value: offset)
            // Animation 3
            .offset(y:CGFloat(1-progress)*global.size.height)
            .animation(.easeInOut(duration: 0.4), value: progress)
            
        }
    }
}

struct TestView: View {
    
    @State var progress: Float = 0.6
    
    var body: some View {
        ZStack{
            SuperStick(progress: $progress)
                .edgesIgnoringSafeArea(.all)
            VStack {
                Stepper("progress: \(String(format: "%.1f", progress))",value: $progress, in: 0.0...1.0, step: 0.2)
                    .padding(.horizontal,40)
            }
        }
    }
}

#Preview {
    TestView()
}

We can see that Animation1 is commented, so there should only be animations for offset and progress. However, we can see that color gets animated as well, with the same speed as offset.

color property gets animated as well

Then I attempted to add an animation for color, hoping color to have its own animation (just uncomment the line below Animation1), however, this time, offset has the same animation as color instead of it's own Animation2.

offset's animation wrongly have Animation1, not 2

And then the weirdest thing happened.

If you comment Animation1 but keep Animation2, and then try to change progress, you'll see that although we only have Animation2 and Animation3, and color is just somehow affected by offset to have Animation2,

when you change progress, Animation2 on offset stops (interrupted by Animation3) but color keeps changing... the weirdest thing

Why is everything happening?

1

There are 1 best solutions below

0
MatBuompy On

You are having those issues because when you, for example, change the progress using the Stepper, the View gets re-rendered, but since the View was already being displayed and the animtions were initialised in the onAppear they did not get a proper reinitialisation. I'm talking about the offset animation specifically. Also, the animation modifier documentation says this:

Applies the given animation to this view when the specified value changes.

So, what happens is that when you use something like this: .animation(.linear(duration: 1).delay(0.1).repeatForever(autoreverses: true), value: color) is that when color changes it animates the entire view to which the animation modifier is applied to, not only the color value. To only animate the color value you should use the withAnimation function, which animates any part of the code that uses the color property.

I've modified your code like so:

struct SuperStick: View {
    
    @Binding var progress: Float
    @State var offset: CGFloat = 0
    @State var color: Color = .blue.opacity(0.5)
    
    var body: some View {
        let _ = print("Rendered")
        GeometryReader { global in
            ZStack(alignment: .bottom) {
                Stick(color: color)
                    .onAppear {
                        
                        /// Called for the first time. The animation now animates from 0 to 1.
                        offset = 1
                        
                        withAnimation(.linear(duration: 1).delay(0.1).repeatForever(autoreverses: true)) {
                            color = .green.opacity(0.5)
                        }
                    }
            }
            .frame(width: global.size.width, height: global.size.height)
            // Animation 1
            //.animation(.linear(duration: 1).delay(0.1).repeatForever(autoreverses: true), value: color)
            // Animation 2
            .position(x: global.size.width * (0.3+0.4*offset),y: global.size.height/2)
            .animation(.linear(duration: 5).repeatForever(autoreverses: false), value: offset)
            // Animation 3
            .offset(y:CGFloat(1-progress)*global.size.height)
            .animation(.easeInOut(duration: 0.4), value: progress)
            .onChange(of: progress) { oldValue, newValue in
                withAnimation(.linear(duration: 1).repeatForever(autoreverses: false)) {
                    /// Called when progress changes. This resets the animation and make it start again.
                    if offset == 1 {
                        offset = 0
                    } else {
                        offset = 1
                    }
                }
            }
            
        }
    }
}

I've used the iOS 17 onChange modifier, but you can use the old one too, you only need to pass one parameter in the closure instead of two. In the onChange I'm reinitialising the offset animation so that when a re-render happens based on progress value change the animation refreshes too. I hope to have been clear with this explaination and not to have commited mistakes. Let me know your thoughts and if this code works for you!