Fix dock icon not auto-switching with system dark mode (#1928)

* Fix dock icon not auto-switching with system dark mode

The automatic icon mode relied on the asset catalog to handle
appearance-based icon selection, but the compiled Assets.car does not
include dark variant renditions for AppIcon. This meant setting
applicationIconImage to nil had no effect — the icon stayed on the
light variant regardless of system appearance.

Replace the nil-reset approach with an active KVO observer on
NSApp.effectiveAppearance that programmatically swaps between
AppIconDark and AppIconLight images when in automatic mode.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>

* Address review feedback: fix async race and harden singleton

- Add guard in async callback to skip stale updates after stopObserving()
- Add private init() to prevent external instantiation of singleton
- Remove unused .new KVO option

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>

---------

Co-authored-by: CHE-3 <schumannzheng@gmail.com>
Co-authored-by: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
This commit is contained in:
Matthew Z. 2026-03-25 01:19:49 -04:00 committed by GitHub
parent 960006e6d6
commit 4a8fd3d0fe
No known key found for this signature in database
GPG key ID: B5690EEEBB952194

View file

@ -3695,14 +3695,14 @@ enum AppIconSettings {
static func applyIcon(_ mode: AppIconMode) {
switch mode {
case .automatic:
// Let the asset catalog handle appearance-based icon selection (macOS 15+).
// Reset to the default bundle icon.
NSApplication.shared.applicationIconImage = nil
AppIconAppearanceObserver.shared.startObserving()
case .light:
AppIconAppearanceObserver.shared.stopObserving()
if let icon = NSImage(named: "AppIconLight") {
NSApplication.shared.applicationIconImage = icon
}
case .dark:
AppIconAppearanceObserver.shared.stopObserving()
if let icon = NSImage(named: "AppIconDark") {
NSApplication.shared.applicationIconImage = icon
}
@ -3710,6 +3710,37 @@ enum AppIconSettings {
}
}
final class AppIconAppearanceObserver: NSObject {
static let shared = AppIconAppearanceObserver()
private var observation: NSKeyValueObservation?
private override init() { super.init() }
func startObserving() {
applyIconForCurrentAppearance()
guard observation == nil else { return }
observation = NSApp.observe(\.effectiveAppearance, options: []) { [weak self] _, _ in
DispatchQueue.main.async {
guard let self, self.observation != nil else { return }
self.applyIconForCurrentAppearance()
}
}
}
func stopObserving() {
observation?.invalidate()
observation = nil
}
private func applyIconForCurrentAppearance() {
let isDark = NSApp.effectiveAppearance.bestMatch(from: [.darkAqua, .aqua]) == .darkAqua
let imageName = isDark ? "AppIconDark" : "AppIconLight"
if let icon = NSImage(named: imageName) {
NSApplication.shared.applicationIconImage = icon
}
}
}
enum QuitWarningSettings {
static let warnBeforeQuitKey = "warnBeforeQuitShortcut"
static let defaultWarnBeforeQuit = true