如何在SwiftUI中监听macOS所有应用的窗口开闭事件?
解决方案:监听macOS应用窗口的打开/关闭事件
前提:开启辅助功能权限
你的应用必须在「系统设置 > 隐私与安全性 > 辅助功能」中被授权,否则Accessibility相关API无法正常工作。
核心实现思路
通过AXObserver监听目标应用的kAXWindowsAttribute属性变化,当窗口列表更新时,对比前后的窗口集合来判断是窗口打开还是关闭事件;同时监听应用启动事件,为新启动的常规应用动态注册监听。
完整代码示例
import Cocoa import Accessibility class WindowMonitor: NSObject { // 存储各应用当前的窗口列表,用于对比变化 private var appWindows: [pid_t: [AXUIElement]] = [:] // 存储已创建的AXObserver实例,避免重复注册 private var observers: [pid_t: AXObserver] = [:] override init() { super.init() // 为已运行的常规应用初始化监听 registerObserversForRunningApps() // 监听新应用启动事件,动态注册监听 NotificationCenter.default.addObserver( self, selector: #selector(handleAppLaunch(_:)), name: NSWorkspace.didLaunchApplicationNotification, object: nil ) } private func registerObserversForRunningApps() { let workspace = NSWorkspace.shared let regularApps = workspace.runningApplications.filter { $0.activationPolicy == .regular } regularApps.forEach { app in guard let pid = app.processIdentifier as? pid_t else { return } registerObserver(for: pid) } } @objc private func handleAppLaunch(_ notification: Notification) { guard let app = notification.userInfo?[NSWorkspace.applicationUserInfoKey] as? NSRunningApplication, app.activationPolicy == .regular, let pid = app.processIdentifier as? pid_t else { return } registerObserver(for: pid) } private func registerObserver(for pid: pid_t) { guard observers[pid] == nil else { return } var observer: AXObserver? let callback: AXObserverCallback = { _, element, _, refcon in guard let monitor = refcon?.assumingMemoryBound(to: WindowMonitor.self).pointee else { return } monitor.handleWindowChange(for: element, pid: pid) } // 创建AXObserver实例 let createError = AXObserverCreate(pid, callback, &observer) guard createError == .success, let observer = observer else { print("Failed to create observer for pid \(pid): \(createError.rawValue)") return } // 注册窗口变化通知 let appElement = AXUIElementCreateApplication(pid) let registerError = AXObserverAddNotification( observer, appElement, kAXWindowsChangedNotification as CFString, Unmanaged.passUnretained(self).toOpaque() ) if registerError == .success { observers[pid] = observer // 将观察者加入RunLoop,确保能接收回调 CFRunLoopAddSource( CFRunLoopGetCurrent(), AXObserverGetRunLoopSource(observer), CFRunLoopMode.defaultMode ) // 初始化当前窗口列表 updateCurrentWindows(for: pid) } else { print("Failed to register windows notification for pid \(pid): \(registerError.rawValue)") } } private func updateCurrentWindows(for pid: pid_t) { let appElement = AXUIElementCreateApplication(pid) var value: AnyObject? let error = AXUIElementCopyAttributeValue(appElement, kAXWindowsAttribute as CFString, &value) guard error == .success, let windows = value as? [AXUIElement] else { appWindows[pid] = [] return } appWindows[pid] = windows } private func handleWindowChange(for element: AXUIElement, pid: pid_t) { let appElement = AXUIElementCreateApplication(pid) var value: AnyObject? let error = AXUIElementCopyAttributeValue(appElement, kAXWindowsAttribute as CFString, &value) guard error == .success, let currentWindows = value as? [AXUIElement] else { return } let previousWindows = appWindows[pid] ?? [] // 处理窗口打开事件 let newWindows = currentWindows.filter { !previousWindows.contains($0) } if !newWindows.isEmpty { handleNewWindow(appPID: pid, window: newWindows.first!) } // 处理窗口关闭事件 let closedWindows = previousWindows.filter { !currentWindows.contains($0) } if !closedWindows.isEmpty { handleClosedWindow(appPID: pid, window: closedWindows.first!) } // 更新存储的窗口列表 appWindows[pid] = currentWindows } private func handleNewWindow(appPID: pid_t, window: AXUIElement) { // 示例:针对Chrome新窗口执行操作 let workspace = NSWorkspace.shared if let app = workspace.runningApplications.first(where: { $0.processIdentifier == appPID }), app.bundleIdentifier == "com.google.Chrome" { print("Chrome新窗口已激活,执行自定义操作") // 在这里添加你的业务逻辑 } } private func handleClosedWindow(appPID: pid_t, window: AXUIElement) { // 窗口关闭后的自定义逻辑 } } // 使用示例 let monitor = WindowMonitor() CFRunLoopRun() // 保持RunLoop运行,否则监听会终止
关键注意事项
- 权限验证:首次运行时系统会弹出权限申请提示,务必在系统设置中完成授权,否则所有Accessibility API会返回权限错误。
- 应用兼容性:部分应用可能限制Accessibility访问,或窗口结构特殊,需要针对性调整监听逻辑。
- 内存管理:需妥善维护
AXObserver和窗口列表的存储,避免内存泄漏。 - RunLoop依赖:必须将
AXObserver的RunLoopSource加入当前RunLoop,否则无法接收回调事件。
内容的提问来源于stack exchange,提问作者J brian
相关产品推荐
相关产品推荐

