diff --git a/alt-tab-macos/api-wrappers/HelperExtensions.swift b/alt-tab-macos/api-wrappers/HelperExtensions.swift index 7fbe44c1..183530a5 100644 --- a/alt-tab-macos/api-wrappers/HelperExtensions.swift +++ b/alt-tab-macos/api-wrappers/HelperExtensions.swift @@ -15,11 +15,11 @@ extension Optional { // add throw-on-nil method on Optional func orThrow() throws -> Wrapped { switch self { - case .some(let value): - return value - case .none: - Thread.callStackSymbols.forEach { print($0) } - throw NSError.make(domain: "Optional", message: "Optional contained nil") + case .some(let value): + return value + case .none: + Thread.callStackSymbols.forEach { print($0) } + throw NSError.make(domain: "Optional", message: "Optional contained nil") } } } @@ -64,3 +64,11 @@ extension Collection { return flatMap { ($0 as? [Any])?.joined() ?? [$0] } } } + +// removing an objc KVO observer if there is none throws an exception +extension NSObject { + func safeRemoveObserver(_ observer: NSObject, _ key: String) { + guard observationInfo != nil else { return } + removeObserver(observer, forKeyPath: key) + } +} diff --git a/alt-tab-macos/logic/Application.swift b/alt-tab-macos/logic/Application.swift index 9a5a670f..27b1e5e4 100644 --- a/alt-tab-macos/logic/Application.swift +++ b/alt-tab-macos/logic/Application.swift @@ -16,6 +16,10 @@ class Application: NSObject { } } + func removeObserver() { + runningApplication.safeRemoveObserver(self, "isFinishedLaunching") + } + private func addAndObserveWindows() { axUiElement = AXUIElementCreateApplication(runningApplication.processIdentifier) AXObserverCreate(runningApplication.processIdentifier, axObserverApplicationCallback, &axObserver) @@ -44,7 +48,7 @@ class Application: NSObject { override func observeValue(forKeyPath keyPath: String?, of object: Any?, change: [NSKeyValueChangeKey: Any]?, context: UnsafeMutableRawPointer?) { guard let isFinishedLaunching = change![.newKey], isFinishedLaunching as! Bool else { return } - runningApplication.removeObserver(self, forKeyPath: "isFinishedLaunching") + removeObserver() addAndObserveWindows() } diff --git a/alt-tab-macos/logic/Applications.swift b/alt-tab-macos/logic/Applications.swift index 90752981..9b39db89 100644 --- a/alt-tab-macos/logic/Applications.swift +++ b/alt-tab-macos/logic/Applications.swift @@ -30,13 +30,15 @@ class Applications { var someAppsAreAlreadyTerminated = false for runningApp in runningApps { guard runningApp.bundleIdentifier != nil else { someAppsAreAlreadyTerminated = true; continue } - guard Applications.map[runningApp.processIdentifier] != nil else { continue } + guard let app = Applications.map[runningApp.processIdentifier] else { continue } var windowsToKeep = [Window]() for window in Windows.listRecentlyUsedFirst { guard window.application.runningApplication.processIdentifier != runningApp.processIdentifier else { continue } windowsToKeep.append(window) } Windows.listRecentlyUsedFirst = windowsToKeep + // some apps never finish launching; the observer leaks for them without this + app.removeObserver() Applications.map.removeValue(forKey: runningApp.processIdentifier) guard Windows.listRecentlyUsedFirst.count > 0 else { (App.shared as! App).hideUi(); return } // TODO: implement of more sophisticated way to decide which thumbnail gets focused on app quit