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!
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.