SwiftUI: Animation Inside NavigationView
Asked Answered
L

2

7

I am trying to create a simple animation in SwiftUI. It is basically a rectangle that changes its frame, while staying in the center of the parent view.

struct ContentView: View {
    var body: some View {
        NavigationView {
            VStack {
                Text("Text")
                ZStack {
                    Color.blue
                    SquareAnimation().frame(width: 200, height: 200, alignment: .center)
                }
                Text("Text")
            }
        }
    }
}

struct SquareAnimation: View {
    var currentRect = CGRect(x: 0, y: 0, width: 50, height: 50)
    var finalRect = CGRect(x: 0, y: 0, width: 100, height: 100)
    
    private let animation = Animation.easeInOut(duration: 1).repeatForever(autoreverses: true)
    
    @State var animate = false
    
    var body: some View {
        ZStack() {
            Color.clear
            Rectangle()
                .frame(width: animate ? finalRect.width: currentRect.width, height: animate ? finalRect.height: currentRect.height, alignment: .center)
                .animation(animation, value: animate)
                .onAppear() {
                    animate = true
                }
        }
        
    }
} 

The problem is, the black rectangle does not stay in the center if the NavigationView is used.
I have also used explicit animations with no avail. Why does the NavigationView affects the rectangle animation?

Lamarlamarck answered 15/3, 2021 at 17:1 Comment(0)
S
26

The onAppear is called too early when view frame is zero being in NavigationView, so animation is applied to change from zero to value.

Here is valid workaround. Tested with Xcode 12.4 / iOS 14.4

var body: some View {
    ZStack() {
        Color.clear
        Rectangle()
            .frame(width: animate ? finalRect.width: currentRect.width, height: animate ? finalRect.height: currentRect.height, alignment: .center)
            .animation(animation, value: animate)
            .onAppear {
                DispatchQueue.main.async {   
                   // << postpone till end of views construction !!
                    animate = true
                }
            }
    }
}

Note: almost any why question can be answered only by Apple... maybe it is a bug, maybe an implementation specifics.

Shellashellac answered 15/3, 2021 at 17:55 Comment(2)
this is annoying - anything in NavigationView animates with frame change of the content (aka view) ... everywhere should be added a "bicycle" (Recluse
@Asperi, I haven't been able to get this to work with main.async - is there another trick? My view still animates from the side of the screen when in a NavigationViewBallista
R
2

This seems a general problem, in my experience. Here's a generalized solution that works for me – I know it's trivial, but I don't like necessary code, and thus want to minimize its repetition.

struct NavigationViewInitialLayoutWorkaround: ViewModifier {
    @Binding var canAnimate: Bool

    func body(content: Content) -> some View {
        content.onAppear {
            if !canAnimate {
                DispatchQueue.main.async {
                    canAnimate = true
                }
            }
        }
    }
}

Apply this modifier to any views wherever animations are affected by the initial layout problems of a NavigationView ancestor.

@State private var canAnimate: Bool = false

NavigationView {
    AllMyAnimatableViews()
        .animation(canAnimate ? .default : .none)
}
.modifier(NavigationViewInitialLayoutWorkaround(canAnimate: $canAnimate))
Rastus answered 2/4, 2022 at 22:33 Comment(0)

© 2022 - 2024 — McMap. All rights reserved.