I have an NSView
whose layer
has various sublayers
.
Its needsDisplay
property is set to YES
when the app changes its effective appearance, which ends up calling:
-(void)updateLayer {
// this is a simplified version
sublayer1.backgroundColor = NSColor.windowBackgroundColor.CGColor;
sublayer2.content = [NSImage imageNamed:@"dynamic image"];
// "dynamic image" is an image asset that has light and dark variants.
}
Both layers render properly according to the theme after the app launches, but when the app changes theme (without being relaunched), the appearance of sublayer2
does not change. The appearance of sublayer1
does change, but I have to relaunch the app for sublayer2
to update to the app theme.
Using "dynamic image" as template image or original image has no effect on this issue.
Note that the same image used in an NSButton
does change appearance properly without having to relaunch the app.
Any suggestion?
From Apple's docs for CALayer
.contents
:
Discussion
The default value of this property is nil.
If you are using the layer to display a static image, you can set this property to the CGImageRef containing the image you want to display. (In macOS 10.6 and later, you can also set the property to an NSImage object.) Assigning a value to this property causes the layer to use your image rather than create a separate backing store.
If the layer object is tied to a view object, you should avoid setting the contents of this property directly. The interplay between views and layers usually results in the view replacing the contents of this property during a subsequent update.
So, when setting:
sublayer2.content = [NSImage imageNamed:@"dynamic image"];
the layer gets a CGImageRef
, not a NSImage
... and appearance changes will have no effect.
One approach would be to replace the CALayer
with a NSImageView
as a subview. Set the .image
property, and you're done.
If you need to use CALayer
, we need to get the CGImageRef
from the image with the current appearance in updateLayer
.
Quick example:
LayeredView.h
#import <Cocoa/Cocoa.h>
NS_ASSUME_NONNULL_BEGIN
@interface LayeredView : NSView
@end
NS_ASSUME_NONNULL_END
LayeredView.m
#import "LayeredView.h"
#import <Quartz/Quartz.h>
@interface LayeredView()
{
CALayer *imgLayer;
NSImage *img;
}
@end
@implementation LayeredView
- (id)initWithFrame:(NSRect)frame {
NSLog(@"%s", __PRETTY_FUNCTION__);
self = [super initWithFrame:frame];
if (self) {
[self commonInit];
}
return self;
}
- (instancetype)initWithCoder:(NSCoder *)coder {
NSLog(@"%s", __PRETTY_FUNCTION__);
self = [super initWithCoder:coder];
if (self) {
[self commonInit];
}
return self;
}
- (void)commonInit {
NSLog(@"%s", __PRETTY_FUNCTION__);
[self setWantsLayer:YES];
// load the image
img = [NSImage imageNamed:@"i100x100"];
// create layer
imgLayer = [CALayer new];
// .contentsGravity as desired
imgLayer.contentsGravity = kCAGravityCenter;
// add the layer
[self.layer addSublayer:imgLayer];
}
- (void)updateLayer {
NSLog(@"%s", __PRETTY_FUNCTION__);
[super updateLayer];
// built-in layer animation will result in a "fade"
// so disable it (if desired)
[CATransaction begin];
[CATransaction setValue:(id)kCFBooleanTrue forKey:kCATransactionDisableActions];
if (img) {
// get the CGImageRef from img -- this will reflect light/dark mode
imgLayer.contents = (__bridge id _Nullable)([img CGImageForProposedRect:nil context:nil hints:nil]);
}
// image layer frame as desired
imgLayer.frame = self.bounds;
[CATransaction commit];
}
@end