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

SwiftUI iOS端.contextMenu关闭监听及.onDrag状态异常问题

SwiftUI 同时使用.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

相关产品推荐
方舟 Agent Plan

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

最近更新时间:2026.08.28 20:06:28