NeuroAgent

How to Detect Dock Clicks in macOS Swift Apps

Learn multiple approaches to detect when users click other apps in the Dock in your macOS Swift app. Use NSWorkspace notifications, Accessibility framework, and CGWindowList monitoring for reliable detection.

Question

How to detect in a macOS app when a user clicks another app in the Dock?

I’m developing a full-screen macOS app (floating level) that should automatically hide when the user launches another application from the Dock. What is the best approach to detect dock clicks in Swift for macOS?

NeuroAgent

To detect when a user clicks another app in the Dock, you can use a combination of NSWorkspace notifications and Accessibility framework monitoring in Swift. The most reliable approach involves listening for app activation events and monitoring window state changes to determine when your app should hide when another application is launched.


Contents


NSWorkspace Notification Approach

The NSWorkspace class provides notifications that can help you track application launches and activations. This is the most straightforward method for detecting when another app is launched from the Dock.

swift
import Cocoa

class AppDelegate: NSObject, NSApplicationDelegate {
    var workspace: NSWorkspace!
    var runningApps: [String: Process] = [:]
    
    func applicationDidFinishLaunching(_ notification: Notification) {
        workspace = NSWorkspace.shared
        setupWorkspaceNotifications()
    }
    
    private func setupWorkspaceNotifications() {
        // Listen for app launches
        workspace.notificationCenter.addObserver(
            forName: NSWorkspace.didLaunchApplicationNotification,
            object: nil,
            queue: .main
        ) { [weak self] notification in
            guard let app = notification.userInfo?[NSWorkspace.applicationUserInfoKey] as? NSRunningApplication else { return }
            self?.handleAppLaunch(app)
        }
        
        // Listen for app activations
        workspace.notificationCenter.addObserver(
            forName: NSWorkspace.didActivateApplicationNotification,
            object: nil,
            queue: .main
        ) { [weak self] notification in
            guard let app = notification.userInfo?[NSWorkspace.applicationUserInfoKey] as? NSRunningApplication else { return }
            self?.handleAppActivation(app)
        }
    }
    
    private func handleAppLaunch(_ app: NSRunningApplication) {
        let bundleId = app.bundleIdentifier ?? "unknown"
        print("App launched: \(app.localizedName ?? bundleId)")
        
        if bundleId != Bundle.main.bundleIdentifier {
            // Another app was launched, hide your app
            hideFullscreenApp()
        }
    }
    
    private func handleAppActivation(_ app: NSRunningApplication) {
        let bundleId = app.bundleIdentifier ?? "unknown"
        print("App activated: \(app.localizedName ?? bundleId)")
        
        if bundleId != Bundle.main.bundleIdentifier {
            // Another app was activated, hide your app
            hideFullscreenApp()
        }
    }
    
    private func hideFullscreenApp() {
        // Implement your app hiding logic here
        NSApp.setActivationPolicy(.prohibited)
        DispatchQueue.main.asyncAfter(deadline: .now() + 0.1) {
            NSApp.setActivationPolicy(.regular)
        }
    }
}

This approach uses the NSWorkspace API to monitor application launch and activation events. When a notification is received, it checks if the launched app is different from your current app and triggers the hide action.

Accessibility Framework Method

For more comprehensive monitoring, you can use the Accessibility framework to track application state changes and window visibility.

swift
import Cocoa
import ApplicationServices

class AppMonitor {
    private var observer: AXObserver?
    private var observedApp: AXUIElement?
    
    func startMonitoring() {
        guard let pid = NSWorkspace.shared.runningApplications.first(where: { $0.bundleIdentifier == Bundle.main.bundleIdentifier })?.processIdentifier else { return }
        
        let appElement: AXUIElement = AXUIElementCreateApplication(pid)
        
        var observer: AXObserver?
        let error = AXObserverCreate(pid, appStateChanged, &observer)
        
        if error == .success, let observer = observer {
            self.observer = observer
            
            // Observe app activation changes
            let notifications = [kAXFocusedUIElementChangedNotification, kAXApplicationActivatedNotification] as CFArray
            AXObserverAddNotification(observer, appElement, notifications, UnsafeMutableRawPointer(Unmanaged.passUnretained(self).toOpaque()))
            
            CFRunLoopAddSource(CFRunLoopGetCurrent(), AXObserverGetRunLoopSource(observer), .commonModes)
        }
    }
    
