web-dev-qa-db-fra.com

Comment redonner au geste de balayage dans SwiftUI le même comportement que dans UIKit (interactivePopGestureRecognizer)

La reconnaissance interactive des gestes pop devrait permettre à l'utilisateur de revenir à la vue précédente dans la pile de navigation lorsqu'il glisse plus de la moitié de l'écran (ou quelque chose autour de ces lignes). Dans SwiftUI, le geste n'est pas annulé lorsque le balayage n'est pas assez loin.

SwiftUI: https://imgur.com/xxVnhY7

UIKit: https://imgur.com/f6WBUne


Question:

Est-il possible d'obtenir le comportement UIKit lors de l'utilisation des vues SwiftUI?


Tentatives

J'ai essayé d'incorporer un UIHostingController dans un UINavigationController mais cela donne exactement le même comportement que NavigationView.

struct ContentView: View {
    var body: some View {
        UIKitNavigationView {
            VStack {
                NavigationLink(destination: Text("Detail")) {
                    Text("SwiftUI")
                }
            }.navigationBarTitle("SwiftUI", displayMode: .inline)
        }.edgesIgnoringSafeArea(.top)
    }
}

struct UIKitNavigationView<Content: View>: UIViewControllerRepresentable {

    var content: () -> Content

    init(@ViewBuilder content: @escaping () -> Content) {
        self.content = content
    }

    func makeUIViewController(context: Context) -> UINavigationController {
        let Host = UIHostingController(rootView: content())
        let nvc = UINavigationController(rootViewController: Host)
        return nvc
    }

    func updateUIViewController(_ uiViewController: UINavigationController, context: Context) {}
}
8

J'ai fini par remplacer les NavigationView et NavigationLink par défaut pour obtenir le comportement souhaité. Cela semble si simple que je dois ignorer quelque chose que les vues SwiftUI par défaut font?

NavigationView

J'encapsule un UINavigationController dans un super simple UIViewControllerRepresentable qui donne le UINavigationController à la vue de contenu SwiftUI en tant qu'objet environnement. Cela signifie que le NavigationLink peut plus tard récupérer cela tant qu'il se trouve dans le même contrôleur de navigation (les contrôleurs de vue présentés ne reçoivent pas les objets d'environnement), ce qui est exactement ce que nous voulons.

Remarque: La NavigationView a besoin de .edgesIgnoringSafeArea(.top) et je ne sais pas encore comment définir cela dans la structure elle-même. Voir l'exemple si votre nvc coupe en haut.

struct NavigationView<Content: View>: UIViewControllerRepresentable {

    var content: () -> Content

    init(@ViewBuilder content: @escaping () -> Content) {
        self.content = content
    }

    func makeUIViewController(context: Context) -> UINavigationController {
        let nvc = UINavigationController()
        let Host = UIHostingController(rootView: content().environmentObject(nvc))
        nvc.viewControllers = [Host]
        return nvc
    }

    func updateUIViewController(_ uiViewController: UINavigationController, context: Context) {}
}

extension UINavigationController: ObservableObject {}

NavigationLink

Je crée un lien de navigation personnalisé qui accède aux environnements UINavigationController pour pousser un UIHostingController hébergeant la vue suivante.

Remarque: Je n'ai pas implémenté les selection et isActive que SwiftUI.NavigationLink a parce que je ne le fais pas comprendre pleinement ce qu'ils font encore. Si vous souhaitez nous aider, veuillez commenter/modifier.

struct NavigationLink<Destination: View, Label:View>: View {
    var destination: Destination
    var label: () -> Label

    public init(destination: Destination, @ViewBuilder label: @escaping () -> Label) {
        self.destination = destination
        self.label = label
    }

    /// If this crashes, make sure you wrapped the NavigationLink in a NavigationView
    @EnvironmentObject var nvc: UINavigationController

    var body: some View {
        Button(action: {
            let rootView = self.destination.environmentObject(self.nvc)
            let hosted = UIHostingController(rootView: rootView)
            self.nvc.pushViewController(hosted, animated: true)
        }, label: label)
    }
}

Cela résout le balayage arrière ne fonctionnant pas correctement sur SwiftUI et parce que j'utilise les noms NavigationView et NavigationLink, mon projet entier est passé immédiatement à ceux-ci.

Exemple

Dans l'exemple, je montre également la présentation modale.

struct ContentView: View {
    @State var isPresented = false

    var body: some View {
        NavigationView {
            VStack(alignment: .center, spacing: 30) {
                NavigationLink(destination: Text("Detail"), label: {
                    Text("Show detail")
                })
                Button(action: {
                    self.isPresented.toggle()
                }, label: {
                    Text("Show modal")
                })
            }
            .navigationBarTitle("SwiftUI")
        }
        .edgesIgnoringSafeArea(.top)
        .sheet(isPresented: $isPresented) {
            Modal()
        }
    }
}
struct Modal: View {
    @Environment(\.presentationMode) var presentationMode

    var body: some View {
        NavigationView {
            VStack(alignment: .center, spacing: 30) {
                NavigationLink(destination: Text("Detail"), label: {
                    Text("Show detail")
                })
                Button(action: {
                    self.presentationMode.wrappedValue.dismiss()
                }, label: {
                    Text("Dismiss modal")
                })
            }
            .navigationBarTitle("Modal")
        }
    }
}

