I have a macOS app that runs only in the macOS status bar. I changed the "Application is agent (UIElement)" property in the Info.plist to "YES":
<key>LSUIElement</key>
<true/>
I have a timer that prints out the appearance's name every 5 seconds like this:
Timer.scheduledTimer(withTimeInterval: 5, repeats: true) { _ in
let appearance = NSAppearance.currentDrawing()
print(appearance.name)
}
The name doesn't actually change when I toggle dark/light mode in system settings. It always prints the name of the appearance that was set when the application launched.
Is there a way to listen to system appearance changes?
My end goal is actually to draw an NSAttributedString
to an NSImage
, and use that NSImage
as the NSStatusItem
button's image.
let image: NSImage = // generate image
statusItem.button?.image = image
For the text in the attributed string I use UIColor.labelColor
that is supposed to be based on the system appearance. However it seems to not respect the system appearance change.
When I start the application in Dark Mode and then switch to Light Mode:
When I start the application in Light Mode and then switch to Dark Mode:
The reason why I turn the NSAttributedString
into an NSImage
and don't use the NSAttributedString
directly on the NSStatusItem
button's attributedTitle
is because it doesn't position correctly in the status bar.
The problem with drawing a NSAttributedString
is, that NSAttributedString
doesn't know how to render dynamic colors such as NSColor.labelColor
. Thus, it doesn't react on appearance changes. You have to use a UI element.
I solved this problem by passing the NSAttributedString
to a NSTextField
and draw that into an NSImage
. Works perfectly fine.
func updateStatusItemImage() {
// Use UI element: `NSTextField`
let attributedString: NSAttributedString = ...
let textField = NSTextField(labelWithAttributedString: attributedString)
textField.sizeToFit()
// Draw the `NSTextField` into an `NSImage`
let size = textField.frame.size
let image = NSImage(size: size)
image.lockFocus()
textField.draw(textField.bounds)
image.unlockFocus()
// Assign the drawn image to the button of the `NSStatusItem`
statusItem.button?.image = image
}
In addition, since NSImage
doesn't know about NSAppearance
either I need to trigger a redraw on appearance changes by observing the effectiveAppearance
property of the button of the NSStatusItem
:
observation = statusItem.observe(\.button?.effectiveAppearance, options: []) { [weak self] _, _ in
// Redraw
self?.updateStatusItemImage()
}