    private func appStateChanged(_ observer: AXObserver?, element: AXUIElement?, notification: CFString, userData: UnsafeMutableRawPointer?) {
        guard let userData = userData else { return }
        
        let monitor = Unmanaged<AppMonitor>.fromOpaque(userData).takeUnretainedValue()
        
        // Check if another app was activated
        monitor.checkForAppActivation()
    }
    
    private func checkForAppActivation() {
        let runningApps = NSWorkspace.shared.runningApplications
        let currentAppPid = NSWorkspace.shared.runningApplications.first(where: { $0.bundleIdentifier == Bundle.main.bundleIdentifier })?.processIdentifier
        
        let otherApps = runningApps.filter { $0.processIdentifier != currentAppPid }
        
        if !otherApps.isEmpty {
            hideFullscreenApp()
        }
    }
    
    private func hideFullscreenApp() {
        // Hide logic here
    }
}

The Accessibility framework provides more granular control over monitoring application state changes, as mentioned in the Stack Overflow discussion.

CGWindowList Monitoring

You can also use CGWindowList to monitor window changes across applications:

swift
import Cocoa

class WindowMonitor {
    private var timer: Timer?
    
    func startMonitoring() {
        timer = Timer.scheduledTimer(withTimeInterval: 1.0, repeats: true) { [weak self] _ in
            self?.checkWindowChanges()
        }
    }
    
    private func checkWindowChanges() {
        let options: CGWindowListOption = [.optionOnScreenOnly, .excludeDesktopElements]
        guard let windowList = CGWindowListCopyWindowInfo(options, kCGNullWindowID) as? [[String: Any]] else { return }
        
        let currentAppPid = NSWorkspace.shared.runningApplications.first(where: { $0.bundleIdentifier == Bundle.main.bundleIdentifier })?.processIdentifier
        
        let otherAppWindows = windowList.filter { window in
            guard let windowPid = window[kCGWindowOwnerPID as String] as? pid_t,
                  windowPid != currentAppPid else { return false }
            return true
        }
        
        if !otherAppWindows.isEmpty {
            hideFullscreenApp()
        }
    }
    
    private func hideFullscreenApp() {
        // Hide logic here
    }
}

Implementation Examples

Here’s a complete implementation combining multiple approaches for reliability:

swift
import Cocoa
import ApplicationServices

class DockClickMonitor {
    private var workspace: NSWorkspace?
    private var accessibilityObserver: AXObserver?
    private var windowMonitor: WindowMonitor?
    
    func startMonitoring() {
        setupWorkspaceNotifications()
        setupAccessibilityMonitoring()
        setupWindowMonitoring()
    }
    
    private func setupWorkspaceNotifications() {
        workspace = NSWorkspace.shared
        
        workspace?.notificationCenter.addObserver(
            forName: NSWorkspace.didLaunchApplicationNotification,
            object: nil,
            queue: .main
        ) { [weak self] notification in
            self?.handleAppLaunch(notification)
        }
        
        workspace?.notificationCenter.addObserver(
            forName: NSWorkspace.didActivateApplicationNotification,
            object: nil,
            queue: .main
        ) { [weak self] notification in
            self?.handleAppActivation(notification)
        }
    }
    
    private func setupAccessibilityMonitoring() {
        guard let pid = NSWorkspace.shared.runningApplications.first(where: { $0.bundleIdentifier == Bundle.main.bundleIdentifier })?.processIdentifier else { return }
        
        let appElement: AXUIElement = AXUIElementCreateApplication(pid)
        
        var observer: AXObserver?
        let error = AXObserverCreate(pid, { observer, element, notification, userData in
            guard let userData = userData else { return }
            let monitor = Unmanaged<DockClickMonitor>.fromOpaque(userData).takeUnretainedValue()
            monitor.handleAccessibilityEvent(notification)
        }, &observer)
        
        if error == .success, let observer = observer {
            self.accessibilityObserver = observer
            
            let notifications = [kAXFocusedUIElementChangedNotification, kAXApplicationActivatedNotification] as CFArray
            AXObserverAddNotification(observer, appElement, notifications, UnsafeMutableRawPointer(Unmanaged.passUnretained(self).toOpaque()))
            
            CFRunLoopAddSource(CFRunLoopGetCurrent(), AXObserverGetRunLoopSource(observer), .commonModes)
        }
    }
    
