If you absolutely need to change the style, I'd recommend a 3rd-party custom control that lets you customize the look as you desire; some viable suggestions can be found here. As of 10.15, NSSwitch
simply doesn't have the customization you desire. If you don't care about details, stop reading now
That said, it's interesting to dissect the components of NSSwitch
. Unlike many Cocoa controls, there is no backing NSCell
for this control. The documentation says this explicitly:
NSSwitch doesn’t use an instance of NSCell to provide its functionality. The cellClass class property and cell instance property both return nil, and they ignore attempts to set a non-nil value.
So instead, it is a rather modern control made up of layers that draw the content. There are 3 NSWidgetView
s which are private NSView
subclasses.

These views mostly utilize -[NSView updateLayer]
to draw into their backing CALayer desired values based on what they're fed via -[NSWidgetView setWidgetDefinition:]
. This method passes in a dictionary of values that defines how NSWidgetView
should draw into the layer. An example dictionary for the back-most view:
{
kCUIPresentationStateKey = kCUIPresentationStateInactive;
kCUIScaleKey = 2;
kCUIUserInterfaceLayoutDirectionKey = kCUIUserInterfaceLayoutDirectionLeftToRight;
size = regular;
state = normal;
value = 0;
widget = kCUIWidgetSwitchFill;
}
Unfortunately, this means that the style is mostly determined by predefined strings as seen by widget = kCUIWidgetSwitchFill;
It gets quite involved in how this fill color or disabled color is drawn depending on system color (dark/light) or highlight color. The colors are tied to NSAppearance and there isn't a clear way of overriding any of these.
One (not recommended, seriously don't do this) solution is to swizzle out the updateLayer
call for NSWidgetView and do your own additional layer customization in the cases that you desire. An example written in Swift:
/// Swizzle out NSWidgetView's updateLayer for our own implementation
class AppDelegate: NSObject, NSApplicationDelegate {
/// Swizzle out NSWidgetView's updateLayer for our own implementation
func applicationWillFinishLaunching(_ notification: Notification) {
let original = Selector("updateLayer")
let swizzle = Selector("xxx_updateLayer")
if let widgetClass = NSClassFromString("NSWidgetView"),
let originalMethod = class_getInstanceMethod(widgetClass, original),
let swizzleMethod = class_getInstanceMethod(NSView.self, swizzle) {
method_exchangeImplementations(originalMethod, swizzleMethod)
}
}
}
extension NSView {
@objc func xxx_updateLayer() {
// This calls the original implementation so all other NSWidgetViews will have the right look
self.xxx_updateLayer()
guard let dictionary = self.value(forKey: "widgetDefinition") as? [String: Any],
let widget = dictionary["widget"] as? String,
let value = (dictionary["value"] as? NSNumber)?.intValue else {
return
}
// If we're specifically dealing with this case, change the colors and remove the contents which are set in the enabled switch case
if widget == "kCUIWidgetSwitchFill" {
layer?.contents = nil;
if value == 0 {
layer?.backgroundColor = NSColor.red.cgColor;
} else {
layer?.backgroundColor = NSColor.yellow.cgColor;
}
}
}
}
Again, DON'T DO THIS! You're bound to break in the future, Apple will reject this in an App Store submission, and you're much more safe with a custom control that does exactly what you want without mucking with private API.