How to Modally push next Screen to be full in SwiftUI

Viewed 6370

how can I push a next SwiftUI View but present it over the full screen without swiping down feature like the Xcode 10 modal presentation.

My current implementation, but it's not pushing onto the fullscreen (dragging down enabled and the gap at the top):

btn
.presentation(
      !showModal.value ?
           nil :
           Popover(content: destination, dismissHandler: onTrigger ?? {})
)
3 Answers

I think the only way to do this at the moment is to use overlay() or a ZStack. I can't seem to get a transition working when using overlay() but I can when using a ZStack

Just make sure your modal view fills the screen with something like a List or using Spacer() otherwise you will still see the other view behind it

struct ContentView: View {
    
    @State var showModal = false
    
    let transition = AnyTransition.move(edge: .bottom)
    
    var body: some View {
        ZStack {
            VStack {
                Button(action: {
                    withAnimation {
                        self.showModal = true
                    }
                }) {
                    Text("Show Modal")
                }
            }
            
            if self.showModal {
                ModalView()
                    .background(Color.white)
                    .transition(transition)
            }
        }
    }
}

The fullScreenCover() modifier

iOS 14 has a new SwiftUI modifier called fullScreenCover(). It is a full screen modal presentation style. It works almost identically to regular sheets. For example, this will present a full screen modal view when the button is pressed:

struct ContentView: View {
 Button("Present Full Screen Modal!") {
            self.isPresented.toggle()
        }
        .fullScreenCover(isPresented: $isPresented) {
            NavigationView {
                FullScreenModalView()
                    .toolbar {
                        ToolbarItem(placement: .navigationBarTrailing) {
                            Button(action: {
                                isPresented = false
                            }, label: {
                                Text("Dismiss")
                            })
                        }
                    }
            }
        }
    }
}

And the destination view can be dismissed with a swipe down or better as Apple recommend, adding a dismiss button (already added in the navigation toolbar as above):

struct FullScreenModalView: View {
    var body: some View {
        ZStack {
            Color.pink
            Text("This my full screen modal view")
                .foregroundColor(.white)
        }
        .ignoresSafeArea()
        .onTapGesture {
            presentationMode.wrappedValue.dismiss()
        }
    }
}

The short answer is that there is NO good way to do this right now.

Here's a few alternatives:

Use a UIKit + Swift Hybrid

See this Present a new view in SwiftUI

Attach a conditional View

The first answer cover this solution. Basically you present you "modal" view on top of or instead of the "base" view using a Bool.

Something like this:


struct ModalView: View {
    var closeAction: (() -> Void) = {}
    var body: some View {
        ZStack {
            Color.blue.edgesIgnoringSafeArea(.all)
            VStack {
                Text("I am a modal.")
                    .font(.largeTitle)
                    .fontWeight(.bold)
                    .foregroundColor(.white)
                    .padding()
                Button(action: {
                    self.closeAction()
                }, label: {
                    Text("OK, BYE!")
                        .foregroundColor(.white)
                        .padding()
                        .overlay(
                            RoundedRectangle(cornerRadius: 5)
                                .stroke(Color.white, lineWidth: 1)
                    )
                })
            }
        }
    }
}

struct BaseView: View {
    @State private var showModal = false
    var body: some View {
        ZStack {
            if showModal {
                ModalView(closeAction: {
                    withAnimation(.easeOut(duration: 0.25)) { self.showModal = false }
                }).transition(.slideBottom)
            } else {
                VStack {
                    Button(action: {
                        withAnimation(.easeOut(duration: 0.25)) {
                            self.showModal = true
                        }
                    }, label: {
                        Text("Open Modal")
                            .padding()
                            .overlay(
                                RoundedRectangle(cornerRadius: 5)
                                    .stroke(Color.blue, lineWidth: 1)
                        )
                    })
                }
            }
        }.statusBar(hidden: true)
    }
}

It has its own set of issues:

  • Not really a modal presentation(?). The "base" view and all its view hierarchy are still present under the "modal". This can be a pain if you want to support accesibility. You can of course present the conditional modal instead of rather than on top of the base view. This setup works but needs to be written rather clever to scale well.
  • You will have to add your own animated transition.
  • Doesn't work if the base view is embeded in a NavigationView root unless the its navigation bar is hidden
  • Doesn't work if the base view is embeded in a NavigationView child, that is a View presented using NavigationLink unless the its navigation bar and back button are hidden
  • In general it ceases to be a fullscreen modal when the modifier is added to a View that is not the root View in a given layout.

For a full exploration of this see https://github.com/piterwilson/SwiftUI-Modal-on-iPad/tree/master/iPadConditionalViewModal and this ViewModifier I made to make the code a bit cleaner https://github.com/piterwilson/SwiftUI-FullscreenModalViewModifier

Use NavigationView + NavigationLink

You an also present fullscreen using NavigationView + NavigationLink but the biggest problem is that you won't be able to customize the animation. It looks something like this:

struct ModalView: DismissableView {
    @Environment(\.presentationMode) var presentationMode: Binding<PresentationMode>
    var body: some View {
        ZStack {
            Color.green.edgesIgnoringSafeArea(.all)
            VStack {
                Text("I am a modal.")
                    .font(.largeTitle)
                    .fontWeight(.bold)
                    .foregroundColor(.white)
                    .padding()
                Button(action: {
                    self.dismiss()
                }, label: {
                    Text("OK, BYE!")
                        .foregroundColor(.white)
                        .padding()
                        .overlay(
                            RoundedRectangle(cornerRadius: 5)
                                .stroke(Color.white, lineWidth: 1)
                    )
                })
            }
        }.navigationBarBackButtonHidden(true)
    }
}

struct BaseView: View {
    var body: some View {
        NavigationView {
            VStack {
                NavigationLink(destination: ModalView()) {
                    Text("Open Modal")
                    .padding()
                    .overlay(
                        RoundedRectangle(cornerRadius: 5).stroke(Color.blue, lineWidth: 1)
                    )
                }
            }
        }
        .navigationViewStyle(StackNavigationViewStyle())
    }
}

It also has problems:

Related