web-dev-qa-db-fra.com

Empêcher le licenciement du contrôleur de vue modale dans SwiftUI

Lors de la WWDC 2019, Apple a annoncé un nouveau look "de type carte" pour les présentations modales, ce qui a entraîné des gestes intégrés pour rejeter les contrôleurs de vue modale en glissant vers le bas sur la carte. Ils ont également a introduit la nouvelle propriété isModalInPresentation sur UIViewController afin que vous puissiez interdire ce comportement de rejet si vous le souhaitez.

Jusqu'à présent, cependant, je n'ai trouvé aucun moyen d'émuler ce comportement dans SwiftUI. L'utilisation de la fonction .presentation(_ modal: Modal?) ne vous permet pas, pour autant que je sache, de désactiver les gestes de licenciement de la même manière. J'ai également essayé de mettre le contrôleur de vue modale dans un UIViewControllerRepresentableView, mais cela ne semble pas aider non plus:

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
    }
}

Même après avoir présenté .presentation(Modal(MyViewControllerView())) j'ai pu balayer vers le bas pour fermer la vue. Existe-t-il actuellement un moyen de le faire avec les constructions SwiftUI existantes?

18
Jumhyn

En changeant le gesture priority de toute vue que vous ne souhaitez pas faire glisser, vous pouvez empêcher DragGesture sur n'importe quelle vue. Par exemple pour Modal cela peut être fait comme ci-dessous:

Ce n'est peut-être pas une meilleure pratique, mais cela fonctionne parfaitement

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()
        }
    }
  }
}
5
FRIDDAY

Je voulais aussi le faire, mais je n'ai trouvé la solution nulle part. La réponse acceptée fonctionne un peu, mais pas lorsqu'elle est rejetée en faisant défiler une vue ou un formulaire de défilement. L'approche dans la question est également moins hacky, donc j'ai approfondi.

Pour mon cas d'utilisation, j'ai un formulaire dans une feuille qui pourrait idéalement être rejeté lorsqu'il n'y a pas de contenu, mais doit être confirmé par une alerte lorsqu'il y a du contenu.

Ma solution à ce problème:

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")
            }
        }
    }
}

La valeur d'état showModally détermine si elle doit être affichée de façon modale. Si c'est le cas, le faire glisser vers le bas pour déclencher ne déclenchera que la fermeture qui affiche simplement "Tentative de rejet" dans l'exemple, mais peut être utilisé pour afficher l'alerte pour confirmer le licenciement.

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)
    }
}

C'est parfait pour mon cas d'utilisation, j'espère que cela vous aidera aussi bien que quelqu'un d'autre.

1
Guido Hendriks

En utilisant un moyen pour obtenir la scène de fenêtre actuelle de ici vous pouvez obtenir le contrôleur de vue de dessus par cette extension ici de @ Bobj-C

extension UIWindow {

    func visibleViewController() -> UIViewController? {
        if let rootViewController: UIViewController = self.rootViewController {
            return UIWindow.getVisibleViewControllerFrom(vc: rootViewController)
        }
        return nil
    }

    static func getVisibleViewControllerFrom(vc:UIViewController) -> UIViewController {
        if let navigationController = vc as? UINavigationController,
            let visibleController = navigationController.visibleViewController  {
            return UIWindow.getVisibleViewControllerFrom( vc: visibleController )
        } else if let tabBarController = vc as? UITabBarController,
            let selectedTabController = tabBarController.selectedViewController {
            return UIWindow.getVisibleViewControllerFrom(vc: selectedTabController )
        } else {
            if let presentedViewController = vc.presentedViewController {
                return UIWindow.getVisibleViewControllerFrom(vc: presentedViewController)
            } else {
                return vc
            }
        }
    }
}

et les combiner dans une autre extension comme

extension UIApplication {
    func disableModalDismiss(_ disableDismiss: Bool) -> Bool {
        guard let window = UIApplication.shared.windows.first(where: { $0.isKeyWindow }) else { return false }
        guard let visibleController = window.visibleViewController() else { return false }
        visibleController.isModalInPresentation = disableDismiss
        return true
    }
}

et utiliser dans votre code de vue SwiftUI comme

struct ShowSheetView: View {

    @State private var showSheet = true

    var body: some View {
        Text("Hello, World!")
        .sheet(isPresented: $showSheet) {
            TestView()
        }
    }
}

struct TestView: View {

    @Environment(\.presentationMode) private var presentationMode

    var body: some View {
        VStack {
            if UIApplication.shared.disableModalDismiss(true) {
                Text("Swipe to dismiss is now disabled")
                Button("Dismiss") {
                    self.presentationMode.wrappedValue.dismiss()
                }
            } else {
                Text("An error occured.")
            }
        }
    }
}
1
R. J.