1

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

Pat_Morita
  • 3,355
  • 3
  • 25
  • 36
  • Have you tried being notified using this method:- (void)viewDidChangeEffectiveAppearance; – apodidae Oct 24 '20 at 20:59
  • If you haven't seen it, this SO reference might be useful: https://stackoverflow.com/questions/51672124/how-can-dark-mode-be-detected-on-macos-10-14 – apodidae Oct 24 '20 at 21:22
  • Getting a notification isn't the issue. I want the app to take the dark appearance of the exact same asset color i have applied in code. See asset screenshot – Pat_Morita Oct 25 '20 at 16:34
  • Does using wantsUpdateLayer = YES; change anything? – apodidae Oct 25 '20 at 17:49
  • Im in the viewController, not in the view actually. I think this is an issue with CG color: A core graphics struct isn't an object like an NSColor and thus cannot dynamically be changed. So i was wondering how to achieve that – Pat_Morita Oct 25 '20 at 17:52
  • Have you found a solution in the meantime? I successfully used NSAppearance.currentAppearance to the new appearance value in the notification handler. That caused the CGColor to resolve correctly. However that setter has been deprecated since macOS 11 and I haven't found a working alternative yet. – tipa Dec 03 '22 at 09:20
  • @tipa use the performAsCurrentDrawingAppearance: instance method – Pat_Morita Dec 03 '22 at 16:28

3 Answers3

1

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;
}

apodidae
  • 1,988
  • 2
  • 5
  • 9
  • Thank you. +1 for the effort. But I don't want to change a color, a want the app to take the dark appearance of the exact same asset color i have applied in code. – Pat_Morita Oct 25 '20 at 16:33
  • @Pat_Morita: I have the same problem, were you able to solve it? – Paul Aug 12 '21 at 10:55
  • I solved it with this answer: https://stackoverflow.com/questions/56968587/nscolor-systemcolor-not-changing-when-dark-light-mode-switched The key was to change the current Appearance of the containing view, as they are independent of the system appearance. – Pat_Morita Aug 13 '21 at 06:59
0

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;
}
Mojo66
  • 1,109
  • 12
  • 21
0

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

Pat_Morita
  • 3,355
  • 3
  • 25
  • 36
  • That requires to use that block *everywhere* I access the CGColor, instead of just setting the global variable. Not what I hoped for... – tipa Dec 04 '22 at 18:23
  • Not only `CGColor` is affected, this method also needs to be called everywhere where `colorWithAlphaComponent` is used. Adds tons of extra code – tipa Dec 04 '22 at 19:31