Update! This bug appears to be fixed in macOS Sonoma 14.5!
In the macOS AppKit framework, there’s an NSWorkspace
API method for setting the current desktop image, scaling method, and fill color. The method is called setDesktopImageURL
, and it accepts a file URL, a screen reference, and an options dictionary, including fillColor
.
Here’s some sample code:
SetDesktopImageURL.swift
import AppKit
import Foundation
let workspace = NSWorkspace.shared
let screens = NSScreen.screens
// transparent Apache logo, part of the base macOS installation
let path = "/Library/WebServer/share/httpd/manual/images/feather.png"
let image = NSURL.fileURL(withPath: path)
// A nice blue green. This will show through any transparent areas of the desktop image
let color = NSColor(calibratedRed: 64/255, green: 116/255, blue: 112/255, alpha: 1.0)
// NSImageScaling options https://developer.apple.com/documentation/appkit/nsimagescaling
let center = NSImageScaling.scaleNone.rawValue
let options: [NSWorkspace.DesktopImageOptionKey: Any] = [
.allowClipping: false,
.imageScaling: center,
.fillColor: color,
]
for screen in screens {
try workspace.setDesktopImageURL(image, for: screen, options: options)
}
We can read the values we’ve just set using a related API method, desktopImageOptions
. This method returns a dictionary of the current desktop image options for the given screen.
More sample code:
DesktopImageOptions.swift
import AppKit
import Foundation
let screen = NSScreen.main!
let options = NSWorkspace.shared.desktopImageOptions(for: screen)!
print("imageScaling:", options[.imageScaling] as Any)
print("allowClipping:", options[.allowClipping] as Any)
print("fillColor:", options[.fillColor] as Any)
This code works as expected in macOS Monterey (version 12.x) and macOS Ventura (version 13.x):
Monterey
macOS version: 12.4.0
imageScaling: Optional(3)
allowClipping: Optional(0)
fillColor: Optional(NSCalibratedRGBColorSpace ...) <-- good
Ventura
macOS version: 13.6.0
imageScaling: Optional(3)
allowClipping: Optional(0)
fillColor: Optional(NSCalibratedRGBColorSpace ...) <-- good
But in Sonoma (version 14.x), the fillColor
key is missing from the returned dictionary:
Sonoma
macOS version: 14.4.1
imageScaling: Optional(3)
allowClipping: Optional(0)
fillColor: nil <-- bad
Similarly, when calling setDesktopImageURL
, Sonoma seems to ignore the fillColor
option, and always sets the fill color to some default blue (even if I have previously set it to some other color manually.)
Keyboard Maestro
I also tested this in the popular macro utility Keyboard Maestro, which has a Set Desktop Image
action that allows you to set the desktop image, scaling method, and fill color, likely using the same setDesktopImageURL
API method. Keyboard Maestro is able to set the fill color in Monterey, but not in Sonoma.
So…
…at this point I’m pretty sure this is a regression in macOS Sonoma.
Next steps
If you have any experience with this API and know of a workaround, I’d love to hear it!
If you have Xcode installed, you can try out a minimal sample project on Github. Clone the repo and type
swift run
. It will output the current desktop image settings in your console. Or you can copy the code above and save it to a file, and run it withswift filename.swift
.I need to file a bug report with Apple, but I haven’t done that before and from what I’ve heard I’m not particularly hopeful that it will get me anywhere. But I’ll try anyway.
A cool thing I found along the way…
I discovered this amazing macOS virtualization tool, Tart.
It allows you to run different versions of macOS (and Linux) in virtual containers on your Mac. I used it to test the above code on Ventura, as I didn’t have a machine with that version running. The setup is dead-easy (if you already have homebrew
installed). It does take a while to download an OS image, but once that’s done the container starts up incredibly quickly, and feels just as responsive as my actual Mac. Definitely worth a look if you do any macOS development.