Come restituire il gesto di scorrimento in SwiftUI nello stesso comportamento di UIKit (interactivePopGestureRecognizer)


9

Il riconoscitore di gesti pop interattivi dovrebbe consentire all'utente di tornare alla vista precedente nello stack di navigazione quando scorre oltre la metà dello schermo (o qualcosa attorno a quelle linee). In SwiftUI il gesto non viene annullato quando il colpo non è abbastanza lontano.

SwiftUI: https://imgur.com/xxVnhY7

UIKit: https://imgur.com/f6WBUne


Domanda:

È possibile ottenere il comportamento di UIKit durante l'utilizzo delle viste SwiftUI?


tentativi

Ho provato a incorporare un UIHostingController all'interno di un UINavigationController ma che offre esattamente lo stesso comportamento di 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) {}
}

Risposte:


4

Ho finito per ignorare l'impostazione predefinita NavigationViewe NavigationLinkper ottenere il comportamento desiderato. Sembra così semplice che devo trascurare qualcosa che fanno le viste predefinite di SwiftUI?

NavigationView

Lo avvolgo UINavigationControllerin un super semplice UIViewControllerRepresentableche dà UINavigationControlleralla vista del contenuto SwiftUI come ambienteObject. Ciò significa che la NavigationLinklattina può afferrarla successivamente fintanto che si trova nello stesso controller di navigazione (i controller di visualizzazione presentati non ricevono l'ambienteObjects) che è esattamente ciò che vogliamo.

Nota: NavigationView ha bisogno .edgesIgnoringSafeArea(.top)e non so ancora come impostarlo nella struttura stessa. Vedi esempio se il tuo nvc viene tagliato in alto.

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

Creo un NavigationLink personalizzato che accede agli ambienti UINavigationController per inviare un UIHostingController che ospita la vista successiva.

Nota: Non ho implementato il selectione isActivequello che SwiftUI.NavigationLink ha perché non capisco ancora esattamente cosa fanno. Se vuoi aiutarti, commenta / modifica.

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

Questo risolve il back swipe che non funziona correttamente su SwiftUI e poiché utilizzo i nomi NavigationView e NavigationLink, il mio intero progetto è passato immediatamente a questi.

Esempio

Nell'esempio mostro anche una presentazione 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")
        }
    }
}

Modifica: ho iniziato con "Sembra così semplice che devo trascurare qualcosa" e penso di averlo trovato. Questo non sembra trasferire EnvironmentObjects alla vista successiva. Non so come lo fa il NavigationLink predefinito, quindi per ora invio manualmente gli oggetti alla vista successiva dove ne ho bisogno.

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

Modifica 2:

Questo espone il controller di navigazione a tutte le viste all'interno NavigationViewfacendo @EnvironmentObject var nvc: UINavigationController. Il modo per risolvere questo problema è rendere environmentObject che utilizziamo per gestire la navigazione in una classe privata di file. Ho risolto questo in sintesi: https://gist.github.com/Amzd/67bfd4b8e41ec3f179486e13e9892eeb


Il tipo di argomento 'UINavigationController' non è conforme al tipo previsto 'ObservableObject'
stardust4891

@kejodion Ho dimenticato di aggiungerlo al post di StackOverflow ma era in sostanza:extension UINavigationController: ObservableObject {}
Casper Zandbergen,

È stato corretto un bug di back swipe che stavo riscontrando, ma sfortunatamente non sembra riconoscere le modifiche per recuperare le richieste e comunque il modo predefinito di NavigationView.
stardust4891,

@kejodion Ah, è un peccato, so che questa soluzione ha problemi con environmentObjects. Non sono sicuro di quali richieste di recupero intendi. Forse apri una nuova domanda.
Casper Zandbergen,

Bene, ho diverse richieste di recupero che vengono automaticamente aggiornate nell'interfaccia utente quando si salva il contesto dell'oggetto gestito. Per qualche motivo non funzionano quando implemento il tuo codice. Vorrei davvero averlo fatto, perché questo ha risolto un problema con il back swipe che ho cercato di risolvere per giorni.
stardust4891

1

Puoi farlo scendendo in UIKit e usando il tuo UINavigationController.

Innanzitutto crea un SwipeNavigationControllerfile:

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

Questo è lo stesso SwipeNavigationControllerfornito qui , con l'aggiunta della pushSwipeBackView()funzione.

Questa funzione richiede una SwipeBackHostingControllerche definiamo come

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

Quindi configuriamo l'app SceneDelegateper utilizzare 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()
    }

Finalmente usalo nel tuo 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
SwipeNavigationController personalizzato non modifica nulla rispetto al comportamento predefinito di UINavigationController. Il func navController()per afferrare la vc e poi spingere la vc se stessi è in realtà una grande idea e mi ha aiutato a capire questo problema fuori! Risponderò a una risposta più amichevole di SwiftUI, ma grazie per il vostro aiuto!
Casper Zandbergen,
Utilizzando il nostro sito, riconosci di aver letto e compreso le nostre Informativa sui cookie e Informativa sulla privacy.
Licensed under cc by-sa 3.0 with attribution required.