Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

Prevent dismissal of modal view controller in SwiftUI

Tags:

ios

swift

swiftui

At WWDC 2019, Apple announced a new "card-style" look for modal presentations, which brought along with it built-in gestures for dismissing modal view controllers by swiping down on the card. They also introduced the new isModalInPresentation property on UIViewController so that you can disallow this dismissal behavior if you so choose.

So far, though, I have found no way to emulate this behavior in SwiftUI. Using the .presentation(_ modal: Modal?), does not, as far as I can tell, allow you to disable the dismissal gestures in the same way. I also attempted putting the modal view controller inside a UIViewControllerRepresentable View, but that didn't seem to help either:

struct MyViewControllerView: UIViewControllerRepresentable {     func makeUIViewController(context: UIViewControllerRepresentableContext<MyViewControllerView>) -> UIHostingController<MyView> {         return UIHostingController(rootView: MyView())     }      func updateUIViewController(_ uiViewController: UIHostingController<MyView>, context: UIViewControllerRepresentableContext<MyViewControllerView>) {         uiViewController.isModalInPresentation = true     } } 

Even after presenting with .presentation(Modal(MyViewControllerView())) I was able to swipe down to dismiss the view. Is there currently any way to do this with existing SwiftUI constructs?

like image 695
Jumhyn Avatar asked Jun 16 '19 01:06

Jumhyn


1 Answers

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.

like image 96
Guido Hendriks Avatar answered Sep 28 '22 20:09

Guido Hendriks