SwiftUI中Menu未关闭时点击Button无法弹出sheet的问题及解决需求
问题详情
开发SwiftUI视图时遇到异常:视图包含Menu组件和触发Sheet弹窗的Button,当Menu处于展开状态时快速点击按钮,Sheet无法弹出,且后续点击该按钮也完全失效。控制台日志报错:
Attempt to present <TtGC7SwiftUI29PresentationHostingControllerVS_7AnyView: 0x113811a00> on <TtGC7SwiftUI19UIHostingControllerGVS_15ModifiedContentVS_7AnyViewVS_12RootModifier_: 0x112825600> (from <TtGC7SwiftUI19UIHostingControllerGVS_15ModifiedContentVS_7AnyViewVS_12RootModifier_: 0x112825600>) which is already presenting <_UIContextMenuActionsOnlyViewController: 0x10c1183c0>.
仅快速点击会触发该问题,长按按钮则正常。测试环境为Xcode 15,问题覆盖iOS 15、16、17版本。
复现代码:
import SwiftUI @main struct menuAndButtonApp: App { var body: some Scene { WindowGroup { ContentView() } } } struct ContentView: View { @State var modalIsPresented = false var body: some View { HStack(spacing: 28) { Menu("Open Menu") { Text("Menu Item") } Button("Present Modal") { modalIsPresented = true } }.sheet(isPresented: $modalIsPresented) { Text("Here is a sheet") } } }
目前已验证用DispatchQueue.main.asyncAfter(.now() + 0.2) { }包裹按钮操作可解决,但该方案需无条件延迟,无法精准判断Menu是否展开。
可行Workaround方案
方案1:基于UIKit控制器状态检测
通过UIViewControllerRepresentable获取底层宿主控制器,仅当无正在展示的控制器时触发Sheet,避免冲突:
import SwiftUI struct PresentationChecker: UIViewControllerRepresentable { @Binding var isTriggering: Bool var onSafePresent: () -> Void func makeUIViewController(context: Context) -> UIViewController { let vc = UIViewController() vc.view.backgroundColor = .clear return vc } func updateUIViewController(_ uiViewController: UIViewController, context: Context) { guard isTriggering else { return } // 检查当前是否有正在弹出的控制器 if uiViewController.presentingViewController == nil { onSafePresent() } else { isTriggering = false } } } struct ContentView: View { @State var modalIsPresented = false @State var triggerModal = false var body: some View { HStack(spacing: 28) { Menu("Open Menu") { Text("Menu Item") } Button("Present Modal") { triggerModal = true } } .overlay(PresentationChecker(isTriggering: $triggerModal) { modalIsPresented = true }) .sheet(isPresented: $modalIsPresented) { Text("Here is a sheet") } } }
方案2:追踪Menu展开状态
通过监听Menu的点击和消失事件,判断是否需要延迟触发Sheet:
struct ContentView: View { @State var modalIsPresented = false @State var isMenuExpanded = false var body: some View { HStack(spacing: 28) { Menu("Open Menu") { Text("Menu Item") .onDisappear { isMenuExpanded = false } } .onTapGesture { isMenuExpanded = true } Button("Present Modal") { if isMenuExpanded { DispatchQueue.main.asyncAfter(deadline: .now() + 0.15) { modalIsPresented = true } } else { modalIsPresented = true } } } .sheet(isPresented: $modalIsPresented) { Text("Here is a sheet") } } }
方案3:临时替换为Alert(应急用)
若上述方案仍有问题,可临时用Alert替代Sheet,规避SwiftUI的Sheet与Menu的冲突:
struct ContentView: View { @State var showAlert = false var body: some View { HStack(spacing: 28) { Menu("Open Menu") { Text("Menu Item") } Button("Present Modal") { showAlert = true } } .alert(isPresented: $showAlert) { Alert(title: Text("Here is a sheet"), dismissButton: .default(Text("OK"))) } } }
内容的提问来源于stack exchange,提问作者Andrew Bennet

