简体   繁体   中英

SwiftUI unexpected animations when toggling animations In .onAppear (using a GeometryReader's size)

I have a strange animation behavior in SwiftUI . I've tried to create a minimal view that demonstrates it below.

I want to animate in three circles with a fade and a scale effect (see column "What I Expect" below). However, the size of the circles depends on the width of the view, so I'm using a GeometryReader to get that.

I want to start the animation in .onAppear(perform:) , but at the time that is called, the GeometryReader hasn't set the size property yet. What I end up with is the animation you see in "Unwanted Animation 1" . This is due to the frames being animated from .zero to their correct sizes.

However, whenever I try to disable the animations for the frames by adding a .animation(nil, value: size) modifier, I get an extremely strange animation behavior (see "Unwanted Animation 2" ). This I don't understand at all. It somehow adds a horizontal translation to the animation which makes it look even worse. Any ideas what's happening here and how to solve this?

Strangely, everything works fine if I use an explicit animation like this:

.onAppear {
    withAnimation {
      show.toggle()
    }
}

But I want to understand what's going on here.

Thanks!

What I Expect Unwanted Animation 1 Unwanted Animation 2
在此处输入图像描述 在此处输入图像描述 在此处输入图像描述
import SwiftUI

struct TestView: View {
    @State private var show = false
    @State private var size: CGSize = .zero

    var body: some View {
        VStack {
            circle
            circle
            circle
        }
        .frame(maxWidth: .infinity, maxHeight: .infinity)
        .contentShape(Rectangle())
        .background {
            GeometryReader { proxy in
                Color.clear.onAppear { size = proxy.size }
            }
        }
        .onAppear { show.toggle() }
    }

    private var circle: some View {
        Circle()
            .frame(width: circleSize, height: circleSize)
            .animation(nil, value: size) // This make the circles animate in from the side for some reason (see "Strange Animation 2")
            .opacity(show ? 1 : 0)
            .scaleEffect(show ? 1 : 2)
            .animation(.easeInOut(duration: 1), value: show)
    }

    private var circleSize: Double {
        size.width * 0.2 // Everything works fine if this is a constant
    }
}


struct TestView_Previews: PreviewProvider {
    static var previews: some View {
        TestView()
    }
}

The real size is known after first layout, but .onAppear is called before , so layout (including frame change, which is animatable) goes under animation.

To solve this we need to delay state change a bit (until first layout/render finished), like

.onAppear {
    DispatchQueue.main.asyncAfter(deadline: .now() + 0.2) {
        show.toggle()
    }
}

... and this is why withAnimation also works - it actually delays closure call to next cycle.

Tested with Xcode 13 / iOS 15

The technical post webpages of this site follow the CC BY-SA 4.0 protocol. If you need to reprint, please indicate the site URL or the original address.Any question please contact:yoyou2525@163.com.

 
粤ICP备18138465号  © 2020-2024 STACKOOM.COM