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) {}
}
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?
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 {}
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.
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
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)
}
}