Search code examples
swiftuiswiftui-animation

how can I get my Child view to respect the Parent's animation, even when it has its own Child animation?


I have a view that transitions in from the bottom. It's a ZStack, so it has a Child View as well.

struct ContentView: View {
    @State private var presentParent = false
    
    var body: some View {
        if presentParent {
            ZStack {
                Rectangle()
                    .foregroundColor(.blue)
                    .frame(width: 300, height: 200)
                ChildView()
            }
            .transition(.move(edge: .bottom))
        } else {
            Color.clear
                .onAppear {
                    withAnimation(.linear(duration: 2)) {
                        presentParent = true
                    }
                }
        }
    }
}

struct ChildView: View {
    @State private var presentChild = false
    
    var body: some View {
            Text("EXAMPLE")
                .font(.largeTitle)
                .foregroundStyle(.red)
    }
}

That works just fine. The ChildView moves in from the bottom with the Parent.

However, if my ChildView has its own transition, it no longer moves in from the bottom. it just slides in from the right while the Parent slides in from the bottom.

NEW ChildView code:

struct ChildView: View {
    @State private var presentChild = false
    
    var body: some View {
        if presentChild {
            Text("EXAMPLE")
                .font(.largeTitle)
                .foregroundStyle(.red)
                .transition(.move(edge: .trailing))
        } else {
            Color.clear
                .onAppear {
                    withAnimation {
                        presentChild = true
                    }
                }
            
        }
    }
}

enter image description here

I've toyed around with using DispatchQueue.main.asyncAfter, but this feels kinda hacky, especially if I'm going to have several different nested ChildViews. Also, it doesn't achieve what I'm going for.

DESIRED OUTPUT: I would like to see the EXAMPLE text slide in from the right, while it is moving up from the bottom with the ParentView.


Solution

  • First, a few observations:

    • In your first version, where the child is not animated separately, you will notice that the blue rectangle is already visible when the transition begins.
    • In the second version, the animation appears to be staggered. First, the text moves in from the side, then the blue rectangle moves in from off-screen below. However, they are not really staggered. The reason why the blue rectangle appears later is because the transition is happening for the full height of the screen. So the initial delay is the time it takes for the blank space in the top half of the screen to slide in. If you give it a colored background then you can actually see it happening.
    • In the second version, if you apply the frame 300x200 to the ZStack instead of to the Rectangle then the transition starts with the blue rectangle already visible, like in the first version. So the height of the ZStack is what determines the start position for the blue rectangle.

    The animations can be brought together by adding .drawingGroup to the parent. To see everything moving together, it helps if the duration of the child animation is the same as the parent, otherwise it finishes much earlier.

    The transition will start off-screen if maxHeight: .infinity is set on the ZStack, but then there is an initial delay while the blank top-half of the screen slides in. Also, much of the child animation gets missed, because it happens off-screen. So to have it start without delay, you can use a GeometryReader to measure the size of the screen and set a height on the ZStack that omits the blank space above the rectangle.

    Like this:

    struct ContentView: View {
        @State private var presentParent = false
        let parentHeight: CGFloat = 200
    
        var body: some View {
            GeometryReader { screen in
                if presentParent {
                    ZStack {
                        Color.blue
                        ChildView()
                    }
                    .frame(width: 300, height: parentHeight)
                    .drawingGroup()
                    .frame(
                        maxWidth: .infinity,
                        maxHeight: (screen.size.height + parentHeight) / 2,
                        alignment: .bottom
                    )
                    .transition(.move(edge: .bottom))
                }
            }
            .onAppear {
                withAnimation(.linear(duration: 2)) {
                    presentParent = true
                }
            }
        }
    }
    
    struct ChildView: View {
        @State private var presentChild = false
    
        var body: some View {
            if presentChild {
                Text("EXAMPLE")
                    .font(.largeTitle)
                    .foregroundStyle(.red)
                    .transition(.move(edge: .trailing))
            } else {
                Color.clear
                    .onAppear {
                        withAnimation(.linear(duration: 2)) {
                            presentChild = true
                        }
                    }
    
            }
        }
    }