SwiftUI iOS端.contextMenu关闭监听及.onDrag状态异常问题
核心问题原因
系统默认长按交互会同时触发上下文菜单唤起和onDrag的前置初始化逻辑:
- iOS 16.2之前的系统中,点击上下文菜单外部区域关闭菜单时,不会主动销毁
onDrag闭包里创建的NSItemProvider实例,也不会抛出菜单关闭的全局通知,因此写在ItemProvider析构函数里的dragging状态重置逻辑不会触发,导致背景一直停留在灰色状态。 - 默认实现下
onDrag的触发时机早于实际拖拽开始,长按唤起上下文菜单时就会执行onDrag闭包,导致dragging状态被提前置为true。
效果演示:
复现代码(原问题代码):
struct ContentView: View { @State var dragging = false var body: some View { ZStack { Rectangle() .foregroundColor(.blue) .frame(width: 100, height: 100) .onDrag { dragging = true let provider = ItemProvider(contentsOf: URL(string: "Test")!)! provider.didEnd = { DispatchQueue.main.async { dragging = false } } print("init ItemProvider") return provider } .contextMenu { Button("Close Menu") { dragging = false } } } .frame(maxWidth: .infinity, maxHeight: .infinity) .background(dragging ? Color.gray : Color.white) } } class ItemProvider: NSItemProvider { var didEnd: (() -> Void)? deinit { print("deinit ItemProvider") didEnd?() } }
适配方案
方案1:通过系统绑定监听上下文菜单关闭(iOS 14+,最低成本适配)
iOS 14 起系统提供了带展示状态绑定的contextMenu修饰符,可直接获取菜单的关闭时机,覆盖所有关闭场景(点击菜单按钮、点击外部区域、手势dismiss):
- 新增
@State变量绑定上下文菜单的展示状态 - 监听该状态变化,菜单关闭时统一重置
dragging状态
修改后的核心代码:
struct ContentView: View { @State var dragging = false // 新增菜单展示状态绑定 @State var showContextMenu = false var body: some View { ZStack { Rectangle() .foregroundColor(.blue) .frame(width: 100, height: 100) .onDrag { dragging = true let provider = ItemProvider(contentsOf: URL(string: "Test")!)! provider.didEnd = { DispatchQueue.main.async { dragging = false } } return provider } // 使用带状态绑定的contextMenu构造器 .contextMenu(isPresented: $showContextMenu) { Button("Close Menu") { dragging = false } } // 监听菜单状态,关闭时重置拖拽状态 .onChange(of: showContextMenu) { isShow in guard !isShow else { return } dragging = false } } .frame(maxWidth: .infinity, maxHeight: .infinity) .background(dragging ? Color.gray : Color.white) } }
注:iOS 16.2以下系统中,该方案下
ItemProvider实例可能会延迟几秒析构,属于系统正常内存调度,不影响业务逻辑。
方案2:自定义手势分离交互逻辑(全系统版本兼容,可解决onDrag提前触发问题)
如果需要支持iOS 13,或是想从根源避免「打开上下文菜单就提前触发onDrag」的问题,可以通过自定义手势拆分两种交互的触发时机:
- 给视图添加长按手势,设置0.3s的触发阈值,识别到长按时先记录手指初始位置
- 长按过程中如果手指移动距离超过10pt,判定为拖拽操作,此时才将
dragging置为true,调用拖拽逻辑 - 长按达到0.5s(系统上下文菜单默认触发时长)且手指未发生明显位移时,判定为唤起上下文菜单,此时不修改
dragging状态 - 根视图添加点击手势,上下文菜单展示时点击空白区域,直接关闭菜单并重置所有状态
版本说明
2022年12月验证:iOS 16.2系统已修复该问题,原代码在该版本下可正常运行,点击菜单外部关闭时会正常销毁ItemProvider并触发状态重置,低版本系统仍需使用上述方案适配。
内容的提问来源于stack exchange,提问作者viedev
相关产品推荐
相关产品推荐

