Starting with macOS (10.16 "Beta"/11.0) "Big Sur", the menu-bar and system tray no longer honor the Desktop dark-mode preference, making it difficult to
I had the same issue but I think I found a solution. As written in AppKit Release Notes for Big Sur (see the entry for NSStatusItem
) you can just observe NSStatusItem
's button
's effectiveAppearance
. If the name of the effectiveAppearance
contains dark
, then it's the dark mode. Otherwise it's the light mode.
A sample code I created which shows light
or dark
as the text label of the NSStatusItem
is available at this GitHub repo, see in particular AppDelegate.m. (I'm sorry for being a near-extinct dinosaur for using Objective-C.) You can test it by running on Catalina or on Big Sur, changing the Dark/Light settings or the color of the desktop picture from System Preferences.
Edit: It turns out that Big Sur sometimes changes effectiveAppearance
from light to light or from dark to dark (in the sense that KVO is invoked although the appearance hasn’t actually changed.) Therefore it’s advisable to check both the value of effectiveApparance
before and after the change to confirm the value actually changed.
I just submitted a TSI and I got an answer:
But I would not add an NSView on top of the NSStatusItem’s button directly. The docs mention to use the button property to customize the appearance and behavior of the status item, but it should work within the confines of the button itself, that is, it’s various properties like the image and text and their placements. Years ago, NSStatusItem allowed for custom views, but then became deprecated, in order to support a button-based UI, therefore allowing its drawing behavior to easily adapt to changes in the menu bar’s appearance.
So unfortunately there is no way to get this information programatically. However, getting information is very important for three of my apps, so I went exploring.
For me personally, it was very important that getting this information will not trigger any security prompts.
I came up with the following idea:
NSStatusItem
NSImage
CALayer
into a NSImage
You can use this code to get the colour information (note: the NSStatusItem
is never visible and does not cause existing items to move or something like that). Feel free to adjust the formatting and classes:
I have created a class called MenuBar
with a public property:
public class MenuBar {
private static var statusItem: NSStatusItem?
public static var theme: MenuBarTheme {
if self.statusItem == nil {
self.statusItem = NSStatusBar.system.statusItem(withLength: NSStatusItem.variableLength)
self.statusItem?.button?.image = NSImage(systemSymbolName: "circle.fill", accessibilityDescription: nil)
self.statusItem?.isVisible = false
}
if let color = self.getPixelColor() {
return color.redComponent < 0.20 && color.blueComponent < 0.20 && color.greenComponent < 0.20 ? .light : .dark
}
else
{
return NSApplication.isDarkMode ? .dark : .light
}
}
public static var tintColor: NSColor {
return self.theme == .light ? NSColor.black : NSColor.white
}
// MARK: - Helper
fileprivate static func getPixelColor() -> NSColor?
{
if let image = self.statusItem?.button?.layer?.getBitmapImage() {
let imageRep = NSBitmapImageRep(data: image.tiffRepresentation!)
if let color = imageRep?.colorAt(x: Int(image.size.width / 2.0), y: Int(image.size.height / 2.0)) {
return color
}
}
return nil
}
}
public enum MenuBarTheme : String
{
case light = "light"
case dark = "dark"
}
public extension NSApplication
{
class var isDarkMode: Bool
{
return NSApplication.shared.appearance?.description.lowercased().contains("dark") ?? false
}
}
public extension CALayer
{
func getBitmapImage() -> NSImage
{
let btmpImgRep = NSBitmapImageRep(bitmapDataPlanes: nil, pixelsWide: Int(self.frame.width), pixelsHigh: Int(self.frame.height), bitsPerSample: 8, samplesPerPixel: 4, hasAlpha: true, isPlanar: false, colorSpaceName: .deviceRGB, bytesPerRow: 0, bitsPerPixel: 32)
let ctx = NSGraphicsContext(bitmapImageRep: btmpImgRep!)
let cgContext = ctx!.cgContext
self.render(in: cgContext)
let cgImage = cgContext.makeImage()
return NSImage(cgImage: cgImage!, size: CGSize(width: self.frame.width, height: self.frame.height))
}
}