SwiftUI:如何在菜单打开时忽略背景上的点击?

hec6srdp  于 2023-10-15  发布在  Swift
关注(0)|答案(6)|浏览(158)

我正在努力解决一个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()
        }
    }
}

所以这里的精髓是什么?

  • 背景中有一个元素(矩形),它对用户的点击输入做出React
  • 有一个菜单,其中包含的项目,也进行一些行动时,点击

现在,我面临着以下问题:
当点击“操作”打开菜单时,菜单会打开-到目前为止一切顺利。然而,当我现在决定不想触发菜单中包含的任何操作,并点击背景上的某个地方来关闭它时,可能会发生我点击背景中的矩形。如果我这样做,在矩形上的点击直接触发onTapGesture中定义的操作。
然而,理想的行为是,当菜单打开时,我可以点击菜单外的任何位置来关闭它,而不会**触发任何其他元素。
你知道我该怎么做吗?谢谢你,谢谢
(Let我在评论中知道是否需要进一步澄清。

eqfvzcg8

eqfvzcg81#

您可以实现一个.overlay,它是可扩展的,当您点击菜单时会出现。让它覆盖整个屏幕,它会被菜单忽略。当点击菜单图标时,您可以将属性设置为true。当点击覆盖或菜单项时,将其设置回false。
你可以把它放在你的根视图中,并使用带有@Environment的视图模型来从任何地方访问它。
唯一的缺点是,你需要在每个菜单按钮中放置isMenuOpen = false
苹果正在使用意想不到的行为本身,一个.ex在天气应用程序。但是,我仍然认为这是一个bug,并提交了一份报告。(FB10033181)

@State var isMenuOpen: Bool = false

var body: some View {
    NavigationView{
        NavigationLink{
            ChildView()
        } label: {
            Text("Some NavigationLink")
                .padding()
        }
        .toolbar{
            ToolbarItem(placement: .navigationBarTrailing){
                Menu{
                    Button{
                        isMenuOpen = false
                    } label: {
                        Text("Some Action")
                    }
                } label: {
                    Image(systemName: "ellipsis.circle")
                }
                .onTapGesture {
                    isMenuOpen = true
                }
            }
        }
    }
    .overlay{
        if isMenuOpen {
            Color.white.opacity(0.001)
            .ignoresSafeArea()
            .frame(maxWidth: .infinity, maxHeight: .infinity)
            .onTapGesture {
                isMenuOpen = false
            }
        }
    }
}
sc4hvdpw

sc4hvdpw2#

这并不令人惊讶,但您可以使用@State var手动跟踪菜单的状态,并在菜单的.onTap中将其设置为true。
然后,您可以根据需要将.disabled(inMenu)应用于背景元素。但是你需要确保菜单的所有退出都正确地将变量设置回false。所以这意味着a)任何菜单项的操作都应该将其设置为false,b)在菜单外点击,包括。在技术上被“禁用”的区域上,也需要将其切换回false。
根据视图层次结构,有很多方法可以实现这一点。最激进的方法(就不错过菜单退出而言)可能是有条件地覆盖一个清晰的阻塞视图,并使用.onTap将inMenu设置为false。然而,这可能具有可访问性的缺点。当然,最理想的情况是,只有一种方法可以直接绑定到菜单的presentationMode,或者可以在菜单上配置对周围点击的处理。与此同时,上面的方法对我来说很有效。

wvyml7n5

wvyml7n53#

我想我有一个解决方案,但它是一个黑客. * 和 * 它不会与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就不会注意到您引用了私有类。

33qvvth1

33qvvth14#

在我的情况下,警报被阻止显示在类似的情况下,与菜单以及日期选择器冲突。我的解决方法是使用DispatchQueue的轻微延迟。

Rectangle()
    .frame(height: 200)
    .onTapGesture { 
        DispatchQueue.main.asyncAfter(deadline: .now() + 0.05){
           toggle.toggle()
        } 
     }

唯一真实的解决方案将发生在苹果修复/完善SwiftUI关于菜单(和日期选择器)的行为。

sqxo8psd

sqxo8psd5#

我可以通过不使用.onTapGesture并将List项目 Package 在Button中来解决这个问题。这可以通过转换为视图修改器来进一步改进。

List {
  Section {
    view.tappable {
      // do something
    }
  }
}

struct TappableModifier: ViewModifier {
    // use this over onTapGestures to resolve tap conflicts when dismissing context menus
    var action: () -> Void
    
    func body(content: Content) -> some View {
        Button {
            action()
        } label: {
            content
        }
    }
}

extension View {
    public func tappable(action: @escaping () -> Void) -> some View {
        ModifiedContent(content: self, modifier: TappableModifier(action: action))
    }
}
dluptydi

dluptydi6#

您可以通过使用FormList而不是“plain view”来获得所需的行为。当菜单在屏幕上时,所有按钮将默认禁用,但它们需要是按钮,每个单元格只有一个,它不会与tapGesture一起工作,因为你实际上正在做的是点击单元格,SwiftUI正在为你禁用TableView点击。
实现这一目标的关键要素是:

  • 使用FormList
  • 使用实际的Button。* 在您的示例中,您使用RectangletapGesture。*

我修改了你提供的代码,如果你打开菜单,你不能点击按钮:

struct SwiftUIView: View {

    @State private var toggle: Bool = true

    var body: some View {
        VStack {
            Spacer()
            if toggle {
                Text("on")
            } else {
                Text("off")
            }
            Spacer()
            
            /// We add a `List` (this could go at whole screen level)
            List {
                /// We use a `Button` that has a `Rectangle`
                /// rather than a tapGesture
                Button {
                    toggle.toggle()
                } label: {
                    Rectangle()
                        .frame(height: 200)
                }
            }
            .listStyle(.plain)
            .frame(height: 200)
            
            Spacer()
            Menu("Actions") {
                Button("Duplicate", action: { toggle.toggle() })
                Button("Rename", action: { toggle.toggle() })
                Button("Delete", action: { toggle.toggle() })
            }
            Spacer()
        }
    }
}

相关问题