SwiftUI 将触摸事件从 UIViewControllerRepresentable 传递到 View behind

SwiftUI passing touch events from UIViewControllerRepresentable to View behind

我正在开发一个混合使用 UIKit 和 SwiftUI 的项目。我目前有一个 ZStack,其中包含我的 SwiftUI 内容,我需要在该内容之上显示一个 UIViewController。所以我的 ZStack 中的最后一项是 UIViewControllerRepresentable。即:

ZStack {
   ...
   SwiftUIContent
   ...
   MyUIViewControllerRepresentative() 
}

我覆盖的 UIViewControllerRepresentative 是其他 UIViewController 的容器。这些子视图控制器并不总是需要占据整个屏幕,因此 UIViewControllerRepresentative 具有透明背景,因此用户可以与后面的 SwiftUI 内容进行交互。

我面临的问题是 UIViewControllerRepresentative 阻止所有触摸事件到达 SwiftUI 内容。

我试过像这样覆盖 UIViewControllers 视图命中测试:

override func hitTest(_ point: CGPoint, with event: UIEvent?) -> UIView? {
    // return nil if the touch event should propagate to the SwiftUI content
}

我也试过完全删除视图控制器视图上的触摸事件:

view.isUserInteractionEnabled = false

即使这样也行不通。

如有任何帮助,我们将不胜感激。

解决方案:

我想出了一个可行的解决方案。

我没有将 UIViewControllerRepresentable 放入 ZStack,而是创建了一个自定义 ViewModifier,它将内容值传递给 UIViewControllerRepresentable。然后将该内容嵌入到我的 UIViewController 中,方法是将其包装在 UIHostingController 中并将其添加为子视图控制器。

ZStack {
    ... SwiftUI content ...
}.modifier(MyUIViewControllerRepresentative)

您可以在 UIViewControllerRepresentable 上使用 allowsHitTesting() 修饰符。

正如 Paul Hudson 在其网站上所说:https://www.hackingwithswift.com/quick-start/swiftui/how-to-disable-taps-for-a-view-using-allowshittesting

"If hit testing is disallowed for a view, any taps automatically continue through the view on to whatever is behind."

ZStack {
   ...
   SwiftUIContent
   ...
   MyUIViewControllerRepresentative()
       .allowsHitTesting(false)
}

我找到的解决方案是将我的 SwiftUI 视图向下传递到覆盖 UIViewControllerRepresentable

使所有涉及的实体成为通用实体并在您的可表示中使用 @ViewBuilder 成为可能。

可以通过使用视图修改器而不是将您的内容包装在可表示的对象中来使它更加清晰。

内容视图
struct ContentView: View {
    var body: some View {
        ViewControllerRepresentable {
            Text("SwiftUI Content")
        }
    }
}
UIViewControllerRepresentable
struct ViewControllerRepresentable<Content: View>: UIViewControllerRepresentable {

    @ViewBuilder let content: Content

    typealias UIViewControllerType = ViewController<Content>


    func makeUIViewController(context: Context) -> BottomSheetViewController<Content> {
        ViewController<Content>(content: UIHostingController(rootView: content))
    }

    func updateUIViewController(_ uiViewController: ViewController<Content>, context: Context) {
        
    }
    
}
UIViewController
import SwiftUI
import UIKit

final class ViewController<Content: View>: UIViewController {

    private let content: UIHostingController<Content>

    init(content: UIHostingController<Content>) {
        self.content = content
        super.init(nibName: nil, bundle: nil)
    }

    @available(*, unavailable)
    required init?(coder: NSCoder) {
        return nil
    }

    override func viewDidLoad() {
        super.viewDidLoad()
        view.backgroundColor = .clear
        setupLayout()
    }

    private func setupLayout() {
        addChild(content)
        view.addSubview(content.view)
        content.didMove(toParent: self)

        // Here you can add UIKit views above your SwiftUI content while keeping it interactive

        NSLayoutConstraint.activate([
            content.view.leadingAnchor.constraint(equalTo: view.leadingAnchor),
            content.view.trailingAnchor.constraint(equalTo: view.trailingAnchor),
            content.view.topAnchor.constraint(equalTo: view.topAnchor),
            content.view.bottomAnchor.constraint(equalTo: view.bottomAnchor)
        ])
    }

}