SwiftUI: How to ignore taps on background when menu is open?
It's not amazing, but you can manually track the menu's state with a @State var and set this to true in the .onTap for the Menu.
You can then apply .disabled(inMenu) to background elements as needed. But you need to ensure all exits out of the menu properly set the variable back to false. So that means a) any menu items' actions should set it back to false and b) taps outside the menu, incl. on areas that technically are "disabled" also need to switch it back to false.
There are a bunch of ways to achieve this, depending on your view hierarchy. The most aggressive approach (in terms of not missing a menu exit) might be to conditionally overlay a clear blocking view with an .onTap that sets inMenu back to false. This could however have Accessibility downsides. Optimally, of course, there would just be a way to directly bind to the menu's presentationMode or the treatment of surrounding taps could be configured on the Menu. In the meantime, the approach above has worked ok for me.
I think I have a solution, but it’s a hack… and it won’t work with the SwiftUI “App” lifecycle.
In your SceneDelegate
, instead of creating a UIWindow
use this HackedUIWindow
subclass instead:
class SceneDelegate: UIResponder, UIWindowSceneDelegate {
var window: UIWindow?
func scene(_ scene: UIScene, willConnectTo session: UISceneSession, options connectionOptions: UIScene.ConnectionOptions) {
guard let windowScene = (scene as? UIWindowScene) else { return }
let window = HackedWindow(windowScene: windowScene) // <-- here!
window.rootViewController = UIHostingController(rootView: ContentView())
self.window = window
window.makeKeyAndVisible()
}
}
class HackedUIWindow: UIWindow {
override func didAddSubview(_ subview: UIView) {
super.didAddSubview(subview)
if type(of: subview) == NSClassFromString("_UIContextMenuContainerView") {
if let rootView = self.rootViewController?.view {
rootView.isUserInteractionEnabled = false
}
}
}
override func willRemoveSubview(_ subview: UIView) {
super.willRemoveSubview(subview)
if type(of: subview) == NSClassFromString("_UIContextMenuContainerView") {
if let rootView = self.rootViewController?.view {
rootView.isUserInteractionEnabled = true
}
}
}
}
The subclass watches for subviews being added/removed, looking for one of type _UIContextMenuContainerView
that’s used by context menus. When it sees one being added, it grabs the window’s root view and disables user interaction; when the context menu is removed, it re-enables user interaction.
This has worked in my testing but YMMV. It may also be wise to obfuscate the "_UIContextMenuContainerView"
string so App Review doesn’t notice you referencing a private class.