如何让带有NSImage的CALayer动态适配应用外观?

How to make a CALayer with NSImage dynamically adapt to the app appearance?

提问人:jeanlain 提问时间:7/28/2023 更新时间:8/2/2023 访问量:58

问:

我有一个谁有各种各样的.NSViewlayersublayers

当应用更改其有效外观时,其属性设置为,最终调用:needsDisplayYES

-(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.
}

应用程序启动后,这两个图层都会根据主题正确呈现,但是当应用程序更改主题(未重新启动)时,外观不会更改。的外观确实发生了变化,但我必须重新启动应用程序才能更新到应用程序主题。 使用“动态图像”作为模板图像或原始图像对此问题没有影响。sublayer2sublayer1sublayer2

请注意,中使用的相同图像确实会正确更改外观,而无需重新启动应用程序。NSButton

有什么建议吗?

Objective-C Calayer NSMior的

评论

0赞 Scott Thompson 7/29/2023
NSImage将根据图像的名称缓存图像。我怀疑当您尝试使用 重新获取图像时,它会返回缓存的图像,而不是重新搜索要显示的图像(并在您的资产目录中找到该图像)。在设置之前尝试正确操作,看看这是否会刷新缓存的映像并强制系统在资产目录中搜索该映像。imageNamed[(NSImage *)sublayer2.content setName: nil];sublayer2.content
0赞 DonMag 7/29/2023
然而,我对 MacOS 应用程序做的很少......在 iOS 上,我们将图层内容设置为不会随着外观变化而改变。似乎有可能(可能?)将 a 指定为图层的内容也做同样的事情(在引擎盖下)。添加 a 作为子视图是否适合您的用例?CGImageNSImageNSImageView
0赞 jeanlain 7/29/2023
@Scott:这个问题肯定与缓存有关,但您的建议没有奏效。我还尝试在删除和重新添加所有图像表示之前/之后调用图像,以及(这应该是多余的)......没有任何效果。我宁愿不使用 .我想我最终会在暗/亮模式下使用不同的图像,因为我没有想法。-recacheNSImageCacheNever-recacheNSImageView
0赞 jeanlain 7/29/2023
更新:当我调用它时(在方法中),可以正确适应外观,而无需重新缓存任何内容。如果我在 中使用它,我不会改变它的外观,即使我没有将其设置为 而是调用它的图像NSImage-drawInRect:-drawRect:CALayercontents-drawInRect:-drawLayer:InContext:
0赞 jeanlain 7/29/2023
更新:它似乎与缓存无关。如果我仅在应用程序更改外观才创建 和 对象,则该层将以应用程序启动时的外观呈现图像。似乎忽略了应用程序的当前外观并依赖于其初始状态。这是一个错误吗?CALayerNSImageCALayer

答:

1赞 DonMag 8/2/2023 #1

来自 Apple 的文档CALayercontents

讨论

此属性的默认值为 nil。

如果使用图层显示静态图像,则可以将此属性设置为包含要显示的图像的 CGImageRef。(在 macOS 10.6 及更高版本中,还可以将该属性设置为 NSImage 对象。为此属性分配值会导致图层使用您的图像,而不是创建单独的后备存储。

如果图层对象与视图对象相关联,则应避免直接设置此属性的内容。视图和图层之间的相互作用通常会导致视图在后续更新期间替换此属性的内容。

因此,在设置时:

sublayer2.content = [NSImage imageNamed:@"dynamic image"];

该层得到一个 ,而不是一个 ...外观变化将不起作用。CGImageRefNSImage

一种方法是将 替换为 a 作为子视图。设置属性,就完成了。CALayerNSImageView.image

如果需要使用 CALayer,我们需要从当前外观的图像中获取 。CGImageRefupdateLayer

简单示例:

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