在SwiftUI中隐藏导航栏但不失去滑动返回手势

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

非常感谢您的建议或解决方案。

7个回答

121

只需扩展UINavigationController即可使其工作。

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

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

1
从我的经验来看,应该是可以的,@GonzoOin。 - Nick Bellucci
滑动手势有时候可以正常工作,但有时候屏幕在来回滑动时会卡住。此外,在向后滑动时我看到了白色区域。 - user832
4
谢谢。我已经找了好几个小时,你的解决方案是唯一一个使用确切滑动手势的。请问 viewControllers.count > 1 是什么意思?再次感谢。 - Merunas Grincalaitis
3
这句话的意思是,@MerunasGrincalaitis 只是在检查导航栈中是否有超过一个视图控制器。如果当前处于根视图控制器,则手势应该返回 false。请注意,翻译过程中不能改变原文的含义。 - Nick Bellucci
3
在iOS 17中存在一个问题,即如果你使用这个解决方案向后滑动到根视图,你的视图会出现故障并冻结。我不知道如何解决这个问题。 - undefined
显示剩余7条评论

27
你需要在UINavigationController上设置interactivePopGestureRecognizer。
完整的答案请参考:https://dev59.com/JFIH5IYBdhLWcg3wXc5R#60067869 我的原始答案更简短,但在执行手势时会引入一个错误。
原始答案如下:
extension UINavigationController {
    override open func viewDidLoad() {
        super.viewDidLoad()
        interactivePopGestureRecognizer?.delegate = nil
    }
}

3
如果我在根视图上向后滑动,屏幕会冻结。有人遇到同样的问题吗? - blrsk
奇怪,这个解决方案对我有效,而且我无法按照@umayanga的说明重现问题。也许是因为我的根视图上面没有任何东西,所以没有可以向后滑动的内容? - blueFox
1
@lopes710,您可以在屏幕上设置弹出手势识别器委托,并实现委托方法,告诉系统识别器是否应该起作用。 - Fab1n
@Fab1n,我可以重现根视图控制器的冻结情况 - 在你的情况下也是如此。你介意通过回答更详细地分享一下你所做的事情吗?这样可以避免引发新的错误,就像你在最后一条评论中提到的那样。 - undefined
@Scorekaj22 使用被接受的答案,因为它可以缓解冻结问题。 - undefined
显示剩余9条评论

11

在使用UINavigationController扩展时,可能会遇到一个bug,当你开始滑动屏幕并松开而没有返回导航时,导航可能被阻塞。将.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()))
        }
    }
}

不支持 iOS 16 :( - Wojciech Kulik

9

根据@Nick Bellucci的解决方案进行调整,但不适用于所有屏幕,

创建一个AppState类

class AppState {
    static let shared = AppState()

    var swipeEnabled = false
}

添加Nick的扩展(修改版)
extension UINavigationController: UIGestureRecognizerDelegate {
    override open func viewDidLoad() {
        super.viewDidLoad()
        interactivePopGestureRecognizer?.delegate = self
    }

    public func gestureRecognizerShouldBegin(_ gestureRecognizer: UIGestureRecognizer) -> Bool {
        if AppState.shared.swipeEnabled {
            return viewControllers.count > 1
        }
        return false
    }
    
}

你的观点。
struct YourSwiftUIView: View {
    var body: some View {
        VStack {
            // your code
        }
        .onAppear {
            AppState.shared.swipeEnabled = false
        }
        .onDisappear {
            AppState.shared.swipeEnabled = true
        }
    }
    
}

2
我一直在寻找这样的东西,很不错的方法。 - mehmetdelikaya

3

我查阅了有关这个问题的文档和其他来源,但并没有找到任何信息。只有一些解决方案,基于使用UIKitUIViewControllerRepresentable。我尝试结合这个问题的解决方案,并成功将返回手势与替换后退按钮的其他视图结合起来。代码还有点混乱,但我认为这是进一步发展的起点(例如完全隐藏导航栏)。下面是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是如何推送视图的,所以后面我会继续研究。


感谢您的努力!我现在会尝试这个解决方案。 - Nguyễn Khắc Hào
@NguyễnKhắcHào 很有趣。你用的Xcode版本是什么? - Hrabovskyi Oleksandr
它是11.3.1(11C504)。移除扩展名可以解决问题(但会破坏类)。 - Nguyễn Khắc Hào
1
@NguyễnKhắcHào,明天我会下载新版本并尝试重现此问题。在11.2(11B52)上它可以工作。 - Hrabovskyi Oleksandr
1
@NguyễnKhắcHào 好的,我下载了Xcode 11.3.1 (11C505)并再次运行代码 - 没有错误。我实际上是从文件中复制代码并将其粘贴到答案中,您可以尝试复制它。尝试重新启动或创建另一个项目 - 它应该可以工作。 - Hrabovskyi Oleksandr
显示剩余3条评论

2

这里有一个简单的SwiftUI解决方案。请注意,它没有针对从右到左的语言进行本地化,并且没有原生滑动手势中的平滑动画。

struct SecondView: View {
  @Environment(\.dismiss) var dismiss
  
  var body: some View{
    Text("As you can see, swipe to go back will not work")
      .navigationBarTitle("")
      .navigationBarHidden(true)
      .gesture(
        DragGesture(minimumDistance: 20, coordinateSpace: .global)
          .onChanged { value in // onChanged better than onEnded for this case
            guard value.startLocation.x < 20, // starting from left edge
                  value.translation.width > 60 else { // swiping right
            return
          }
        dismiss()
      }
    )
  }
}

2
这是一个很好的解决方案,可以在iOS 17上使用,直到找到本地手势的修复方法。 - undefined
我很高兴这个解决方案对至少一个人有用。如果你找到了同时拥有动画的方法,请告诉我。 - undefined

2
在SwiftUI中,全局隐藏导航栏的技巧,而不会丧失滑动返回手势。适用于iOS 14-17版本。
import UIKit

extension UINavigationController {
    open override func viewWillLayoutSubviews() {
        super.viewWillLayoutSubviews()
        navigationBar.isHidden = true
    }
}

很好的解决方案!给其他人一个提示:为了让这个扩展能够正常工作,你必须避免在你自定义的NavigationView视图中使用.navigationBarBackButtonHidden(true).navigationBarHidden(true) - undefined

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接