Has someone tried to host a Compose view inside a ...
# compose
s
Has someone tried to host a Compose view inside a predominantly SwiftUI app which handles navigation on the SwiftUI side? Testing on iOS 26, I am getting a conflict with the built-in gesture of clicking the screen and dragging it towards the right to navigate back. My content also has horizontally scrollable containers which do not have a way to let SwiftUI know that it should not be taking over dragging back I suppose. Sounds like a tricky thing to make work both ways but I do not want to do
.navigationBarHidden(true)
because that also will remove the built-in "back" arrow and the expected behavior of going back with a drag too
This is a screen recording of what I describe. With the back gesture taking over scrolling my horizontal container
m
I didn't run into this with Compose, but I did run into it with SwiftUI. The solution I came up with was to do the
navigationBarBackButtonHidden
and then manually add a back button to the toolbar for that screen.
s
Yeah but that means that you do not get the native-looking back button which would match the rest of the app and it would make specifically only the compose screens look like the odd ones out
m
The button was done with native SwiftUI. I did see that mine looked a little off (I think I didn't pick the correct material for it), but I didn't put anytime in to trying to make it perfect since for me it was not a real app. But I don't see a reason it cannot be fixed.
s
It also looks differently on iOS 26 compared to older ones, has the liquid glass effect etc. So I think I need to leave that part to native, else it's gonna be an uphill battle
Claude managed to help me build a solution on top of this in the end, which so far looks to work but I haven't verified on all OS versions. I added a UIViewController which inside has the code to disable any UIPanGestureRecognizers in the hierarchy
Copy code
import SwiftUI
import UIKit

final class SwipeBackToggleHostingController: UIViewController {
    private let child: UIViewController

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

    required init?(coder: NSCoder) { fatalError("init(coder:) has not been implemented") }

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

    // NavigationStack on iOS 18+/26 uses a private pan recognizer for swipe-back
    // in addition to interactivePopGestureRecognizer, so toggle every
    // UIPanGestureRecognizer up the nav controller's view chain.
    func setSwipeBackEnabled(_ enabled: Bool) {
        navigationController?.interactivePopGestureRecognizer?.isEnabled = enabled
        var view: UIView? = navigationController?.view
        while let current = view {
            for gr in current.gestureRecognizers ?? [] where gr is UIPanGestureRecognizer {
                gr.isEnabled = enabled
            }
            view = current.superview
        }
    }
}
Then on my horizontally scrollable items I applied a modifier
Copy code
// Implemented by Swift. While a pointer is down on a marked region we
// disable the system swipe-back so Compose's horizontal scroll can win;
// on touch end we re-enable it.
interface IosSwipeBackController {
  fun setSwipeBackEnabled(isEnabled: Boolean)
}

internal val LocalIosSwipeBackController = staticCompositionLocalOf<IosSwipeBackController?> { null }

@Composable
internal actual fun Modifier.blockSwipeBackOnIos(): Modifier {
  val controller = LocalIosSwipeBackController.current ?: return this
  return this.pointerInput(controller) {
    awaitPointerEventScope {
      while (true) {
        awaitFirstDown(requireUnconsumed = false, pass = PointerEventPass.Initial)
        controller.setSwipeBackEnabled(false)
        try {
          do {
            val event = awaitPointerEvent(PointerEventPass.Initial)
            if (event.changes.none { it.pressed }) break
          } while (true)
        } finally {
          controller.setSwipeBackEnabled(true)
        }
      }
    }
  }
}
Which reports to that class when a gesture starts and temporarily disables this handling.
This works, but I definitely couldn't have thought of doing this myself xD
a
Could you please file an issue in our YouTrack? We'll take a look how to improve the situation. As a workaround - you can find the corresponding gesture recognizer and make it recognize only edge swipes, but not one that start from content center. Unlike UIKit, it might be tricky with SwiftUI, but it definitely possible.
thank you color 1
make it recognize only edge swipes
I specifically want to avoid doing that, since this is a pilot project to test embedding some compose into our iOS project. If by doing so it means that suddenly this screen is the "odd one out" with missing behavior etc then it's a bust. Might as well just do the UI in SwiftUI and consider sharing the rest of the code instead.
a
Thank you - yes, it seems like we have similar issue already reported. We'll see what we can improve there.