    private func setupWindowMonitoring() {
        windowMonitor = WindowMonitor()
        windowMonitor?.startMonitoring()
    }
    
    private func handleAppLaunch(_ notification: Notification) {
        guard let app = notification.userInfo?[NSWorkspace.applicationUserInfoKey] as? NSRunningApplication else { return }
        let bundleId = app.bundleIdentifier ?? "unknown"
        
        if bundleId != Bundle.main.bundleIdentifier {
            print("Detected app launch: \(app.localizedName ?? bundleId)")
            hideFullscreenApp()
        }
    }
    
    private func handleAppActivation(_ notification: Notification) {
        guard let app = notification.userInfo?[NSWorkspace.applicationUserInfoKey] as? NSRunningApplication else { return }
        let bundleId = app.bundleIdentifier ?? "unknown"
        
        if bundleId != Bundle.main.bundleIdentifier {
            print("Detected app activation: \(app.localizedName ?? bundleId)")
            hideFullscreenApp()
        }
    }
    
    private func handleAccessibilityEvent(_ notification: CFString) {
        print("Accessibility event: \(notification)")
        checkForOtherActiveApps()
    }
    
    private func checkForOtherActiveApps() {
        let runningApps = NSWorkspace.shared.runningApplications
        let currentAppPid = NSWorkspace.shared.runningApplications.first(where: { $0.bundleIdentifier == Bundle.main.bundleIdentifier })?.processIdentifier
        
        let otherApps = runningApps.filter { $0.processIdentifier != currentAppPid }
        
        if !otherApps.isEmpty {
            hideFullscreenApp()
        }
    }
    
    private func hideFullscreenApp() {
        // Implement your fullscreen app hiding logic
        DispatchQueue.main.async {
            NSApp.setActivationPolicy(.prohibited)
            DispatchQueue.main.asyncAfter(deadline: .now() + 0.2) {
                NSApp.setActivationPolicy(.regular)
            }
        }
    }
}

Best Practices and Considerations

1. Combine Multiple Approaches

  • Use both NSWorkspace notifications and Accessibility monitoring for maximum reliability
  • Window monitoring provides additional fallback coverage

2. Performance Optimization

  • Debounce rapid events to avoid excessive processing
  • Use proper cleanup in dealloc or deinit methods

3. Permission Requirements

  • Your app may need Accessibility permissions for the advanced monitoring
  • Request permissions gracefully from users

4. App State Management

  • Store your app’s state properly before hiding
  • Ensure proper restoration when your app becomes active again

5. Error Handling

  • Handle cases where monitoring fails gracefully
  • Provide fallback mechanisms
swift
// Permission request example
func requestAccessibilityPermission() {
    let options = [kAXTrustedCheckOptionPrompt.takeUnretainedValue() as String: true] as CFDictionary
    let accessEnabled = AXIsProcessTrustedWithOptions(options)
    
    if accessEnabled {
        print("Accessibility permission granted")
        setupAccessibilityMonitoring()
    } else {
        print("Accessibility permission denied")
    }
}

The combination of these approaches provides comprehensive monitoring of dock clicks and app activations, ensuring your full-screen app can reliably detect and respond to user interactions with other applications.


Sources

  1. NSWorkspace Documentation - Application Monitoring
  2. Stack Overflow: When clicking on an app icon in the dock after it’s opened
  3. Stack Overflow: How to detect when an app is launched on macOS
  4. Accessibility Framework Documentation
  5. CGWindowList Reference

Conclusion

To effectively detect when a user clicks another app in the Dock in your macOS Swift application, you should:

  1. Implement NSWorkspace notifications as your primary method for detecting app launches and activations
  2. Add Accessibility framework monitoring for more granular event detection
  3. Use CGWindowList monitoring as a reliable fallback mechanism
  4. Combine multiple approaches for maximum reliability and coverage
  5. Handle permissions gracefully and provide proper error handling

The multi-layered approach ensures that your app will reliably detect dock clicks and can automatically hide when another application is launched, providing the smooth user experience you’re looking for for your floating full-screen application.