Другое

Как обнаруживать клики по Dock в macOS Swift приложениях

Узнайте несколько способов обнаружения, когда пользователи кликают по другим приложениям в Dock в вашем macOS Swift приложении. Используйте уведомления NSWorkspace, фреймворк Accessibility и мониторинг CGWindowList для надежного обнаружения.

Как обнаружить в приложении macOS, когда пользователь кликает другое приложение в Dock?

Я разрабатыва полноэкранное приложение для macOS (плавающий уровень), которое должно автоматически скрываться, когда пользователь запускает другое приложение из Dock. Какой лучший подход для обнаружения кликов по Dock в Swift для macOS?

Для обнаружения щелчка пользователя по другому приложению в Dock можно использовать комбинацию уведомлений NSWorkspace и мониторинга фреймворка Accessibility в Swift. Наиболее надежный подход включает прослушивание событий активации приложений и отслеживание изменений состояния окна для определения, когда ваше приложение должно скрываться при запуске другого приложения.


Содержание


Подход с использованием уведомлений NSWorkspace

Класс NSWorkspace предоставляет уведомления, которые могут помочь отслеживать запуск и активацию приложений. Это наиболее прямой метод для обнаружения запуска другого приложения из 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() {
        // Прослушиваем запуски приложений
        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)
        }
        
        // Прослушиваем активации приложений
        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.localizedName ?? bundleId)")
        
        if bundleId != Bundle.main.bundleIdentifier {
            // Запущено другое приложение, скрываем ваше приложение
            hideFullscreenApp()
        }
    }
    
    private func handleAppActivation(_ app: NSRunningApplication) {
        let bundleId = app.bundleIdentifier ?? "unknown"
        print("Приложение активировано: \(app.localizedName ?? bundleId)")
        
        if bundleId != Bundle.main.bundleIdentifier {
            // Активировано другое приложение, скрываем ваше приложение
            hideFullscreenApp()
        }
    }
    
    private func hideFullscreenApp() {
        // Реализуйте здесь логику скрытия вашего приложения
        NSApp.setActivationPolicy(.prohibited)
        DispatchQueue.main.asyncAfter(deadline: .now() + 0.1) {
            NSApp.setActivationPolicy(.regular)
        }
    }
}

Этот подход использует API NSWorkspace для мониторинга событий запуска и активации приложений. При получении уведомления проверяется, было ли запущено другое приложение по сравнению с вашим текущим приложением, и запускается действие по скрытию.

Метод с использованием фреймворка Accessibility

Для более комплексного мониторинга можно использовать фреймворк Accessibility для отслеживания изменений состояния приложения и видимости окна.

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
            
            // Наблюдаем за изменениями активации приложения
            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()
        
        // Проверяем, было ли активировано другое приложение
        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() {
        // Логика скрытия здесь
    }
}

Фреймворк Accessibility предоставляет более детальный контроль над мониторингом изменений состояния приложения, как упоминается в обсуждении на Stack Overflow.

Мониторинг CGWindowList

Также можно использовать CGWindowList для отслеживания изменений окон между приложениями:

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() {
        // Логика скрытия здесь
    }
}

Примеры реализации

Вот полная реализация, объединяющая несколько подходов для повышения надежности:

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("Обнаружен запуск приложения: \(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("Обнаружена активация приложения: \(app.localizedName ?? bundleId)")
            hideFullscreenApp()
        }
    }
    
    private func handleAccessibilityEvent(_ notification: CFString) {
        print("Событие Accessibility: \(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() {
        // Реализуйте логику скрытия полноэкранного приложения
        DispatchQueue.main.async {
            NSApp.setActivationPolicy(.prohibited)
            DispatchQueue.main.asyncAfter(deadline: .now() + 0.2) {
                NSApp.setActivationPolicy(.regular)
            }
        }
    }
}

Лучшие практики и рекомендации

1. Комбинируйте несколько подходов

  • Используйте как уведомления NSWorkspace, так и мониторинг Accessibility для максимальной надежности
  • Мониторинг окон обеспечивает дополнительную резервную coverage

2. Оптимизация производительности

  • Используйте дебаунсинг для быстрых событий, чтобы избежать избыточной обработки
  • Используйте правильную очистку в методах dealloc или deinit

3. Требования к разрешениям

  • Вашему приложению могут понадобиться разрешения Accessibility для расширенного мониторинга
  • Запрашивайте разрешения у пользователей корректным образом

4. Управление состоянием приложения

  • Правильно сохраняйте состояние вашего приложения перед скрытием
  • Обеспечьте правильное восстановление при повторной активации вашего приложения

5. Обработка ошибок

  • Корректно обрабатывайте случаи сбоя мониторинга
  • Предоставляйте механизмы резервного копирования
swift
// Пример запроса разрешения
func requestAccessibilityPermission() {
    let options = [kAXTrustedCheckOptionPrompt.takeUnretainedValue() as String: true] as CFDictionary
    let accessEnabled = AXIsProcessTrustedWithOptions(options)
    
    if accessEnabled {
        print("Разрешение Accessibility предоставлено")
        setupAccessibilityMonitoring()
    } else {
        print("Разрешение Accessibility отклонено")
    }
}

Комбинация этих подходов обеспечивает комплексный мониторинг щелчков по Dock и активации приложений, гарантируя, что ваше полноэкранное приложение может надежно обнаруживать и реагировать на взаимодействие пользователя с другими приложениями.


Источники

  1. Документация NSWorkspace - Мониторинг приложений
  2. Stack Overflow: При щелчке по значку приложения в Dock после его открытия
  3. Stack Overflow: Как обнаружить запуск приложения на macOS
  4. Документация фреймворка Accessibility
  5. Справочник по CGWindowList

Заключение

Чтобы эффективно обнаруживать, когда пользователь щелкает по другому приложению в Dock в вашем приложении macOS на Swift, вы должны:

  1. Реализовать уведомления NSWorkspace как основной метод для обнаружения запуска и активации приложений
  2. Добавить мониторинг фреймворка Accessibility для более детального обнаружения событий
  3. Использовать мониторинг CGWindowList в качестве надежного механизма резервного копирования
  4. Комбинировать несколько подходов для максимальной надежности и покрытия
  5. Корректно обрабатывать разрешения и предоставлять надлежащую обработку ошибок

Многоуровневый подход гарантирует, что ваше приложение будет надежно обнаруживать щелчки по Dock и автоматически скрываться при запуске другого приложения, обеспечивая плавный пользовательский опыт для вашего полноэкранного плавающего приложения.

Авторы
Проверено модерацией
Модерация