You need to enable JavaScript to run this app.
优惠活动
大模型
产品
解决方案
定价
更多

SwiftUI中Sheet与Menu组件冲突问题及解决方案问询

SwiftUI Menu与Sheet共存时的冲突Bug及规避方案

在以下SwiftUI示例中,页面包含一个Menu组件和一个触发Sheet的按钮。当Menu处于展开状态时点击该按钮,会触发如下错误日志(可能需要多次尝试复现),随后按钮会失效:

2023-03-02 12:01:59.850609+0100 SheetButtonDemo[50295:6802586] [Presentation] Attempt to present <_TtGC7SwiftUI29PresentationHostingControllerVS_7AnyView_: 0x124815200> on <_TtGC7SwiftUI19UIHostingControllerGVS_15ModifiedContentVS_7AnyViewVS_12RootModifier__: 0x10c809200> (from <_TtGC7SwiftUI19UIHostingControllerGVS_15ModifiedContentVS_7AnyViewVS_12RootModifier__: 0x10c809200>) which is already presenting <_UIContextMenuActionsOnlyViewController: 0x107d0bf20>.

这是SwiftUI的已知Bug,已上报为FB12026200,与社区中「SwiftUI: popover + sheet in different hierarchies problem」问题类似。

示例代码

struct ContentView: View {
    @State var showSheet = false
    var body: some View {
        
        VStack(spacing: 100) {
            Button("Show Sheet") {
                self.showSheet = true
            }
            Menu {
                Button("Action") {
                    print("Hello")
                }
            } label: {
                Text("Menu")
            }

        }
        .sheet(isPresented: $showSheet, content: {
            Text("Sheet")
        })
        .padding()
    }
}

问题演示

问题演示动图

可行规避方案

方案1:延迟触发Sheet,让Menu先关闭

通过短暂延迟给系统留出关闭Menu的时间,再触发Sheet展示,对用户体验影响极小:

Button("Show Sheet") {
    DispatchQueue.main.asyncAfter(deadline: .now() + 0.1) {
        self.showSheet = true
    }
}

方案2:借助UIKit层级展示Sheet

绕过SwiftUI的presentation逻辑,直接用UIKit的视图控制器来呈现Sheet,彻底避免层级冲突:
先添加UIViewController扩展用于获取顶层控制器:

extension UIViewController {
    static func topViewController() -> UIViewController? {
        let keyWindow = UIApplication.shared.connectedScenes
            .filter({$0.activationState == .foregroundActive})
            .map({$0 as? UIWindowScene})
            .compactMap({$0})
            .first?.windows
            .filter({$0.isKeyWindow}).first
        
        var topController = keyWindow?.rootViewController
        while let presentedViewController = topController?.presentedViewController {
            topController = presentedViewController
        }
        return topController
    }
}

再修改按钮点击逻辑:

Button("Show Sheet") {
    let sheetVC = UIHostingController(rootView: Text("Sheet"))
    UIViewController.topViewController()?.present(sheetVC, animated: true)
}

方案3:使用自定义Menu组件

完全替换系统Menu,用SwiftUI原生组件实现下拉菜单,避免系统内部的视图控制器参与层级:

struct CustomMenu: View {
    @State private var isMenuOpen = false
    
    var body: some View {
        Button("Menu") {
            isMenuOpen.toggle()
        }
        .overlay(
            Group {
                if isMenuOpen {
                    VStack(spacing: 0) {
                        Button("Action") {
                            print("Hello")
                            isMenuOpen = false
                        }
                        .padding()
                        .frame(maxWidth: .infinity)
                        .background(Color.white)
                    }
                    .border(Color.gray)
                    .offset(y: 40)
                    .onTapGesture {
                        isMenuOpen = false
                    }
                }
            }, alignment: .top
        )
    }
}

在ContentView中替换原Menu:

VStack(spacing: 100) {
    Button("Show Sheet") {
        self.showSheet = true
    }
    CustomMenu()
}
.sheet(isPresented: $showSheet, content: {
    Text("Sheet")
})

内容的提问来源于stack exchange,提问作者Ralf Ebert

相关产品推荐
方舟 Agent Plan

超全模态模型 × Harness 升级,最新支持 Deepseek-V4.1-Flash、GLM-5.3 系列、Doubao-Seedream-5.0-pro、Kimi-K3 (部分), 限时 9.9 元起

最近更新时间:2026.07.29 18:32:04