SwiftUI:菜单打开时如何忽略背景上的点击?
Posted
技术标签:
【中文标题】SwiftUI:菜单打开时如何忽略背景上的点击?【英文标题】:SwiftUI: How to ignore taps on background when menu is open? 【发布时间】:2021-02-01 13:33:38 【问题描述】:我目前正在努力解决 SwiftUI 问题:
以非常抽象的方式,这就是我的应用程序代码的样子(不是实际代码,只是为了讨论这里的事情):
struct SwiftUIView: View
@State private var toggle: Bool = true
var body: some View
VStack
Spacer()
if toggle
Text("on")
else
Text("off")
Spacer()
Rectangle()
.frame(height: 200)
.onTapGesture toggle.toggle()
Spacer()
Menu("Actions")
Button("Duplicate", action: toggle.toggle() )
Button("Rename", action: toggle.toggle() )
Button("Delete", action: toggle.toggle() )
Spacer()
那么这里的本质是什么?
背景中有一个元素(矩形)会对用户的点击输入做出反应 有一个菜单,其中包含在点击时也会执行某些操作的项目现在,我面临以下问题:
点击“操作”打开菜单时,菜单会打开 - 到目前为止一切顺利。但是,当我现在决定不想触发菜单中包含的任何操作并点击背景上的某处以将其关闭时,我可能会点击背景中的矩形。如果我这样做,矩形上的点击会直接触发 onTapGesture 中定义的动作。
但是,理想的行为是,当菜单打开时,我可以点击菜单外的任意位置将其关闭而不触发任何其他元素。
知道如何实现这一目标吗?谢谢!
(如果需要进一步说明,请在 cmets 中告诉我。)
【问题讨论】:
在重新测试我给你的代码后你是对的,它不起作用。我看起来到处都是,似乎没有办法做到这一点,因为当菜单出现时我找不到执行代码的方法。我假设苹果希望菜单用户意识到菜单不像警报那样起作用,并且在显示它时,它后面的元素没有被禁用。从用户的角度来看,我不认为你的问题真的会是一个问题,因为他们已经习惯了。 感谢您重新测试。可能,你是对的。但是,我不完全同意您的假设(或者 Apple 可能在这里选择的方法。因为他们自己实现的方式不同。例如,当您打开文件应用程序并导航到“浏览”选项卡时,您可以打开一个菜单通过点击右上角的三个点。然后,当点击屏幕上的任何其他位置时,它不会触发背景中的任何按钮/菜单项,而只会在用户执行任何其他操作之前关闭菜单。这就是我作为用户所期望的行为。 确实如此。老实说,我不知道他们是怎么做到的!我知道无法以编程方式显示菜单,因此在显示菜单时不能更改任何变量。菜单的元素在背景中呈现,因此出现事件时没有正确的。我还查看了实际上对您有用的上下文菜单。缺点是它们需要长按并模糊背后的背景。所以他们不是这样做的,因为它显示在一个简单的点击上,并且在文件应用程序中没有模糊。并且无法在单击时显示上下文菜单... 我只是想补充一点,我在ToolBarItem
的.navigationBarTrailling
位置有一个菜单,在.principal
位置还有一个选择器。在选择器上点击菜单外部不会触发选择器。但是导航栏外的任何点击都会触发。似乎是对屏幕的疏忽,因为苹果确实认为阻止点击导航栏是合适的。
【参考方案1】:
这并不令人惊奇,但是您可以使用 @State 变量手动跟踪菜单的状态,并在菜单的 .onTap 中将其设置为 true。
然后,您可以根据需要将 .disabled(inMenu) 应用于背景元素。但是您需要确保菜单中的所有退出都正确地将变量设置回 false。这意味着 a) 任何菜单项的操作都应将其设置回 false 和 b) 在菜单外点击,包括。在技术上“禁用”的区域也需要将其切换回 false。
有很多方法可以实现这一点,具体取决于您的视图层次结构。最激进的方法(就不会错过菜单退出而言)可能是有条件地使用 .onTap 覆盖一个清晰的阻塞视图,将 inMenu 设置回 false。然而,这可能具有可访问性的缺点。当然,最理想的情况是,只有一种方法可以直接绑定到菜单的presentationMode,或者可以在菜单上配置对周围点击的处理。与此同时,上面的方法对我来说效果很好。
【讨论】:
叹息我必须使用相同的技巧。向 Apple 提交了错误报告,希望他们在 ios 16 或 17 中修复此问题。 这是一个真正的问题。您的解决方案假设您可以检查菜单是打开还是关闭,但这是真正的解决方案!如何检查菜单是打开还是关闭? 我不确定我是否理解您所说的“您的解决方案假定您可以检查菜单是打开还是关闭”的意思。我的,公认的 hacky,解决方案只是指出,通过菜单上的 .onTap,您确实有机会至少知道用户何时进入菜单。然后,我列出了用户退出菜单时手动跟踪的潜在路线。【参考方案2】:我想我有一个解决方案,但这是一个 hack……并且它不适用于 SwiftUI“应用程序”生命周期。
在您的SceneDelegate
中,不要创建UIWindow
,而是使用这个HackedUIWindow
子类:
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
子类监视正在添加/删除的子视图,寻找上下文菜单使用的_UIContextMenuContainerView
类型之一。当它看到一个被添加时,它会抓取窗口的根视图并禁用用户交互;删除上下文菜单后,它会重新启用用户交互。
这在我的测试中有效,但 YMMV。混淆 "_UIContextMenuContainerView"
字符串也是明智之举,这样 App Review 就不会注意到您引用了私有类。
【讨论】:
以上是关于SwiftUI:菜单打开时如何忽略背景上的点击?的主要内容,如果未能解决你的问题,请参考以下文章
检测 GeometryReader SwiftUI 背景上的点击手势