Welcome to OGeek Q&A Community for programmer and developer-Open, Learning and Share
Welcome To Ask or Share your Answers For Others

Categories

0 votes
403 views
in Technique[技术] by (71.8m points)

ios - Prevent dismissal of modal view controller in 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?

See Question&Answers more detail:os

与恶龙缠斗过久,自身亦成为恶龙;凝视深渊过久,深渊将回以凝视…
Welcome To Ask or Share your Answers For Others

1 Reply

0 votes
by (71.8m points)

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.


与恶龙缠斗过久,自身亦成为恶龙;凝视深渊过久,深渊将回以凝视…
OGeek|极客中国-欢迎来到极客的世界,一个免费开放的程序员编程交流平台!开放,进步,分享!让技术改变生活,让极客改变未来! Welcome to OGeek Q&A Community for programmer and developer-Open, Learning and Share
Click Here to Ask a Question

...