Custom modal transitions in SwiftUI

SwiftUI in iOS/tvOS 14 and macOS 11 has matchedGeometryEffect(id:in:properties:anchor:isSource:) to animate view transitions between different hierarchies.

Link to Official Documentation

Here's a minimal example:

struct SomeView: View {
    @State var isPresented = false
    @Namespace var namespace
 
    var body: some View {
        VStack {
            Button(action: {
                withAnimation {
                    self.isPresented.toggle()
                }
            }) {
                Text("Toggle")
            }

            SomeSourceContainer {
                MatchedView()
                .matchedGeometryEffect(id: "UniqueViewID", in: namespace, properties: .frame, isSource: !isPresented)
            }

            if isPresented {
                SomeTargetContainer {
                    MatchedTargetView()
                    .matchedGeometryEffect(id: "UniqueViewID", in: namespace, properties: .frame, isSource: isPresented)
                }
            }
        }
    }
}

SwiftUI doesn't do custom modal transitions right now, so we have to use a workaround.

One method that I could think of is to do the presentation yourself using a ZStack. The source frame could be obtained using a GeometryReader. Then, the destination shape could be controlled using frame and position modifiers.

In the beginning, the destination will be set to exactly match position and size of the source. Then immediately afterwards, the destination will be set to fullscreen size in an animation block.

struct ContentView: View {
    @State var isPresenting = false
    @State var isFullscreen = false
    @State var sourceRect: CGRect? = nil

    var body: some View {
        ZStack {
            GeometryReader { proxy in
                Button(action: {
                    self.isFullscreen = false
                    self.isPresenting = true
                    self.sourceRect = proxy.frame(in: .global)
                }) { ... }
            }

            if isPresenting {
                GeometryReader { proxy in
                    ModalView()
                    .frame(
                        width: self.isFullscreen ? nil : self.sourceRect?.width ?? nil, 
                        height: self.isFullscreen ? nil : self.sourceRect?.height ?? nil)
                    .position(
                        self.isFullscreen ? proxy.frame(in: .global).center : 
                            self.sourceRect?.center ?? proxy.frame(in: .global).center)
                    .onAppear {
                        withAnimation {
                            self.isFullscreen = true
                        }
                    }
                }
            }
        }
        .edgesIgnoringSafeArea(.all)
    }
}

extension CGRect {
    var center : CGPoint {
        return CGPoint(x:self.midX, y:self.midY)
    }
}

Tags:

Ios

Swift

Swiftui