简体   繁体   English

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

[英]How to give back swipe gesture in SwiftUI the same behaviour as in UIKit (interactivePopGestureRecognizer)

The interactive pop gesture recognizer should allow the user to go back the the previous view in navigation stack when they swipe further than half the screen (or something around those lines).交互式弹出手势识别器应该允许用户在滑动超过一半屏幕(或这些行周围的东西)时返回导航堆栈中的前一个视图。 In SwiftUI the gesture doesn't get canceled when the swipe wasn't far enough.在 SwiftUI 中,当滑动不够远时,手势不会被取消。

SwiftUI: https://imgur.com/xxVnhY7 SwiftUI: https://imgur.com/xxVnhY7

UIKit: https://imgur.com/f6WBUne UIKit: https://imgur.com/f6WBUne


Question:问题:

Is it possible to get the UIKit behaviour while using SwiftUI views?使用 SwiftUI 视图时是否可以获得 UIKit 行为?


Attempts尝试

I tried to embed a UIHostingController inside a UINavigationController but that gives the exact same behaviour as NavigationView.我尝试在 UINavigationController 中嵌入 UIHostingController,但这给出了与 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) {}
}

I ended up overriding the default NavigationView and NavigationLink to get the desired behaviour.我最终覆盖了默认的NavigationViewNavigationLink以获得所需的行为。 This seems so simple that I must be overlooking something that the default SwiftUI views do?这似乎很简单,以至于我必须忽略默认 SwiftUI 视图所做的事情?

NavigationView导航视图

I wrap a UINavigationController in a super simple UIViewControllerRepresentable that gives the UINavigationController to the SwiftUI content view as an environmentObject.我将UINavigationController包装在一个超级简单的UIViewControllerRepresentable中,它将 UINavigationController 作为环境对象提供给UINavigationController内容视图。 This means the NavigationLink can later grab that as long as it's in the same navigation controller (presented view controllers don't receive the environmentObjects) which is exactly what we want.这意味着NavigationLink以后可以抓住它,只要它在同一个导航 controller (呈现的视图控制器不接收 environmentObjects),这正是我们想要的。

Note: The NavigationView needs .edgesIgnoringSafeArea(.top) and I don't know how to set that in the struct itself yet.注意: NavigationView 需要.edgesIgnoringSafeArea(.top)而我还不知道如何在结构本身中设置它。 See example if your nvc cuts off at the 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导航链接

I create a custom NavigationLink that accesses the environments UINavigationController to push a UIHostingController hosting the next view.我创建了一个访问环境 UINavigationController 的自定义 NavigationLink 以推送一个 UIHostingController 托管下一个视图。

Note: I didn't implement the selection and isActive that the SwiftUI.NavigationLink has because I don't fully understand what they do yet.注意:我没有实现 SwiftUI.NavigationLink 具有的selectionisActive ,因为我还不完全了解它们的作用。 If you want to help with that please comment/edit.如果您想对此提供帮助,请发表评论/编辑。

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

This solves the back swipe not working correctly on SwiftUI and because I use the names NavigationView and NavigationLink my entire project switched to these immediately.这解决了在 SwiftUI 上无法正常工作的后滑动问题,并且因为我使用 NavigationView 和 NavigationLink 这两个名称,我的整个项目立即切换到这些名称。

Example例子

In the example I show modal presentation too.在示例中,我也展示了模态演示。

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: I started off with "This seems so simple that I must be overlooking something" and I think I found it.编辑:我从“这看起来很简单,我必须忽略某些东西”开始,我想我找到了。 This doesn't seem to transfer EnvironmentObjects to the next view.这似乎不会将 EnvironmentObjects 转移到下一个视图。 I don't know how the default NavigationLink does that so for now I manually send objects on to the next view where I need them.我不知道默认 NavigationLink 是如何做到的,所以现在我手动将对象发送到我需要它们的下一个视图。

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

Edit 2:编辑2:

This exposes the navigation controller to all views inside NavigationView by doing @EnvironmentObject var nvc: UINavigationController .这通过执行@EnvironmentObject var nvc: UINavigationController将导航 controller 暴露给NavigationView内的所有视图。 The way to fix this is making the environmentObject we use to manage navigation a fileprivate class.解决此问题的方法是将我们用来管理导航的 environmentObject 设为文件私有 class。 I fixed this in the gist: https://gist.github.com/Amzd/67bfd4b8e41ec3f179486e13e9892eeb我在要点中解决了这个问题: https://gist.github.com/Amzd/67bfd4b8e41ec3f179486e13e9892eeb

You can do this by descending into UIKit and using your own UINavigationController.您可以通过下降到 UIKit 并使用您自己的 UINavigationController 来做到这一点。

First create a SwipeNavigationController file:首先创建一个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
    }
}

This is the same SwipeNavigationController provided here , with the addition of the pushSwipeBackView() function.这与此处提供的SwipeNavigationController相同,但添加了pushSwipeBackView() function。

This function requires a SwipeBackHostingController which we define as这个 function 需要一个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
    }
}

We then set up the app's SceneDelegate to use the SwipeNavigationController :然后我们设置应用程序的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()
    }

Finally use it in your ContentView :最后在您的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)
    }
}

声明:本站的技术帖子网页,遵循CC BY-SA 4.0协议,如果您需要转载,请注明本站网址或者原文地址。任何问题请咨询:yoyou2525@163.com.

 
粤ICP备18138465号  © 2020-2024 STACKOOM.COM