Updating programmatically set colors on Mode change (dark mode, light mode) on macOS (objective-c)

1.6k views Asked by At

i am on macOS, objective-c, not iOS. XCode 12.

In a lot of views i set colors like this:

self.menuIconBar.wantsLayer = YES;
self.menuIconBar.layer.backgroundColor = [NSColor colorNamed:@"color_gradient_right"].CGColor;

Whenever the user changes the Appeareance, e.g. to Dark mode, i expect my colors to change according to the Asset setup:

enter image description here

Unfortunately, nothing happens. BUT: The same color applied in IB directly changes as expected. Still i'd need them to change programmatically too.

Then i tried to hook on notifications:

[[NSDistributedNotificationCenter defaultCenter] addObserver:self selector:@selector(appleInterfaceThemeChangedNotification:) name:@"AppleInterfaceThemeChangedNotification" object:nil];

I receive the notifications, but when i then call the same code like above again, still the wrong color is loaded.

self.menuIconBar.layer.backgroundColor = [NSColor colorNamed:@"color_gradient_right"].CGColor;

Any help appreciated

3

There are 3 answers

2
Pat_Morita On BEST ANSWER

As of macOS 11 one should use the performAsCurrentDrawingAppearance: instance method and add anything to apply after an appearance change into the given block.

3
apodidae On

The following example will change the background color of a custom view depending on the Appearance setting in System Preferences. It may be run in Xcode by creating an objc project, deleting the pre-existing App Delegate, and replacing the code in 'main.m' with the code below:

#import <Cocoa/Cocoa.h>

@interface CustomView : NSView
@end

@implementation CustomView

- (id)initWithFrame:(NSRect)frameRect {
 if ((self = [super initWithFrame:frameRect]) != nil) {
 // Add initialization code here
 }
 return self;
}
 
- (void)drawRect:(NSRect)rect {
}

- (void)viewDidChangeEffectiveAppearance {
 NSLog (@"appearance did change.");
 NSAppearance *changedAppearance = NSApp.effectiveAppearance;
 NSAppearanceName newAppearance = [changedAppearance bestMatchFromAppearancesWithNames:@[NSAppearanceNameAqua, NSAppearanceNameDarkAqua]];
 NSLog (@"new appearance name = %@", newAppearance);
  if([newAppearance isEqualToString:NSAppearanceNameDarkAqua]){
    [[self layer] setBackgroundColor:CGColorCreateGenericRGB( 1.0, 0.0, 0.0, 1.0 )];
  } else {
    [[self layer] setBackgroundColor:CGColorCreateGenericRGB( 0.0, 0.0, 1.0, 1.0 )];
  }
}

 // Use this if you want 0,0 (origin) to be top, left
 // Otherwise origin will be at bottom, left (Unflipped)
-(BOOL)isFlipped {
  return YES;
}
@end
 
@interface AppDelegate : NSObject <NSApplicationDelegate> {
 NSWindow *window;
}

 - (void) buildMenu;
 - (void) buildWindow;
@end
 
@implementation AppDelegate
   
- (void) buildMenu {
 NSMenu *menubar = [NSMenu new];
 NSMenuItem *menuBarItem = [NSMenuItem new];
 [menubar addItem:menuBarItem];
 [NSApp setMainMenu:menubar];
 NSMenu *appMenu = [NSMenu new];
 NSMenuItem *quitMenuItem = [[NSMenuItem alloc] initWithTitle:@"Quit"
 action:@selector(terminate:) keyEquivalent:@"q"];
 [appMenu addItem:quitMenuItem];
 [menuBarItem setSubmenu:appMenu];
}
 
- (void) buildWindow {
 #define _wndW  600
 #define _wndH  550
 
 window = [[NSWindow alloc] initWithContentRect: NSMakeRect( 0, 0, _wndW, _wndH )
 styleMask: NSWindowStyleMaskTitled | NSWindowStyleMaskMiniaturizable | NSWindowStyleMaskClosable
 backing: NSBackingStoreBuffered defer: NO];
 
 [window center];
 [window setTitle: @"Test window"];
 [window makeKeyAndOrderFront: nil];
 
 // **** CustomView **** //
 CustomView *view = [[CustomView alloc]initWithFrame:NSMakeRect( 20, 60, _wndW - 40, _wndH - 80 )];
 [view setWantsLayer:YES];
 [[view layer] setBackgroundColor:CGColorCreateGenericRGB( 0.0, 0.0, 1.0, 1.0 )];
 [[window contentView] addSubview:view];
 
 // **** Quit btn **** //
 NSButton *quitBtn = [[NSButton alloc]initWithFrame:NSMakeRect( _wndW - 50, 10, 40, 40 )];
 [quitBtn setBezelStyle:NSBezelStyleCircular ];
 [quitBtn setTitle: @"Q" ];
 [quitBtn setAction:@selector(terminate:)];
 [[window contentView] addSubview: quitBtn];
}
 
- (void) applicationWillFinishLaunching: (NSNotification *)notification {
 [self buildMenu];
 [self buildWindow];
}
 
- (void) applicationDidFinishLaunching: (NSNotification *)notification {
}
@end
 
int main() {
 NSApplication *application = [NSApplication sharedApplication];
 AppDelegate *appDelegate = [[AppDelegate alloc] init];
 [application setDelegate:appDelegate];
 [application run];
 return 0;
}

0
Mojo66 On

This worked for me in my NSView subclass:

- (void)awakeFromNib {
    self.wantsLayer             = YES;// might be unnecessary
}

- (void)viewDidChangeEffectiveAppearance {
    self.needsDisplay           = YES;
}

- (void)updateLayer {
    self.layer.backgroundColor  = NSColor.unemphasizedSelectedContentBackgroundColor.CGColor;
}