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?

Sunderam Dubey
  • 1
  • 11
  • 20
  • 40
crom87
  • 1,141
  • 9
  • 18

2 Answers2

23

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.

Martijn Pieters
  • 1,048,767
  • 296
  • 4,058
  • 3,343
Asperi
  • 228,894
  • 20
  • 464
  • 690
  • this is annoying - anything in `NavigationView` animates with frame change of the content (aka view) ... everywhere should be added a "bicycle" ( – hbk Jul 14 '21 at 09:59
  • @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 NavigationView – Gligor Nov 02 '21 at 20:11
1

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))
Jiropole
  • 134
  • 1
  • 3