如何在Exposé/Mission Control时自动关闭NSStatusBar中的NSPopover
嘿,这个问题我刚好研究过!其实不用费劲去监听F3按键本身,Mac系统本身会给应用发送各种状态变化的通知,咱们完全可以利用这些系统通知来实现NSPopover在Mission Control(F3)操作时自动隐藏再恢复的效果,而且全程不需要辅助功能权限,和1Password的实现思路一致。
我们的核心逻辑是监听系统级的状态变化通知,而不是直接捕获键盘事件——毕竟F3是系统快捷键,直接监听需要额外权限,但系统会主动告诉我们它要进入/退出Mission Control、切换空间这类操作,我们只需要响应这些通知即可。
1. 先保存Popover的显示状态
首先在你的NSStatusItem相关类里,加个变量记录Popover当前是否处于显示状态,方便后续恢复:
private var wasPopoverVisible = false
2. 监听应用激活状态变化(区分Mission Control)
当你按下F3打开Mission Control时,系统会临时把Mission Control设为前台激活应用,此时你的应用会失去激活状态。我们可以监听这个状态变化,并且判断是否是Mission Control导致的:
首先在初始化代码里添加通知监听:
// 监听应用失去激活/重新激活的通知 NotificationCenter.default.addObserver(self, selector: #selector(appDidResignActive), name: NSApplication.didResignActiveNotification, object: nil) NotificationCenter.default.addObserver(self, selector: #selector(appDidBecomeActive), name: NSApplication.didBecomeActiveNotification, object: nil)
然后实现对应的处理方法:
@objc private func appDidResignActive() { guard let frontApp = NSWorkspace.shared.frontmostApplication else { return } // 只有当激活的是Mission Control(bundle ID固定为com.apple.exposelauncher)时,才记录状态并隐藏Popover if frontApp.bundleIdentifier == "com.apple.exposelauncher" && popover.isShown { wasPopoverVisible = true popover.close() } else { wasPopoverVisible = false } } @objc private func appDidBecomeActive() { // 当应用重新激活(退出Mission Control)时,恢复之前显示的Popover if wasPopoverVisible { popover.show(relativeTo: statusItem.button!.bounds, of: statusItem.button!, preferredEdge: .maxY) wasPopoverVisible = false } }
3. 监听空间切换事件
如果用户在Mission Control里直接切换空间,我们也需要让Popover先隐藏,再在新空间里重新显示。这时候可以监听NSWorkspace的空间变化通知:
// 添加空间切换监听 NotificationCenter.default.addObserver(self, selector: #selector(activeSpaceChanged), name: NSWorkspace.activeSpaceDidChangeNotification, object: nil) @objc private func activeSpaceChanged() { if popover.isShown { wasPopoverVisible = true popover.close() // 延迟一点再恢复,确保空间切换完成 DispatchQueue.main.asyncAfter(deadline: .now() + 0.1) { if self.wasPopoverVisible { self.popover.show(relativeTo: self.statusItem.button!.bounds, of: self.statusItem.button!, preferredEdge: .maxY) self.wasPopoverVisible = false } } } }
4. 收尾:移除通知观察者
记得在类销毁时移除所有通知监听,避免内存泄漏:
deinit { NotificationCenter.default.removeObserver(self) }
为什么不需要辅助功能权限?
因为我们用到的都是AppKit提供的公开系统通知和API,这些是系统主动推送给所有应用的,不需要访问任何隐私敏感的权限(比如辅助功能需要的进程信任权限),所以像1Password这类应用完全可以在不申请额外权限的前提下实现这个功能。
额外优化建议
- 把Popover的
behavior属性设置为NSPopoverBehavior.transient,这样用户点击Popover外部时也会自动隐藏,和系统原生控件的行为保持一致。 - 延迟恢复Popover的时间可以根据实际情况调整,确保空间切换或Mission Control动画完成后再显示,避免视觉上的突兀。
内容的提问来源于stack exchange,提问作者Alex Beals

