Prevent dismissal of modal view controller in SwiftUI

后端 未结 3 2177
情深已故
情深已故 2020-11-30 02:08

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 swipi

相关标签:
3条回答
  • 2020-11-30 02:27

    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 = true
    
    var body: some View {
    
        Button(action: {
            self.showModal.toggle()
    
        }) {
            Text("Show Modal")
        }.sheet(isPresented: self.$showModal) {
            ModalView()
        }
      }
    }
    

    struct ModalView : View {
    @Environment(\.presentationMode) var presentationMode
    
    let 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()
            }
        }
      }
    }
    
    0 讨论(0)
  • 2020-11-30 02:32

    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
        @Binding var isModal: Bool
        let onDismissalAttempt: (()->())?
        
        func makeUIViewController(context: Context) -> UIHostingController<T> {
            UIHostingController(rootView: view)
        }
        
        func updateUIViewController(_ uiViewController: UIHostingController<T>, context: Context) {
            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: Binding<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.

    0 讨论(0)
  • 2020-11-30 02:32

    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))
            }
        }
    }
    
    0 讨论(0)
提交回复
热议问题