Hide navigation bar without losing swipe back gesture in SwiftUI

前端 未结 3 1676
野的像风
野的像风 2020-12-09 00:21

In SwiftUI, whenever the navigation bar is hidden, the swipe to go back gesture is disabled as well.

Is there any way to hide the navigation bar while preserving t

相关标签:
3条回答
  • 2020-12-09 00:27

    When using the UINavigationController extension you might encounter a bug that blocks your navigation after you start swiping the screen and let it go without navigating back. Adding .navigationViewStyle(StackNavigationViewStyle()) to NavigationView does fix this issue.

    If you need different view styles based on device, this extension helps:

    extension View {
        public func currentDeviceNavigationViewStyle() -> AnyView {
            if UIDevice.current.userInterfaceIdiom == .pad {
                return AnyView(self.navigationViewStyle(DefaultNavigationViewStyle()))
            } else {
                return AnyView(self.navigationViewStyle(StackNavigationViewStyle()))
            }
        }
    }
    
    0 讨论(0)
  • 2020-12-09 00:37

    I looked around documentation and other sources about this issue and found nothing. There are only a few solutions, based on using UIKit and UIViewControllerRepresentable. I tried to combine solutions from this question and I saved swipe back gesture even while replacing back button with other view. The code is still dirty a little, but I think that is the start point to go further (totally hide navigation bar, for example). So, here is how ContentView looks like:

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

    Here is realization of SwipeBackNavController and SwipeBackNavigationLink which mimic NavigationView and NavigationLink. They are just wrappers for SwipeNavigationController's work. The last one is a subclass of UINavigationController, which can be customized for your needs:

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

    This realization provides to save custom back button and swipe back gesture for now. I still don't like some moments, like how SwipeBackNavigationLink pushes view, so later I'll try to continue research.

    0 讨论(0)
  • 2020-12-09 00:43

    This should work by just extending UINavigationController.

    extension UINavigationController: UIGestureRecognizerDelegate {
        override open func viewDidLoad() {
            super.viewDidLoad()
            interactivePopGestureRecognizer?.delegate = self
        }
    
        public func gestureRecognizerShouldBegin(_ gestureRecognizer: UIGestureRecognizer) -> Bool {
            return viewControllers.count > 1
        }
    }
    
    0 讨论(0)
提交回复
热议问题