Prevent dismissal of modal view controller in SwiftUI Prevent dismissal of modal view controller in SwiftUI swift swift

Prevent dismissal of modal view controller in SwiftUI


Update for iOS 15

As per pawello2222's answer below, this is now supported by the new interactiveDismissDisabled(_:) API.

struct ContentView: View {    @State private var showSheet = false    var body: some View {        Text("Content View")            .sheet(isPresented: $showSheet) {                Text("Sheet View")                    .interactiveDismissDisabled(true)            }    }}

Pre-iOS-15 answer

I wanted to do this as well, but couldn't find the solution anywhere. The answer that hijacks the drag gesture kinda works, but not when it's dismissed by scrolling a scroll view or form. The approach in the question is less hacky also, so I investigated it further.

For my use case I have a form in a sheet which ideally could be dismissed when there's no content, but has to be confirmed through a alert when there is content.

My solution for this problem:

struct ModalSheetTest: View {    @State private var showModally = false    @State private var showSheet = false        var body: some View {        Form {            Toggle(isOn: self.$showModally) {                Text("Modal")            }            Button(action: { self.showSheet = true}) {                Text("Show sheet")            }        }        .sheet(isPresented: $showSheet) {            Form {                Button(action: { self.showSheet = false }) {                    Text("Hide me")                }            }            .presentation(isModal: self.showModally) {                print("Attempted to dismiss")            }        }    }}

The state value showModally determines if it has to be showed modally. If so, dragging it down to dismiss will only trigger the closure which just prints "Attempted to dismiss" in the example, but can be used to show the alert to confirm dismissal.

struct ModalView<T: View>: UIViewControllerRepresentable {    let view: T    let isModal: Bool    let onDismissalAttempt: (()->())?        func makeUIViewController(context: Context) -> UIHostingController<T> {        UIHostingController(rootView: view)    }        func updateUIViewController(_ uiViewController: UIHostingController<T>, context: Context) {        context.coordinator.modalView = self        uiViewController.rootView = view        uiViewController.parent?.presentationController?.delegate = context.coordinator    }        func makeCoordinator() -> Coordinator {        Coordinator(self)    }        class Coordinator: NSObject, UIAdaptivePresentationControllerDelegate {        let modalView: ModalView                init(_ modalView: ModalView) {            self.modalView = modalView        }                func presentationControllerShouldDismiss(_ presentationController: UIPresentationController) -> Bool {            !modalView.isModal        }                func presentationControllerDidAttemptToDismiss(_ presentationController: UIPresentationController) {            modalView.onDismissalAttempt?()        }    }}extension View {    func presentation(isModal: Bool, onDismissalAttempt: (()->())? = nil) -> some View {        ModalView(view: self, isModal: isModal, onDismissalAttempt: onDismissalAttempt)    }}

This is perfect for my use case, hope it helps you or someone else out as well.


By changing the gesture priority of any view you don't want to be dragged, you can prevent DragGesture on any view. For example for Modal it can be done as bellow:

Maybe it is not a best practice, but it works perfectly

struct ContentView: View {@State var showModal = truevar body: some View {    Button(action: {        self.showModal.toggle()    }) {        Text("Show Modal")    }.sheet(isPresented: self.$showModal) {        ModalView()    }  }}

struct ModalView : View {@Environment(\.presentationMode) var presentationModelet dg = DragGesture()var body: some View {    ZStack {        Rectangle()            .fill(Color.white)            .frame(width: UIScreen.main.bounds.width, height: UIScreen.main.bounds.height)            .highPriorityGesture(dg)        Button("Dismiss Modal") {            self.presentationMode.wrappedValue.dismiss()        }    }  }}


Note: This code has been edited for clarity and brevity.

Using a way to get the current window scene from here you can get the top view controller by this extension here from @Bobj-C

extension UIApplication {    func visibleViewController() -> UIViewController? {        guard let window = UIApplication.shared.windows.first(where: { $0.isKeyWindow }) else { return nil }        guard let rootViewController = window.rootViewController else { return nil }        return UIApplication.getVisibleViewControllerFrom(vc: rootViewController)    }    private static func getVisibleViewControllerFrom(vc:UIViewController) -> UIViewController {        if let navigationController = vc as? UINavigationController,            let visibleController = navigationController.visibleViewController  {            return UIApplication.getVisibleViewControllerFrom( vc: visibleController )        } else if let tabBarController = vc as? UITabBarController,            let selectedTabController = tabBarController.selectedViewController {            return UIApplication.getVisibleViewControllerFrom(vc: selectedTabController )        } else {            if let presentedViewController = vc.presentedViewController {                return UIApplication.getVisibleViewControllerFrom(vc: presentedViewController)            } else {                return vc            }        }    }}

and turn it into a view modifier like this:

struct DisableModalDismiss: ViewModifier {    let disabled: Bool    func body(content: Content) -> some View {        disableModalDismiss()        return AnyView(content)    }    func disableModalDismiss() {        guard let visibleController = UIApplication.shared.visibleViewController() else { return }        visibleController.isModalInPresentation = disabled    }}

and use like:

struct ShowSheetView: View {    @State private var showSheet = true    var body: some View {        Text("Hello, World!")        .sheet(isPresented: $showSheet) {            TestView()                .modifier(DisableModalDismiss(disabled: true))        }    }}