0

Imagine that you have a desk with a drawer. In the closed state, you can only see the table because the drawer is beneath it. In the opened state, you see the drawer which is below the desk (because it slides down). How would I animate this such that the drawer slides out from beneath the desk in SwiftUI?

This view will be work inside a list view, so a row's dimensions must accommodate any change. So I don't know if an offset based animation would work here (as shown in this question). The top view (the desk) and the bottom view (the drawer) are the same size.

Here's what I've tried...

In the code below, I use Rectangles but eventually I will use views.

Matched Geometry Effect

struct ContentView: View {
    @Namespace private var animation
    @State var shown: Bool = false
    var body: some View {
        Group {
            if !shown {
                ZStack {
                    Rectangle().frame(width: 300, height: 80, alignment: .center)
                        .foregroundColor(.blue)
                        .matchedGeometryEffect(id: "Bottom", in: animation)
                    Rectangle().frame(width: 300, height: 80, alignment: .center)
                        .matchedGeometryEffect(id: "Top", in: animation)
                }
            } else {
                VStack(spacing: 0) {
                    Rectangle().frame(width: 300, height: 80, alignment: .center)
                        .matchedGeometryEffect(id: "Top", in: animation)
                    Rectangle().frame(width: 300, height: 80, alignment: .center)
                        .foregroundColor(.blue)
                        .matchedGeometryEffect(id: "Bottom", in: animation)
                }
            }
        }.onTapGesture {
            withAnimation(.linear(duration: 1)) {
                shown.toggle()
            }
        }
    }
}

Produces this...

matched geometry

Transitions

struct ContentView: View {
    @State var shown: Bool = false
    var body: some View {
        VStack(spacing: 0) {
            Rectangle().frame(width: 300, height: 80, alignment: .center)
                .zIndex(0)
            if shown {
                Rectangle().frame(width: 300, height: 80, alignment: .center)
                    .foregroundColor(.blue)
                    .zIndex(-1)
                    .transition(.move(edge: .top))
            }
        }.onTapGesture {
            withAnimation(.linear(duration: 1)) {
                shown.toggle()
            }
        }
    }
}

transition

DanubePM
  • 1,526
  • 1
  • 10
  • 24

1 Answers1

0

If the size of your drawer (height) is a known constant I'd go like this:

var body: some View {
    ZStack {
        Rectangle()
            .foregroundColor(.blue)
            .frame(width: 300, height: 80, alignment: .center)
            .offset(y: shown ? 80 : 0)
            .animation(.easeInOut(duration: 0.2))
        
        Rectangle()
            .frame(width: 300, height: 80, alignment: .center)
    }
    .onTapGesture {
        shown.toggle()
    }
}
Maciek Czarnik
  • 5,950
  • 2
  • 37
  • 50