Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

iOS 18 update #5

Open
SaladDays831 opened this issue Oct 4, 2024 · 4 comments
Open

iOS 18 update #5

SaladDays831 opened this issue Oct 4, 2024 · 4 comments

Comments

@SaladDays831
Copy link

SaladDays831 commented Oct 4, 2024

The solution doesn't work on iOS 18, I'm in the process of figuring out why, but will throw this issue in just in case some good samaritan has already done it and would like to share the fix :))

Something changed in the hitTesting logic and now the SwiftUI views contained in the UIHostingController's rootView are not interactive.

Bare bones example:

class ViewController: UIViewController {
	
	private let button = UIButton()
    
    override func viewDidLoad() {
        super.viewDidLoad()
		
		view.backgroundColor = .gray
		
		view.addSubview(button)
		button.translatesAutoresizingMaskIntoConstraints = false
		button.widthAnchor.constraint(equalToConstant: 100).isActive = true
		button.heightAnchor.constraint(equalToConstant: 100).isActive = true
		button.topAnchor.constraint(equalTo: view.safeAreaLayoutGuide.topAnchor).isActive = true
		button.leadingAnchor.constraint(equalTo: view.leadingAnchor).isActive = true
		button.addTarget(self, action: #selector(tapAction), for: .touchUpInside)
		button.backgroundColor = .yellow
		
		let hostingController = ContainerViewController()
		hostingController.forwardBaseTouchesTo = view
		
		hostingController.view.frame = view.bounds
		hostingController.view.translatesAutoresizingMaskIntoConstraints = false
		hostingController.view.backgroundColor = UIColor.clear
		view.addSubview(hostingController.view)
		
		NSLayoutConstraint.activate([
			hostingController.view.leadingAnchor.constraint(equalTo: view.leadingAnchor),
			hostingController.view.trailingAnchor.constraint(equalTo: view.trailingAnchor),
			hostingController.view.bottomAnchor.constraint(equalTo: view.bottomAnchor),
			hostingController.view.topAnchor.constraint(equalTo: view.topAnchor),
		])
		
		addChild(hostingController)
		hostingController.didMove(toParent: self)
    }
	
	@objc private func tapAction() {
		print("Tap in UIKit")
	}
    
}

struct DummyView: View {
	var body: some View {
		Button("Test") {
			print("SwiftUI button tap")
		}
		.frame(width: 100, height: 150)
		.foregroundStyle(Color.red)
		.background(Color.green)
	}
}

class ContainerViewController: HostingParentController {
	override func viewDidLoad() {
		super.viewDidLoad()
		
		let hostingViewController = UIHostingController(rootView: DummyView())
		
		hostingViewController.view.frame = view.bounds
		hostingViewController.view.autoresizingMask = [.flexibleWidth, .flexibleHeight]
		view.addSubview(hostingViewController.view)
		hostingViewController.view.backgroundColor = .clear
		
		addChild(hostingViewController)
		hostingViewController.didMove(toParent: self)
	}
}


Tapping the button in the DummyView propagates the touch down into UIKit, not triggering the SwiftUI button action

@Cedric-bemobile
Copy link

I am having the exact same issue, where the touches from the SwiftUI view are indeed ignored. Will this be fixed soon? It does show me how brittle the communication between SwiftUI and UIKit can become, and I am glad the code is currently not in production for us, else those views would have stopped working in iOS 18.

Maybe mixing both of them inside the same view is not the best idea, this might happen in the future once more.

@aehlke
Copy link

aehlke commented Oct 11, 2024

Find a solution?

Here's a related thread https://forums.developer.apple.com/forums/thread/762292 with a potential solution at the end

@SaladDays831
Copy link
Author

Find a solution?

Here's a related thread https://forums.developer.apple.com/forums/thread/762292 with a potential solution at the end

Yeah I ended up using a separate UIWindow like the one in the Apple dev thread

@Cedric-bemobile
Copy link

Cedric-bemobile commented Oct 14, 2024

Hi guys, I found a solution that worked for me, and might possibly work for you as well.
I changed the hitTest code to the following:

override public func hitTest(_ point: CGPoint, with event: UIEvent?) -> UIView? {
        guard let result = super.hitTest(point, with: event) else { return nil }

        if #available(iOS 18, *), String(describing: type(of: result)).contains("_UIHostingView") {
            // Check behind _UIHostingView and return result if nothing else is found
            return checkBehind(view: result, point: point, event: event) ?? result
        }

        // For earlier iOS versions, or if the result is not _UIHostingView
        return checkBehind(view: result, point: point, event: event)
    }

on iOS 18 the resulting hittest should contain a UIHostingView, if it does we do the checkBehind logic. It it returns nil, you return the result since you are touching inside the SwiftUI view. If it returns a value, you know you are touching a view behind the UIHostingView.

Tested it inside our project, and it now does what it is supposed to do. Hope this helps someone

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

No branches or pull requests

3 participants