如何在macOS中检测同一应用内的窗口焦点变化?
Great question! The NSWorkspaceDidActivateApplication notification only triggers when you switch between different applications—it won't fire when you toggle windows within the same app, since the app itself stays active. Here are the most reliable ways to detect window focus changes, whether you need system-wide coverage or just want to monitor your own app:
Solution 1: Use the Accessibility API (Global, Real-Time)
This is the most robust method for tracking window focus across all apps, including same-app window switches. The catch? Your app needs to request Accessibility permissions from the user (this is required by macOS for any tool that monitors system-wide UI interactions).
Steps to Implement:
- First, add a usage description to your
Info.plistto explain why you need Accessibility access:- Add the key
NSAccessibilityUsageDescriptionwith a message like "This app needs access to monitor window focus changes."
- Add the key
- Use the
ApplicationServicesframework to register a global observer for window focus changes:
After setting this up, the user will need to enable your app in System Settings > Privacy & Security > Accessibility for it to work.import ApplicationServices class GlobalWindowFocusMonitor { private var accessibilityObserver: AXObserver? init() { // Create an observer for system-wide accessibility events let systemWideElement = AXUIElementCreateSystemWide() var observerRef: AXObserver? let status = AXObserverCreate(systemWideElement, windowFocusChangedCallback, &observerRef) guard status == .success, let observer = observerRef else { print("Failed to initialize accessibility observer") return } accessibilityObserver = observer // Attach the observer to the main run loop CFRunLoopAddSource( CFRunLoopGetCurrent(), AXObserverGetRunLoopSource(observer), CFRunLoopMode.commonModes ) // Register for the focused window change notification AXObserverAddNotification( observer, systemWideElement, kAXFocusedWindowChangedNotification as CFString, nil ) } // Callback triggered when the focused window changes private func windowFocusChangedCallback( observer: AXObserver, element: AXUIElement, notification: CFString, userData: UnsafeMutableRawPointer? ) { // Fetch the currently focused window var focusedWindow: AXUIElement? AXUIElementCopyAttributeValue(element, kAXFocusedWindowAttribute as CFString, &focusedWindow) guard let window = focusedWindow else { return } // Get window details (PID, title, etc.) var pid: pid_t = 0 AXUIElementGetPid(window, &pid) var windowTitle: AnyObject? AXUIElementCopyAttributeValue(window, kAXTitleAttribute as CFString, &windowTitle) let title = windowTitle as? String ?? "Untitled Window" print("Focused window updated: \(title) (PID: \(pid))") } }
Solution 2: Poll CGWindowList (No Permissions, Non-Real-Time)
If you want to avoid asking for Accessibility permissions, you can periodically check the system's window list to find which window has focus. This method isn't real-time (you'll set a polling interval), but it works without extra permissions.
Example Implementation:
import Cocoa class WindowFocusPoller { private var pollingTimer: Timer? // Start polling with a custom interval (default: 0.5 seconds) func startMonitoring(interval: TimeInterval = 0.5) { pollingTimer = Timer.scheduledTimer(withTimeInterval: interval, repeats: true) { [weak self] _ in self?.checkFocusedWindow() } } func stopMonitoring() { pollingTimer?.invalidate() pollingTimer = nil } private func checkFocusedWindow() { // Get a list of on-screen windows (excluding desktop elements) let windowListOptions = CGWindowListOption(arrayLiteral: .optionOnScreenOnly, .optionExcludeDesktopElements) guard let windowInfoList = CGWindowListCopyWindowInfo(windowListOptions, CGMainDisplayID()) as? [[String: Any]] else { return } // Find the window with keyboard focus for windowInfo in windowInfoList { if let hasFocus = windowInfo[kCGWindowHasKeyboardFocus as String] as? Bool, hasFocus { let windowTitle = windowInfo[kCGWindowName as String] as? String ?? "No Title" let appName = windowInfo[kCGWindowOwnerName as String] as? String ?? "Unknown App" let pid = windowInfo[kCGWindowOwnerPID as String] as? Int ?? 0 print("Current focused window: \(windowTitle) (App: \(appName), PID: \(pid))") break } } } }
Adjust the polling interval based on your needs—shorter intervals mean more frequent checks (but slightly higher resource usage).
Bonus: Monitor Only Your Own App's Windows
If you don't need system-wide coverage and only care about window switches within your own app, you can use native Cocoa notifications directly:
// In your AppDelegate or window controller override func applicationDidFinishLaunching(_ aNotification: Notification) { // Listen for main window changes NotificationCenter.default.addObserver( self, selector: #selector(onWindowBecameMain(_:)), name: NSWindow.didBecomeMainNotification, object: nil ) NotificationCenter.default.addObserver( self, selector: #selector(onWindowResignedMain(_:)), name: NSWindow.didResignMainNotification, object: nil ) } @objc private func onWindowBecameMain(_ notification: Notification) { if let window = notification.object as? NSWindow { print("Window became main: \(window.title ?? "Untitled")") } } @objc private func onWindowResignedMain(_ notification: Notification) { if let window = notification.object as? NSWindow { print("Window lost focus: \(window.title ?? "Untitled")") } }
This is lightweight and doesn't require any special permissions, since it only monitors your app's own windows.
内容的提问来源于stack exchange,提问作者Dr Force




