Search code examples
animationswiftuiswiftui-animation

SwiftUI unexpected position changes during animation


I have made a simple animation in SwiftUI which repeats forever, directly when starting the app. However when running the code the animated circle goes to the left top and then back to its normal position while doing the rest of the animation. This top left animation is not expected and nowhere written in my code.

Sample code:

struct Loader: View {
    
    @State var animateFirst = false

    @State var animationFirst = Animation.easeInOut(duration: 3).repeatForever()
    var body: some View {
        ZStack {
            Circle().trim(from: 0, to: 1).stroke(ColorsPalette.Primary, style: StrokeStyle(lineWidth: 3, lineCap: .round)).rotation3DEffect(
                .degrees(self.animateFirst ? 360 : -360),
                axis: (x: 1, y: 0, z: 0), anchor: .center).frame(width: 200, height: 200, alignment: .center).animation(animationFirst).onAppear {
                self.animateFirst.toggle()
            }
        }
    }
    
}

and then showing it in a view like this:

struct LoaderViewer: View {
    
    var body: some View {
        VStack {
            Loader().frame(width: 200, height: 200)
        }
}

Solution

  • I don't know exactly why this happens but I have a vague idea. Because the animation starts before the view is fully rendered in the width an height, the view starts in the op left corner. The animation takes the starting position in the animation and the change to its normal position and keep repeating this part with the given animation. Which causes this unexpected behaviour.

    I think it is a bug, but with the theory above I have created a workaround for now. In the view where we place the loader we have to wait till the parent view is fully rendered before adding the animated view (Loader view), we can do that like this:

    struct LoaderViewer: View {
    
        @State showLoader = false
    
        var body: some View {
            VStack {
                if showLoader {
                    Loader().frame(width: 200, height: 200)
                }
            }.onAppear {
                DispatchQueue.main.asyncAfter(deadline: .now() + 0.01) {
                    showLoader = true
                }
            }
        }
    }