M1 macOS 11.4设备上NSOpenPanel、NSSavePanel调用runModal立即自动取消
NSOpenPanel自动消失/立即返回取消的解决方案
问题原因
1. 事件循环调度冲突
你在openDocument菜单项的回调中同步发送通知、同步调用runModal弹出面板,此时菜单点击的系统事件处理流程还未结束,macOS 11.x(Big Sur)在M1设备上的事件循环调度逻辑与Intel设备存在差异,会主动取消未在事件循环结束后弹出的模态面板,触发立即返回取消的问题。
注意:
DispatchQueue.main.async的作用不只是切换到主线程,即使当前已经在主线程,该方法也会把代码逻辑放到下一个RunLoop周期执行,避开当前菜单事件的生命周期,刚好可以解决该冲突。
2. 代码未定义行为
原AppDelegate中发送通知的代码nc.post(Notification(name: Notification.Name("documentOpenRequested"), object: object))里,object变量未声明定义,属于未定义行为,M1的内存模型与Intel架构不同,该异常会导致后续创建的NSOpenPanel对象被系统提前释放。
3. 系统权限/第三方软件干扰
- 如果用户未给App开放对应目录的文件访问权限,系统会自动关闭打开面板,返回取消结果
- 窗口管理类、快捷键管理类、安全杀毒类第三方软件可能会主动拦截模态弹窗,触发自动取消
修复方案
第一步:修正通知发送代码
class AppDelegate: NSObject, NSApplicationDelegate { @IBAction func openDocument(_ sender: NSMenuItem) { let nc = NotificationCenter.default // 修正未定义的object参数,不需要传值则填nil nc.post(Notification(name: Notification.Name("documentOpenRequested"), object: nil)) } }
第二步:调整面板弹出时机
@objc func doOpenDocument(_ notification: Notification) { print ("doOpenDocument called on MainThread: \(Thread.current.isMainThread)") var URLToOpen: URL? if let selectedURL = notification.object as? URL { URLToOpen = selectedURL } else { // 调度到下一个RunLoop执行,避开当前菜单事件生命周期 DispatchQueue.main.async { let openPanel = NSOpenPanel() openPanel.allowsMultipleSelection = false openPanel.canChooseDirectories = false openPanel.canCreateDirectories = false openPanel.canChooseFiles = true openPanel.allowedFileTypes = ["sdf", "json", "txt"] openPanel.allowsOtherFileTypes = true let response = openPanel.runModal() if response == .OK, let myURL = openPanel.url { URLToOpen = myURL NSDocumentController.shared.noteNewRecentDocumentURL(myURL) } else { print ("RunModal exited with response not OK: \(response.rawValue)") } guard let theURL = URLToOpen else { return } // 原有处理URL的业务逻辑移到此处 } return } guard let theURL = URLToOpen else { return } // 处理notification带的URL的逻辑保留在此处 }
第三步:用户侧排查
- 引导用户检查「系统设置>安全与隐私>文件和文件夹」,确认你的App已获得需要的文件访问权限
- 引导用户暂时退出第三方窗口管理、快捷键、杀毒类软件,测试问题是否复现
内容的提问来源于stack exchange,提问作者greg
相关产品推荐
相关产品推荐