Edit: J'ai commencé avec "Cela semble si simple que je dois oublier quelque chose" et je pense que je l'ai trouvé. Cela ne semble pas transférer EnvironmentObjects à la vue suivante. Je ne sais pas comment le NavigationLink par défaut fait cela, donc pour l'instant j'envoie manuellement des objets à la vue suivante où j'en ai besoin.

NavigationLink(destination: Text("Detail").environmentObject(objectToSendOnToTheNextView)) {
    Text("Show detail")
}

Modifier 2:

Cela expose le contrôleur de navigation à toutes les vues à l'intérieur de NavigationView en faisant @EnvironmentObject var nvc: UINavigationController. La solution consiste à faire de l'objet environnement que nous utilisons pour gérer la navigation une classe fileprivate. J'ai corrigé cela dans le Gist: https://Gist.github.com/Amzd/67bfd4b8e41ec3f179486e13e9892eeb

3
Casper Zandbergen

Vous pouvez le faire en descendant dans UIKit et en utilisant votre propre UINavigationController.

Créez d'abord un fichier SwipeNavigationController:

import UIKit
import SwiftUI

final class SwipeNavigationController: UINavigationController {

    // MARK: - Lifecycle

    override init(rootViewController: UIViewController) {
        super.init(rootViewController: rootViewController)
    }

    override init(nibName nibNameOrNil: String?, bundle nibBundleOrNil: Bundle?) {
        super.init(nibName: nibNameOrNil, bundle: nibBundleOrNil)

        delegate = self
    }

    required init?(coder aDecoder: NSCoder) {
        super.init(coder: aDecoder)

        delegate = self
    }

    override func viewDidLoad() {
        super.viewDidLoad()

        // This needs to be in here, not in init
        interactivePopGestureRecognizer?.delegate = self
    }

    deinit {
        delegate = nil
        interactivePopGestureRecognizer?.delegate = nil
    }

    // MARK: - Overrides

    override func pushViewController(_ viewController: UIViewController, animated: Bool) {
        duringPushAnimation = true

        super.pushViewController(viewController, animated: animated)
    }

    var duringPushAnimation = false

    // MARK: - Custom Functions

    func pushSwipeBackView<Content>(_ content: Content) where Content: View {
        let hostingController = SwipeBackHostingController(rootView: content)
        self.delegate = hostingController
        self.pushViewController(hostingController, animated: true)
    }

}

// MARK: - UINavigationControllerDelegate

extension SwipeNavigationController: UINavigationControllerDelegate {

    func navigationController(_ navigationController: UINavigationController, didShow viewController: UIViewController, animated: Bool) {
        guard let swipeNavigationController = navigationController as? SwipeNavigationController else { return }

        swipeNavigationController.duringPushAnimation = false
    }

}

// MARK: - UIGestureRecognizerDelegate

extension SwipeNavigationController: UIGestureRecognizerDelegate {

    func gestureRecognizerShouldBegin(_ gestureRecognizer: UIGestureRecognizer) -> Bool {
        guard gestureRecognizer == interactivePopGestureRecognizer else {
            return true // default value
        }

        // Disable pop gesture in two situations:
        // 1) when the pop animation is in progress
        // 2) when user swipes quickly a couple of times and animations don't have time to be performed
        let result = viewControllers.count > 1 && duringPushAnimation == false
        return result
    }
}

C'est le même SwipeNavigationController fourni ici , avec l'ajout de la fonction pushSwipeBackView().

Cette fonction nécessite un SwipeBackHostingController que nous définissons comme

import SwiftUI

class SwipeBackHostingController<Content: View>: UIHostingController<Content>, UINavigationControllerDelegate {
    func navigationController(_ navigationController: UINavigationController, didShow viewController: UIViewController, animated: Bool) {
        guard let swipeNavigationController = navigationController as? SwipeNavigationController else { return }
        swipeNavigationController.duringPushAnimation = false
    }

    override func viewWillDisappear(_ animated: Bool) {
        super.viewWillDisappear(animated)

        guard let swipeNavigationController = navigationController as? SwipeNavigationController else { return }
        swipeNavigationController.delegate = nil
    }
}

Nous avons ensuite configuré le SceneDelegate de l'application pour utiliser le SwipeNavigationController:

    if let windowScene = scene as? UIWindowScene {
        let window = UIWindow(windowScene: windowScene)
        let hostingController = UIHostingController(rootView: ContentView())
        window.rootViewController = SwipeNavigationController(rootViewController: hostingController)
        self.window = window
        window.makeKeyAndVisible()
    }

Enfin, utilisez-le dans votre ContentView:

struct ContentView: View {
    func navController() -> SwipeNavigationController {
        return UIApplication.shared.windows[0].rootViewController! as! SwipeNavigationController
    }

    var body: some View {
        VStack {
            Text("SwiftUI")
                .onTapGesture {
                    self.navController().pushSwipeBackView(Text("Detail"))
            }
        }.onAppear {
            self.navController().navigationBar.topItem?.title = "Swift UI"
        }.edgesIgnoringSafeArea(.top)
    }
}
1
neptune