'SwiftUI: Broken explicit animations in NavigationView?
When I put an explicit animation inside a NavigationView, as an undesirable side effect, it animates the initial layout of the NavigationView content. It gets especially bad with infinite animations. Is there a way to disable this side effect?
Example: the image below is supposed to be an animated red loader on a full screen blue background. Instead I get this infinite loop of a scaling blue background:
import SwiftUI
struct EscapingAnimationTest: View {
var body: some View {
NavigationView {
VStack {
Spacer()
EscapingAnimationTest_Inner()
Spacer()
}
.backgroundFill(Color.blue)
}
}
}
struct EscapingAnimationTest_Inner: View {
@State var degrees: CGFloat = 0
var body: some View {
Circle()
.trim(from: 0.0, to: 0.3)
.stroke(Color.red, lineWidth: 5)
.rotationEffect(Angle(degrees: degrees))
.onAppear() {
withAnimation(Animation.linear(duration: 1).repeatForever(autoreverses: false)) {
degrees = 360
}
}
}
}
struct EscapingAnimationTest_Previews: PreviewProvider {
static var previews: some View {
EscapingAnimationTest()
}
}
Solution 1:[1]
Here is fixed part (another my answer with explanations is here).
Tested with Xcode 12 / iOS 14.
struct EscapingAnimationTest_Inner: View {
@State var degrees: CGFloat = 0
var body: some View {
Circle()
.trim(from: 0.0, to: 0.3)
.stroke(Color.red, lineWidth: 5)
.rotationEffect(Angle(degrees: Double(degrees)))
.animation(Animation.linear(duration: 1).repeatForever(autoreverses: false), value: degrees)
.onAppear() {
DispatchQueue.main.async { // << here !!
degrees = 360
}
}
}
}
Update: the same will be using withAnimation
.onAppear() {
DispatchQueue.main.async {
withAnimation(Animation.linear(duration: 1).repeatForever(autoreverses: false)) {
degrees = 360
}
}
}
Solution 2:[2]
Using DispatchQueue.main.async
before outside of the withAnimation
blocks worked for me but this code didn't look very clean.
I found another (and in my opinion cleaner) solution which is this:
- Create a
isAnimating
variable outside of the body
@State var isAnimating = false
Then at the end of your outer VStack, set this variable to true inside onAppear
. Then call rotationEffect
with isAnimating ternary operator and then clal .animation() after. Here is the full code:
var body: some View {
VStack {
// the trick is to use .animation and some helper variables
Circle()
.trim(from: 0.0, to: 0.3)
.stroke(Color.red, lineWidth: 5)
.rotationEffect(Angle(degrees: isAnimating ? 360 : 0))
.animation(Animation.linear(duration:1).repeatForever(autoreverses: false), value: isAnimating)
} //: VStack
.onAppear {
isAnimating = true
}
}
This way you don't need to use DispatchQueue.main.async.
Sources
This article follows the attribution requirements of Stack Overflow and is licensed under CC BY-SA 3.0.
Source: Stack Overflow
Solution | Source |
---|---|
Solution 1 | |
Solution 2 |