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?
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
- Accessibility Framework Method
- CGWindowList Monitoring
- Implementation Examples
- Best Practices and Considerations
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.
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.
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:
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:
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
deallocordeinitmethods
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
// 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
- NSWorkspace Documentation - Application Monitoring
- Stack Overflow: When clicking on an app icon in the dock after it’s opened
- Stack Overflow: How to detect when an app is launched on macOS
- Accessibility Framework Documentation
- CGWindowList Reference
Conclusion
To effectively detect when a user clicks another app in the Dock in your macOS Swift application, you should:
- Implement NSWorkspace notifications as your primary method for detecting app launches and activations
- Add Accessibility framework monitoring for more granular event detection
- Use CGWindowList monitoring as a reliable fallback mechanism
- Combine multiple approaches for maximum reliability and coverage
- 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.