隐藏导航栏而不会丢失SwiftUI中的向后滑动手势

时间:2020-01-26 18:06:21

标签: swift swiftui

在SwiftUI中,每当隐藏导航栏时,也会禁用向后滑动手势。

在SwiftUI中保留向后滑动手势时,有什么方法可以隐藏导航栏?我已经有一个自定义的“后退”按钮,但仍然需要该手势。

我已经看到了UIKit的一些解决方案,但是仍然不知道如何在SwiftUI中做到这一点

这是尝试代码的地方:

import SwiftUI

struct RootView: View {
    var body: some View {
        NavigationView {
            NavigationLink(destination: SecondView()) {
                Text("Go to second view")
            }
        }
    }
}

struct SecondView: View {
    var body: some View{
        Text("As you can see, swipe to go back will not work")
        .navigationBarTitle("")
        .navigationBarHidden(true)
    }
}

任何建议或解决方案都将受到赞赏

4 个答案:

答案 0 :(得分:27)

这应该通过扩展UINavigationController而起作用。

extension UINavigationController: UIGestureRecognizerDelegate {
    override open func viewDidLoad() {
        super.viewDidLoad()
        interactivePopGestureRecognizer?.delegate = self
    }

    public func gestureRecognizerShouldBegin(_ gestureRecognizer: UIGestureRecognizer) -> Bool {
        return viewControllers.count > 1
    }
}

答案 1 :(得分:3)

我环顾了有关此问题的文档和其他来源,却一无所获。基于使用UIKitUIViewControllerRepresentable的解决方案很少。我尝试结合解决方案from this question,即使在将后退按钮替换为其他视图时,也保存了后退滑动手势。代码仍然有点脏,但是我认为这是进一步发展的起点(例如,完全隐藏导航栏)。因此,ContentView的样子如下:

import SwiftUI

struct ContentView: View {

    var body: some View {

        SwipeBackNavController {

            SwipeBackNavigationLink(destination: DetailViewWithCustomBackButton()) {
                Text("Main view")
            }
            .navigationBarTitle("Standard SwiftUI nav view")


        }
        .edgesIgnoringSafeArea(.top)

    }

}

// MARK: detail view with custom back button
struct DetailViewWithCustomBackButton: View {

    @Environment(\.presentationMode) var presentationMode

    var body: some View {

        Text("detail")
            .navigationBarItems(leading: Button(action: {
                self.dismissView()
            }) {
                HStack {
                    Image(systemName: "return")
                    Text("Back")
                }
            })
        .navigationBarTitle("Detailed view")

    }

    private func dismissView() {
        presentationMode.wrappedValue.dismiss()
    }

}

这里是SwipeBackNavControllerSwipeBackNavigationLink的模仿NavigationViewNavigationLink的实现。它们只是SwipeNavigationController工作的包装器。最后一个是UINavigationController的子类,可以根据需要进行自定义:

import UIKit
import SwiftUI

struct SwipeBackNavController<Content: View>: UIViewControllerRepresentable {

    let content: Content

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

    func makeUIViewController(context: Context) -> SwipeNavigationController {
        let hostingController = UIHostingController(rootView: content)
        let swipeBackNavController = SwipeNavigationController(rootViewController: hostingController)
        return swipeBackNavController
    }

    func updateUIViewController(_ pageViewController: SwipeNavigationController, context: Context) {

    }

}

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

    var body: some View {
        Button(action: {
            guard let window = UIApplication.shared.windows.first else { return }
            guard let swipeBackNavController = window.rootViewController?.children.first as? SwipeNavigationController else { return }
            swipeBackNavController.pushSwipeBackView(DetailViewWithCustomBackButton())
        }, label: label)
    }
}

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
        setNavigationBarHidden(true, animated: false)
        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
    }
}

// MARK: Hosting controller
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
    }
}

此实现可立即保存自定义后退按钮和后退手势。我仍然不喜欢某些时刻,例如SwipeBackNavigationLink如何推动视野,因此以后我将尝试继续进行研究。

答案 2 :(得分:3)

在使用UINavigationController扩展时,您可能会遇到一个错误,该错误会在您开始滑动屏幕并将其放开而不进行导航后阻止您的导航。将.navigationViewStyle(StackNavigationViewStyle())添加到NavigationView确实可以解决此问题。

如果您需要基于设备的不同视图样式,此扩展程序将帮助您

extension View {
    public func currentDeviceNavigationViewStyle() -> AnyView {
        if UIDevice.current.userInterfaceIdiom == .pad {
            return AnyView(self.navigationViewStyle(DefaultNavigationViewStyle()))
        } else {
            return AnyView(self.navigationViewStyle(StackNavigationViewStyle()))
        }
    }
}

答案 3 :(得分:2)

这比 Nick Bellucci 的回答还要简单。 这是最简单的工作解决方案:

extension UINavigationController {
    override open func viewDidLoad() {
        super.viewDidLoad()
        interactivePopGestureRecognizer?.delegate = nil
    }
}