Nick
Nick

Reputation: 309

SwiftUI passing touch events from UIViewControllerRepresentable to View behind

I'm working on a project which uses a mixture of UIKit and SwiftUI. I currently have a ZStack which I hold my SwiftUI content in, I need to display a UIViewController over top of that content. So the last item in my ZStack is a UIViewControllerRepresentable. ie:

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

My overlaid UIViewControllerRepresentative is a container for other UIViewControllers. Those child view controllers don't always need to take up the full screen, so the UIViewControllerRepresentative has a transparent background so the user can interact with the SwiftUI content behind.

The problem I'm facing is that the UIViewControllerRepresentative blocks all touch events from reaching the SwiftUI content.

I've tried overriding the UIViewControllers views hit test like so:

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

I've also event tried completely removing the touch events on the view controllers view with:

view.isUserInteractionEnabled = false

Even that doesn't work.

Any help would be really appreciated.

Upvotes: 19

Views: 9670

Answers (3)

Clément Cardonnel
Clément Cardonnel

Reputation: 5257

The solution I've found was to pass my SwiftUI view down to the overlaid UIViewControllerRepresentable.

What makes it possible is to make all entities involved generic and using @ViewBuilder in your representable.

It's possible to make this even cleaner by using a View Modifier instead of wrapping your content in the representable.

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

}

Upvotes: 2

Juan Carlos Gonzalez
Juan Carlos Gonzalez

Reputation: 69

You could use allowsHitTesting() modifier on the UIViewControllerRepresentable.

As Paul Hudson states on his website: 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)
}

Upvotes: 5

Nick
Nick

Reputation: 309

SOLUTION:

I managed to come up with a solution that works.

Instead of putting the UIViewControllerRepresentable inside the ZStack, I created a custom ViewModifier which passes the content value into the UIViewControllerRepresentable. That content is then embedded within my UIViewController by wrapping it inside of a UIHostingController and adding it as a child view controller.

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

Upvotes: 8

Related Questions