如何在SwiftUI中返回与UIKit中相同的行为(interactivePopGestureRecognizer)


9

交互式弹出手势识别器应允许用户在滑动超过屏幕的一半(或这些行周围的内容)时返回导航堆栈中的上一个视图。在SwiftUI中,如果滑动距离不够远,手势不会被取消。

SwiftUI: https ://imgur.com/xxVnhY7

UIKit: https //imgur.com/f6WBUne


题:

使用SwiftUI视图时能否获得UIKit行为?


尝试次数

我试图将UIHostingController嵌入UINavigationController内,但其行为与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) {}
}

Answers:


4

我最终覆盖了默认设置,NavigationViewNavigationLink获得了期望的行为。这看起来是如此简单,以至于我必须忽略默认SwiftUI视图所做的事情?

导航视图

我用一个UINavigationController超级简单的方法包装了一个UIViewControllerRepresentable,它UINavigationController作为环境对象提供给SwiftUI内容视图。这意味着NavigationLink以后可以抢到它,只要它在同一个导航控制器中(呈现的视图控制器不接收environmentObjects),这正是我们想要的。

注意: NavigationView需要.edgesIgnoringSafeArea(.top),但我还不知道如何在结构本身中进行设置。如果您的nvc在顶部中断,请参见示例。

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,用于访问环境UINavigationController来推送托管下一个视图的UIHostingController。

注意:我没有实现SwiftUI.NavigationLink selectionisActive,因为我还不完全了解它们的作用。如果您想提供帮助,请评论/编辑。

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

这解决了向后滑动在SwiftUI上无法正常工作的问题,并且由于我使用了NavigationView和NavigationLink这两个名称,因此我的整个项目都立即切换到了这些位置。

在示例中,我也显示了模态表示。

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

编辑:我从“这看起来很简单,我必须忽略某些东西”开始,我想我找到了它。这似乎没有将EnvironmentObjects转移到下一个视图。我不知道默认的NavigationLink是如何做到的,所以现在我手动将对象发送到需要它们的下一个视图。

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

编辑2:

这样做会将导航控制器暴露给内部NavigationView的所有视图@EnvironmentObject var nvc: UINavigationController。解决此问题的方法是使用于管理导航的environmentObject成为fileprivate类。我在要点中解决了这个问题:https : //gist.github.com/Amzd/67bfd4b8e41ec3f179486e13e9892eeb


参数类型'UINavigationController'不符合预期的类型'ObservableObject'–
stardust4891

@kejodion我忘记将其添加到stackoverflow帖子中,但要点在于:extension UINavigationController: ObservableObject {}
Casper Zandbergen

它修复了我遇到的向后滑动错误,但不幸的是,它似乎并未确认获取请求的更改,以及默认NavigationView所做的更改。
stardust4891

@kejodion啊,这太糟糕了,我知道此解决方案与environmentObjects有关。不确定获取请求是什么意思。也许打开一个新问题。
卡斯珀·赞伯根

好吧,我有几个获取请求,这些请求在保存托管对象上下文时会在UI中自动更新。由于某些原因,当我实现您的代码时它们不起作用。我真的很希望他们这样做,因为这解决了我几天来一直试图解决的向后滑动问题。
stardust4891

1

您可以通过进入UIKit并使用自己的UINavigationController来实现。

首先创建一个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
    }
}

此功能与此处SwipeNavigationController提供的功能相同。pushSwipeBackView()

此功能要求SwipeBackHostingController我们定义为

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

然后,我们将应用设置SceneDelegate为使用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()
    }

最后在您的中使用它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实际上不会更改默认UINavigationController行为中的任何内容。该func navController()抢VC,然后按下VC自己实际上是一个伟大的想法,帮助我弄清楚这个问题了!我将回答一个更加SwiftUI友好的答案,但是谢谢您的帮助!
卡斯珀·赞伯根
By using our site, you acknowledge that you have read and understand our Cookie Policy and Privacy Policy.
Licensed under cc by-sa 3.0 with attribution required.