如何让SwiftUI macOS App在窗口失焦时保持后台运行
macOS App后台单键快捷键实现方案
核心问题说明
macOS系统默认会优先拦截无修饰键的单键输入(比如单独按A、B),交给当前活跃的App处理,普通全局快捷键框架(如你提到的HotKeys)通常只支持带修饰键的组合。要实现后台单键触发,需要用到更底层的事件监听机制,同时确保App能在后台持续运行。
具体实现步骤
1. 配置App后台运行权限
在项目的Info.plist中添加NSUIElement键,设置值为YES。这会让App以后台模式运行,不在Dock中显示,同时允许App在无窗口焦点时接收系统事件。
2. 使用CGEventTap实现全局单键监听
CGEventTap是Core Graphics提供的底层事件拦截机制,可以捕获系统级的键盘事件,包括无修饰键的单键输入。以下是Swift示例代码:
import Cocoa import CoreGraphics class GlobalKeyListener { private var eventTap: CFMachPort? private var runLoopSource: CFRunLoopSource? func startListening() { guard let eventTap = CGEvent.tapCreate( tap: .cgSessionEventTap, place: .headInsertEventTap, options: .defaultTap, eventsOfInterest: CGEventMask(1 << CGEventType.keyDown.rawValue), callback: { proxy, type, event, refcon in let keyCode = event.getIntegerValueField(.keyboardEventKeycode) // 监听单键示例:比如监听键盘码为0x00的A键 if keyCode == 0x00 { // 在这里执行你的任务逻辑 print("A键被按下,执行后台任务") // 返回nil表示不拦截事件,让其他App也能接收;返回event则拦截 return nil } return event }, userInfo: nil ) else { print("无法创建事件Tap,请检查辅助功能权限") return } self.eventTap = eventTap runLoopSource = CFMachPortCreateRunLoopSource(kCFAllocatorDefault, eventTap, 0) CFRunLoopAddSource(CFRunLoopGetCurrent(), runLoopSource, .commonModes) CGEvent.tapEnable(tap: eventTap, enable: true) } func stopListening() { if let eventTap = eventTap { CGEvent.tapEnable(tap: eventTap, enable: false) CFRunLoopRemoveSource(CFRunLoopGetCurrent(), runLoopSource, .commonModes) runLoopSource = nil self.eventTap = nil } } } // 在App启动时启动监听 @main class AppDelegate: NSObject, NSApplicationDelegate { let keyListener = GlobalKeyListener() func applicationDidFinishLaunching(_ aNotification: Notification) { keyListener.startListening() } func applicationWillTerminate(_ aNotification: Notification) { keyListener.stopListening() } }
3. 处理辅助功能权限
CGEventTap需要App获得系统的辅助功能权限。首次运行时,系统会弹出权限请求弹窗,如果用户拒绝,你需要在代码中检测权限状态,并引导用户到「系统设置-隐私与安全性-辅助功能」中开启权限。
可以通过以下代码检测权限:
func checkAccessibilityPermission() -> Bool { let options: NSDictionary = [kAXTrustedCheckOptionPrompt.takeRetainedValue() as NSString: true] return AXIsProcessTrustedWithOptions(options) }
4. 避免冲突与优化
- 尽量选择不常用的单键作为触发键,避免和系统或其他App的功能冲突;
- 在App退出或进入 inactive 状态时,记得停止事件监听,减少系统资源占用;
- 如果需要拦截事件(不让其他App接收该单键输入),在回调中返回
nil即可;如果只是监听不拦截,返回原始event。
替代方案说明
如果你不想用底层的CGEventTap,也可以尝试使用MASShortcut框架(但同样需要处理权限),不过它对单键的支持也需要额外配置,本质上还是依赖系统事件机制。
内容的提问来源于stack exchange,提问作者Ekoreh
相关产品推荐
相关产品推荐

