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 UIViewControllerRepresentable
View
, 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?
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()
}
}
}
}
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.
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.")
}
}
}
}