SwiftUI VStack Animation looks broken - Animated View will not change position

1.2k views Asked by At

Let's take a look at the following Code Snippet:

struct ContentView: View {
    @State private var isViewHidden: Bool = false
    let data = [1,2,3,4,5,6,7]
    public var body: some View {
        VStack {
            Button("Hide", action: {
                withAnimation {
                    isViewHidden.toggle()
                }
            })
            ForEach(data, id: \.self) { _ in
                VStack {
                    Text("Foo")
                    if isViewHidden {
                        Text("Bar").animation(nil)
                    }
                }.padding().background(Color.green)
            }
        }
    }
}

I would expect that the Text("Hide") will animate the position inside the parent VStack. But it will stick to its last position and fade from there and also animate back to that position. Is there a possibility to give this animation a more natural feel so it will animate inside its parent.

Here is a gif that visualizes the Problem: enter image description here

1

There are 1 answers

2
vacawama On BEST ANSWER

The problem is that Bar is a new View and thus just fades or appears at its final destination, so its position doesn't animate because it previously wasn't on screen.

The trick is to keep Bar on screen at all times and just adjust its opacity. .offset is used to keep the view from growing due to the word Bar that isn't always present. A blank text view Text(" ") is added or removed to cause the view to grow as before.

Here's the workaround that gives a better looking animation:

struct ContentView: View {
    @State private var isViewHidden: Bool = false
    let data = [1,2,3,4,5,6,7]
    public var body: some View {
        VStack {
            Button("Hide", action: {
                withAnimation {
                    isViewHidden.toggle()
                }
            })
            ForEach(data, id: \.self) { _ in
                VStack {
                    ZStack {
                        Text("Foo")
                        Text("Bar")
                            .offset(y: 28)
                            .opacity(isViewHidden ? 0 : 1)
                    }
                    if !isViewHidden {
                        Text(" ")
                    }

                }.padding().background(Color.green)
            }
        }
    }
}

Here it is running in the simulator:

Demo in the siumlator


Solution 2: Use .matchedGeometryEffect

Here's a solution that uses .matchedGeometryEffect to animate between Text("Bar").frame(height: 0) and Text("Bar").

struct ContentView: View {
    @Namespace var namespace
    @State private var isViewHidden: Bool = false
    let data = [1,2,3,4,5,6,7]
    public var body: some View {
        VStack {
            Button("Hide") {
                withAnimation {
                    isViewHidden.toggle()
                }
            }
            ForEach(data, id: \.self) { id in
                VStack {
                    Text("Foo")
                    if isViewHidden {
                        Text("Bar").frame(height: 0)
                            .matchedGeometryEffect(id: id, in: namespace)
                    } else {
                        Text("Bar")
                            .matchedGeometryEffect(id: id, in: namespace)
                    }
                }
                .padding().background(Color.green)
            }
        }
    }
}