Question about using native iOS navigation hosting...
# compose
s
Question about using native iOS navigation hosting individual ComposeUIViewControllers as destinations (each via UIViewControllerRepresentable, no Compose-side navigation). The system back-swipe and Compose's input pipeline don't coordinate. When swiping from the leading edge, the back-pop animates and Compose simultaneously scrolls horizontally/vertically on whatever's underneath the finger. Has anyone actually shipped this in a way which feels nice and native without such issues of the input propagating to both the navigation back and the compose view itself?
Tried doing smth like this, to wrap my ViewController:
Copy code
struct ComposeHost: UIViewControllerRepresentable {
    func makeUIViewController(context: Context) -> UIViewController {
        let composeVC = SomeFileKt.ViewController()
        return SwipeBackHostingController(child: composeVC)
    }
    func updateUIViewController(_ uiViewController: UIViewController, context: Context) {}
}
...
fun ViewController(): UIViewController {
 return ComposeUIViewController { ... compose code }
}
Where the SwipeBackhostingController is an amalgamation that I got from AI to be honest which looks like this:
Copy code
import UIKit

private let edgeSwipeZoneWidth: CGFloat = 16

private final class EdgeSwipePassthroughView: UIView {
    override func hitTest(_ point: CGPoint, with event: UIEvent?) -> UIView? {
        let isRTL = effectiveUserInterfaceLayoutDirection == .rightToLeft
        let inLeadingZone = isRTL
            ? point.x > bounds.maxX - edgeSwipeZoneWidth
            : point.x < edgeSwipeZoneWidth
        if inLeadingZone {
            return nil
        }
        return super.hitTest(point, with: event)
    }
}

final class SwipeBackHostingController: UIViewController, UIGestureRecognizerDelegate {
    private let child: UIViewController
    private weak var previousDelegate: UIGestureRecognizerDelegate?

    init(child: UIViewController) {
        self.child = child
        super.init(nibName: nil, bundle: nil)
    }

    required init?(coder: NSCoder) { fatalError("init(coder:) is not used") }

    override func loadView() {
        view = EdgeSwipePassthroughView()
    }

    override func viewDidLoad() {
        super.viewDidLoad()
        addChild(child)
        child.view.translatesAutoresizingMaskIntoConstraints = false
        view.addSubview(child.view)
        NSLayoutConstraint.activate([
                                        child.view.topAnchor.constraint(equalTo: view.topAnchor),
                                        child.view.bottomAnchor.constraint(equalTo: view.bottomAnchor),
                                        child.view.leadingAnchor.constraint(equalTo: view.leadingAnchor),
                                        child.view.trailingAnchor.constraint(equalTo: view.trailingAnchor),
                                    ])
        child.didMove(toParent: self)
    }

    override func viewDidAppear(_ animated: Bool) {
        super.viewDidAppear(animated)
        guard let recognizer = navigationController?.interactivePopGestureRecognizer else { return }
        previousDelegate = recognizer.delegate
        recognizer.delegate = self
        recognizer.isEnabled = true
        recognizer.addTarget(self, action: #selector(handleInteractivePop(_:)))
    }

    override func viewWillDisappear(_ animated: Bool) {
        super.viewWillDisappear(animated)
        guard let recognizer = navigationController?.interactivePopGestureRecognizer else { return }
        if recognizer.delegate === self {
            recognizer.delegate = previousDelegate
        }
        recognizer.removeTarget(self, action: #selector(handleInteractivePop(_:)))
        // Always restore — if we leave the screen mid-gesture we mustn't leak a disabled child.
        child.view.isUserInteractionEnabled = true
    }

    func gestureRecognizerShouldBegin(_: UIGestureRecognizer) -> Bool {
        (navigationController?.viewControllers.count ?? 0) > 1
    }

    /// When the system's edge-pan fires, Compose may already be tracking the same touch
    /// (e.g. as a horizontal scroll on a list row). Toggling `isUserInteractionEnabled`
    /// makes UIKit deliver `touchesCancelled` so Compose abandons the in-flight gesture.
    /// Restored on gesture end so taps work again immediately.
    @objc private func handleInteractivePop(_ gesture: UIGestureRecognizer) {
        switch gesture.state {
        case .began:
            child.view.isUserInteractionEnabled = false
        case .ended, .cancelled, .failed:
            child.view.isUserInteractionEnabled = true
        default:
            break
        }
    }
}
But in general it is never perfect.
If I do nothing, then the back navigation gesture simply does not work
With this wrapper either: • the input is propagated to both the native navigation and the compose view at the same time • the
edgeSwipeZoneWidth
being big enough introduces a "dead zone" at the edge where neither of the two systems receive the input
Using latest compose fwiw, CMP 1.11.0-rc01 and Kotlin 2.3.20.