From 33a85d686578584dd0da1e2ac5d6d63ef8322534 Mon Sep 17 00:00:00 2001 From: Saad Najmi Date: Wed, 26 Nov 2025 15:32:38 -0800 Subject: [PATCH 1/5] fix(fabric): borders and shadows render consistently --- .../View/RCTViewComponentView.mm | 63 ++++++++++++++++--- 1 file changed, 56 insertions(+), 7 deletions(-) diff --git a/packages/react-native/React/Fabric/Mounting/ComponentViews/View/RCTViewComponentView.mm b/packages/react-native/React/Fabric/Mounting/ComponentViews/View/RCTViewComponentView.mm index 0086fc4ce33536..0eb4aa7ece14d0 100644 --- a/packages/react-native/React/Fabric/Mounting/ComponentViews/View/RCTViewComponentView.mm +++ b/packages/react-native/React/Fabric/Mounting/ComponentViews/View/RCTViewComponentView.mm @@ -48,6 +48,11 @@ @implementation RCTViewComponentView { __weak CALayer *_borderLayer; CALayer *_outlineLayer; CALayer *_boxShadowLayer; +#if TARGET_OS_OSX // [macOS + UIImage *_boxShadowImage; // Strong reference to keep CGImage valid for layer.contents + UIImage *_borderImage; // Strong reference to keep CGImage valid for layer.contents + UIImage *_outlineImage; // Strong reference to keep CGImage valid for layer.contents +#endif // macOS] CALayer *_filterLayer; NSMutableArray *_backgroundImageLayers; BOOL _needsInvalidateLayer; @@ -832,16 +837,29 @@ static RCTCornerRadii RCTCreateOutlineCornerRadiiFromBorderRadii(const BorderRad } // To be used for CSS properties like `border` and `outline`. +// On macOS, outImage is set to the generated UIImage so the caller can keep a strong reference. static void RCTAddContourEffectToLayer( CALayer *layer, const RCTCornerRadii &cornerRadii, const RCTBorderColors &contourColors, const UIEdgeInsets &contourInsets, - const RCTBorderStyle &contourStyle) + const RCTBorderStyle &contourStyle +#if TARGET_OS_OSX + , UIImage * __strong *outImage // [macOS] +#endif + ) { UIImage *image = RCTGetBorderImage( contourStyle, layer.bounds.size, cornerRadii, contourInsets, contourColors, [RCTUIColor clearColor], NO); // [macOS] +#if TARGET_OS_OSX // [macOS + // Return the image to the caller so they can keep a strong reference. + // This prevents the CGImage from being deallocated while the layer uses it. + if (outImage) { + *outImage = image; + } +#endif // macOS] + if (image == nil) { layer.contents = nil; } else { @@ -854,7 +872,14 @@ static void RCTAddContourEffectToLayer( layer.contents = (id)image.CGImage; layer.contentsScale = image.scale; #else // [macOS - layer.contents = (__bridge id) UIImageGetCGImageRef(image); + // Force the lazy NSImage to render by locking focus. + // NSImage created with imageWithSize:flipped:drawingHandler: is lazy - + // the drawing handler isn't called until the image is actually drawn. + // We need to force rendering before extracting the CGImage. + [image lockFocus]; + [image unlockFocus]; + + layer.contents = (__bridge id)UIImageGetCGImageRef(image); layer.contentsScale = UIImageGetScale(image); #endif // macOS] @@ -1050,8 +1075,10 @@ - (void)invalidateLayer } #if TARGET_OS_OSX // [macOS - // clipsToBounds is stubbed out on macOS because it's not part of NSView - layer.masksToBounds = self.clipsToBounds; + // clipsToBounds is stubbed out on macOS because it's not part of NSView. + // Only set masksToBounds if there's no boxShadow - otherwise the shadow + // sublayer (which extends beyond bounds) would be clipped. + layer.masksToBounds = _props->boxShadow.empty() && self.clipsToBounds; #endif // macOS] const auto borderMetrics = _props->resolveBorderMetrics(_layoutMetrics); @@ -1171,6 +1198,9 @@ - (void)invalidateLayer if (useCoreAnimationBorderRendering) { [_borderLayer removeFromSuperlayer]; _borderLayer = nil; +#if TARGET_OS_OSX + _borderImage = nil; // [macOS] Clear image reference when not using custom border layer +#endif layer.borderWidth = (CGFloat)borderMetrics.borderWidths.left; RCTUIColor *borderColor = RCTUIColorFromSharedColor(borderMetrics.borderColors.left); // [macOS] @@ -1184,11 +1214,13 @@ - (void)invalidateLayer if (!_borderLayer) { CALayer *borderLayer = [CALayer new]; borderLayer.zPosition = BACKGROUND_COLOR_ZPOSITION + 1; - borderLayer.frame = layer.bounds; borderLayer.magnificationFilter = kCAFilterNearest; [layer addSublayer:borderLayer]; _borderLayer = borderLayer; } + + // Always update frame in case view resized + _borderLayer.frame = layer.bounds; layer.borderWidth = 0; layer.borderColor = nil; @@ -1201,12 +1233,19 @@ - (void)invalidateLayer RCTCornerRadiiFromBorderRadii(borderMetrics.borderRadii), borderColors, RCTUIEdgeInsetsFromEdgeInsets(borderMetrics.borderWidths), - RCTBorderStyleFromBorderStyle(borderMetrics.borderStyles.left)); + RCTBorderStyleFromBorderStyle(borderMetrics.borderStyles.left) +#if TARGET_OS_OSX + , &_borderImage // [macOS] Keep strong reference to prevent CGImage deallocation +#endif + ); } // outline [_outlineLayer removeFromSuperlayer]; _outlineLayer = nil; +#if TARGET_OS_OSX + _outlineImage = nil; // [macOS] Clear image reference when outline is removed +#endif if (_props->outlineWidth != 0) { if (!_outlineLayer) { CALayer *outlineLayer = [CALayer new]; @@ -1231,7 +1270,11 @@ - (void)invalidateLayer RCTCreateOutlineCornerRadiiFromBorderRadii(borderMetrics.borderRadii, _props->outlineWidth), RCTBorderColors{outlineColor, outlineColor, outlineColor, outlineColor}, UIEdgeInsets{_props->outlineWidth, _props->outlineWidth, _props->outlineWidth, _props->outlineWidth}, - RCTBorderStyleFromOutlineStyle(_props->outlineStyle)); + RCTBorderStyleFromOutlineStyle(_props->outlineStyle) +#if TARGET_OS_OSX + , &_outlineImage // [macOS] Keep strong reference to prevent CGImage deallocation +#endif + ); } } @@ -1286,6 +1329,9 @@ - (void)invalidateLayer // box shadow [_boxShadowLayer removeFromSuperlayer]; _boxShadowLayer = nil; +#if TARGET_OS_OSX // [macOS + _boxShadowImage = nil; // Release previous image +#endif // macOS] if (!_props->boxShadow.empty()) { _boxShadowLayer = [CALayer layer]; [self.layer addSublayer:_boxShadowLayer]; @@ -1301,6 +1347,9 @@ - (void)invalidateLayer #if !TARGET_OS_OSX // [macOS] _boxShadowLayer.contents = (id)boxShadowImage.CGImage; #else // [macOS + // Keep a strong reference to the NSImage so that the CGImage it provides + // (via UIImageGetCGImageRef) remains valid while the layer uses it. + _boxShadowImage = boxShadowImage; _boxShadowLayer.contents = (__bridge id)UIImageGetCGImageRef(boxShadowImage); #endif // macOS] } From 287dfa4587fdec265ef2851d733767dc844f2258 Mon Sep 17 00:00:00 2001 From: Saad Najmi Date: Wed, 26 Nov 2025 15:39:39 -0800 Subject: [PATCH 2/5] fixup ifdefs --- .../Mounting/ComponentViews/View/RCTViewComponentView.mm | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/packages/react-native/React/Fabric/Mounting/ComponentViews/View/RCTViewComponentView.mm b/packages/react-native/React/Fabric/Mounting/ComponentViews/View/RCTViewComponentView.mm index 0eb4aa7ece14d0..6683d77b65d7b8 100644 --- a/packages/react-native/React/Fabric/Mounting/ComponentViews/View/RCTViewComponentView.mm +++ b/packages/react-native/React/Fabric/Mounting/ComponentViews/View/RCTViewComponentView.mm @@ -1214,13 +1214,15 @@ - (void)invalidateLayer if (!_borderLayer) { CALayer *borderLayer = [CALayer new]; borderLayer.zPosition = BACKGROUND_COLOR_ZPOSITION + 1; + borderLayer.frame = layer.bounds; borderLayer.magnificationFilter = kCAFilterNearest; [layer addSublayer:borderLayer]; _borderLayer = borderLayer; } - - // Always update frame in case view resized +#if TARGET_OS_OSX // [macOS + // Update frame on every call in case view was resized _borderLayer.frame = layer.bounds; +#endif // macOS] layer.borderWidth = 0; layer.borderColor = nil; From 03b74f1e930df10bd4b5ceaeaa5d04d09e232920 Mon Sep 17 00:00:00 2001 From: Saad Najmi Date: Wed, 26 Nov 2025 16:39:57 -0800 Subject: [PATCH 3/5] more fixes --- .../View/RCTViewComponentView.mm | 31 +++++++++++++++---- 1 file changed, 25 insertions(+), 6 deletions(-) diff --git a/packages/react-native/React/Fabric/Mounting/ComponentViews/View/RCTViewComponentView.mm b/packages/react-native/React/Fabric/Mounting/ComponentViews/View/RCTViewComponentView.mm index 6683d77b65d7b8..9255c482cfc1cf 100644 --- a/packages/react-native/React/Fabric/Mounting/ComponentViews/View/RCTViewComponentView.mm +++ b/packages/react-native/React/Fabric/Mounting/ComponentViews/View/RCTViewComponentView.mm @@ -1040,10 +1040,20 @@ - (RCTUIView *)currentContainerView // [macOS] if (_useCustomContainerView) { if (!_containerView) { _containerView = [[RCTPlatformView alloc] initWithFrame:CGRectMake(0, 0, self.frame.size.width, self.frame.size.height)]; // [macOS] +#if TARGET_OS_OSX // [macOS + _containerView.wantsLayer = YES; +#endif // macOS] for (RCTPlatformView *subview in self.subviews) { // [macOS] [_containerView addSubview:subview]; } +#if !TARGET_OS_OSX // [macOS] _containerView.clipsToBounds = self.clipsToBounds; +#else // [macOS + // On macOS, clipsToBounds doesn't automatically set layer.masksToBounds + // like it does on iOS, so we need to set it directly. + _containerView.clipsToBounds = _props->getClipsContentToBounds(); + _containerView.layer.masksToBounds = _props->getClipsContentToBounds(); +#endif // macOS] self.clipsToBounds = NO; _containerView.layer.mask = self.layer.mask; self.layer.mask = nil; @@ -1075,10 +1085,15 @@ - (void)invalidateLayer } #if TARGET_OS_OSX // [macOS - // clipsToBounds is stubbed out on macOS because it's not part of NSView. - // Only set masksToBounds if there's no boxShadow - otherwise the shadow - // sublayer (which extends beyond bounds) would be clipped. - layer.masksToBounds = _props->boxShadow.empty() && self.clipsToBounds; + // On macOS, clipsToBounds doesn't automatically set layer.masksToBounds like iOS does. + // When _useCustomContainerView is true (boxShadow + overflow:hidden), the container + // view handles clipping children while the main layer stays unclipped for the shadow. + // The container view's masksToBounds is set in currentContainerView getter. + if (_useCustomContainerView) { + layer.masksToBounds = NO; + } else { + layer.masksToBounds = _props->getClipsContentToBounds(); + } #endif // macOS] const auto borderMetrics = _props->resolveBorderMetrics(_layoutMetrics); @@ -1337,7 +1352,8 @@ - (void)invalidateLayer if (!_props->boxShadow.empty()) { _boxShadowLayer = [CALayer layer]; [self.layer addSublayer:_boxShadowLayer]; - _boxShadowLayer.zPosition = _borderLayer.zPosition; + // Box shadow should be behind all content but still visible + _boxShadowLayer.zPosition = BACKGROUND_COLOR_ZPOSITION - 1; _boxShadowLayer.frame = RCTGetBoundingRect(_props->boxShadow, self.layer.bounds.size); UIImage *boxShadowImage = RCTGetBoxShadowImage( @@ -1351,8 +1367,11 @@ - (void)invalidateLayer #else // [macOS // Keep a strong reference to the NSImage so that the CGImage it provides // (via UIImageGetCGImageRef) remains valid while the layer uses it. + // The image is lazy - force it to render before extracting CGImage. _boxShadowImage = boxShadowImage; - _boxShadowLayer.contents = (__bridge id)UIImageGetCGImageRef(boxShadowImage); + [_boxShadowImage lockFocus]; + [_boxShadowImage unlockFocus]; + _boxShadowLayer.contents = (__bridge id)UIImageGetCGImageRef(_boxShadowImage); #endif // macOS] } From 74370657080f0016b0306bffa03587e1fc9b847d Mon Sep 17 00:00:00 2001 From: Saad Najmi Date: Wed, 26 Nov 2025 17:13:30 -0800 Subject: [PATCH 4/5] move lock focus / unlock focus to RCTUIKit --- packages/react-native/React/Base/macOS/RCTUIKit.m | 8 ++++++-- .../ComponentViews/View/RCTViewComponentView.mm | 10 ---------- 2 files changed, 6 insertions(+), 12 deletions(-) diff --git a/packages/react-native/React/Base/macOS/RCTUIKit.m b/packages/react-native/React/Base/macOS/RCTUIKit.m index 5ab447832ed5a9..77a846cf26530c 100644 --- a/packages/react-native/React/Base/macOS/RCTUIKit.m +++ b/packages/react-native/React/Base/macOS/RCTUIKit.m @@ -826,11 +826,9 @@ - (nonnull instancetype)initWithSize:(CGSize)size format:(nonnull RCTUIGraphicsI } - (nonnull NSImage *)imageWithActions:(NS_NOESCAPE RCTUIGraphicsImageDrawingActions)actions { - NSImage *image = [NSImage imageWithSize:_size flipped:YES drawingHandler:^BOOL(NSRect dstRect) { - RCTUIGraphicsImageRendererContext *context = [NSGraphicsContext currentContext]; if (self->_format.opaque) { CGContextSetAlpha([context CGContext], 1.0); @@ -838,6 +836,12 @@ - (nonnull NSImage *)imageWithActions:(NS_NOESCAPE RCTUIGraphicsImageDrawingActi actions(context); return YES; }]; + + // Force the image to render immediately by locking focus. + // This creates the backing store and makes CGImageForProposedRect work reliably. + [image lockFocus]; + [image unlockFocus]; + return image; } diff --git a/packages/react-native/React/Fabric/Mounting/ComponentViews/View/RCTViewComponentView.mm b/packages/react-native/React/Fabric/Mounting/ComponentViews/View/RCTViewComponentView.mm index 9255c482cfc1cf..f67704036d08e6 100644 --- a/packages/react-native/React/Fabric/Mounting/ComponentViews/View/RCTViewComponentView.mm +++ b/packages/react-native/React/Fabric/Mounting/ComponentViews/View/RCTViewComponentView.mm @@ -872,13 +872,6 @@ static void RCTAddContourEffectToLayer( layer.contents = (id)image.CGImage; layer.contentsScale = image.scale; #else // [macOS - // Force the lazy NSImage to render by locking focus. - // NSImage created with imageWithSize:flipped:drawingHandler: is lazy - - // the drawing handler isn't called until the image is actually drawn. - // We need to force rendering before extracting the CGImage. - [image lockFocus]; - [image unlockFocus]; - layer.contents = (__bridge id)UIImageGetCGImageRef(image); layer.contentsScale = UIImageGetScale(image); #endif // macOS] @@ -1367,10 +1360,7 @@ - (void)invalidateLayer #else // [macOS // Keep a strong reference to the NSImage so that the CGImage it provides // (via UIImageGetCGImageRef) remains valid while the layer uses it. - // The image is lazy - force it to render before extracting CGImage. _boxShadowImage = boxShadowImage; - [_boxShadowImage lockFocus]; - [_boxShadowImage unlockFocus]; _boxShadowLayer.contents = (__bridge id)UIImageGetCGImageRef(_boxShadowImage); #endif // macOS] } From 86e0f9f6c305ecac8d1ab3568fe08202444e5b0d Mon Sep 17 00:00:00 2001 From: Saad Najmi Date: Wed, 26 Nov 2025 17:59:35 -0800 Subject: [PATCH 5/5] Implement RCTUIImage --- packages/react-native/React/Base/RCTUIKit.h | 30 ++++-- .../react-native/React/Base/macOS/RCTUIKit.m | 92 +++++++++++++++---- .../View/RCTViewComponentView.mm | 51 ++-------- 3 files changed, 102 insertions(+), 71 deletions(-) diff --git a/packages/react-native/React/Base/RCTUIKit.h b/packages/react-native/React/Base/RCTUIKit.h index d01c4029f8b371..edffb66e4aefd3 100644 --- a/packages/react-native/React/Base/RCTUIKit.h +++ b/packages/react-native/React/Base/RCTUIKit.h @@ -264,7 +264,6 @@ extern "C" { // UIGraphics.h CGContextRef UIGraphicsGetCurrentContext(void); -CGImageRef UIImageGetCGImageRef(NSImage *image); #ifdef __cplusplus } @@ -330,7 +329,14 @@ NS_INLINE NSEdgeInsets UIEdgeInsetsMake(CGFloat top, CGFloat left, CGFloat botto #define UIApplication NSApplication // UIImage -@compatibility_alias UIImage NSImage; +// RCTUIImage is a subclass of NSImage that caches its CGImage representation. +// This is needed because NSImage's CGImageForProposedRect: returns a new autoreleased +// CGImage each time, which causes issues when used with CALayer.contents. +@interface RCTUIImage : NSImage +@property (nonatomic, readonly, nullable) CGImageRef CGImage; +@end + +@compatibility_alias UIImage RCTUIImage; typedef NS_ENUM(NSInteger, UIImageRenderingMode) { UIImageRenderingModeAlwaysOriginal, @@ -338,20 +344,24 @@ typedef NS_ENUM(NSInteger, UIImageRenderingMode) { }; #ifdef __cplusplus -extern "C" +extern "C" { #endif -CGFloat UIImageGetScale(NSImage *image); -CGImageRef UIImageGetCGImageRef(NSImage *image); +CGFloat UIImageGetScale(RCTUIImage *image); +CGImageRef UIImageGetCGImageRef(RCTUIImage *image); + +#ifdef __cplusplus +} +#endif -NS_INLINE UIImage *UIImageWithContentsOfFile(NSString *filePath) +NS_INLINE RCTUIImage *UIImageWithContentsOfFile(NSString *filePath) { - return [[NSImage alloc] initWithContentsOfFile:filePath]; + return [[RCTUIImage alloc] initWithContentsOfFile:filePath]; } -NS_INLINE UIImage *UIImageWithData(NSData *imageData) +NS_INLINE RCTUIImage *UIImageWithData(NSData *imageData) { - return [[NSImage alloc] initWithData:imageData]; + return [[RCTUIImage alloc] initWithData:imageData]; } NSData *UIImagePNGRepresentation(NSImage *image); @@ -624,7 +634,7 @@ typedef void (^RCTUIGraphicsImageDrawingActions)(RCTUIGraphicsImageRendererConte - (instancetype)initWithSize:(CGSize)size; - (instancetype)initWithSize:(CGSize)size format:(RCTUIGraphicsImageRendererFormat *)format; -- (NSImage *)imageWithActions:(NS_NOESCAPE RCTUIGraphicsImageDrawingActions)actions; +- (RCTUIImage *)imageWithActions:(NS_NOESCAPE RCTUIGraphicsImageDrawingActions)actions; @end NS_ASSUME_NONNULL_END diff --git a/packages/react-native/React/Base/macOS/RCTUIKit.m b/packages/react-native/React/Base/macOS/RCTUIKit.m index 77a846cf26530c..79351305dbf064 100644 --- a/packages/react-native/React/Base/macOS/RCTUIKit.m +++ b/packages/react-native/React/Base/macOS/RCTUIKit.m @@ -57,7 +57,7 @@ CGContextRef UIGraphicsGetCurrentContext(void) // UIImage -CGFloat UIImageGetScale(NSImage *image) +CGFloat UIImageGetScale(RCTUIImage *image) { if (image == nil) { return 0.0; @@ -76,9 +76,33 @@ CGFloat UIImageGetScale(NSImage *image) return 1.0; } -CGImageRef __nullable UIImageGetCGImageRef(NSImage *image) +// RCTUIImage - NSImage subclass with cached CGImage + +@implementation RCTUIImage { + CGImageRef _cachedCGImage; +} + +- (void)dealloc { + if (_cachedCGImage != NULL) { + CGImageRelease(_cachedCGImage); + } +} + +- (CGImageRef)CGImage { + if (_cachedCGImage == NULL) { + CGImageRef cgImage = [self CGImageForProposedRect:NULL context:NULL hints:NULL]; + if (cgImage != NULL) { + _cachedCGImage = CGImageRetain(cgImage); + } + } + return _cachedCGImage; +} + +@end + +CGImageRef __nullable UIImageGetCGImageRef(RCTUIImage *image) { - return [image CGImageForProposedRect:NULL context:NULL hints:NULL]; + return image.CGImage; } static NSData *NSImageDataForFileType(NSImage *image, NSBitmapImageFileType fileType, NSDictionary *properties) @@ -825,22 +849,54 @@ - (nonnull instancetype)initWithSize:(CGSize)size format:(nonnull RCTUIGraphicsI return self; } -- (nonnull NSImage *)imageWithActions:(NS_NOESCAPE RCTUIGraphicsImageDrawingActions)actions { - NSImage *image = [NSImage imageWithSize:_size - flipped:YES - drawingHandler:^BOOL(NSRect dstRect) { - RCTUIGraphicsImageRendererContext *context = [NSGraphicsContext currentContext]; - if (self->_format.opaque) { - CGContextSetAlpha([context CGContext], 1.0); - } - actions(context); - return YES; - }]; +- (nonnull RCTUIImage *)imageWithActions:(NS_NOESCAPE RCTUIGraphicsImageDrawingActions)actions { + // Create an RCTUIImage which caches its CGImage for efficient layer.contents usage. + // We draw into a bitmap context and create the image from that. + + CGFloat scale = _format.scale > 0 ? _format.scale : [[NSScreen mainScreen] backingScaleFactor]; + NSInteger pixelWidth = (NSInteger)(_size.width * scale); + NSInteger pixelHeight = (NSInteger)(_size.height * scale); + + if (pixelWidth <= 0 || pixelHeight <= 0) { + return [[RCTUIImage alloc] initWithSize:_size]; + } + + // Create a bitmap context + NSBitmapImageRep *bitmapRep = [[NSBitmapImageRep alloc] + initWithBitmapDataPlanes:NULL + pixelsWide:pixelWidth + pixelsHigh:pixelHeight + bitsPerSample:8 + samplesPerPixel:4 + hasAlpha:YES + isPlanar:NO + colorSpaceName:NSCalibratedRGBColorSpace + bytesPerRow:0 + bitsPerPixel:0]; + + bitmapRep.size = _size; + + NSGraphicsContext *context = [NSGraphicsContext graphicsContextWithBitmapImageRep:bitmapRep]; + [NSGraphicsContext saveGraphicsState]; + [NSGraphicsContext setCurrentContext:context]; + + // Flip the context to match iOS coordinate system (origin at top-left) + CGContextRef cgContext = [context CGContext]; + CGContextTranslateCTM(cgContext, 0, _size.height); + CGContextScaleCTM(cgContext, 1.0, -1.0); + + if (_format.opaque) { + CGContextSetAlpha(cgContext, 1.0); + } + + // Execute the drawing actions + actions(context); + + [NSGraphicsContext restoreGraphicsState]; - // Force the image to render immediately by locking focus. - // This creates the backing store and makes CGImageForProposedRect work reliably. - [image lockFocus]; - [image unlockFocus]; + // Create an RCTUIImage from the bitmap representation + RCTUIImage *image = [[RCTUIImage alloc] initWithSize:_size]; + [image addRepresentation:bitmapRep]; return image; } diff --git a/packages/react-native/React/Fabric/Mounting/ComponentViews/View/RCTViewComponentView.mm b/packages/react-native/React/Fabric/Mounting/ComponentViews/View/RCTViewComponentView.mm index f67704036d08e6..17387e7aeef598 100644 --- a/packages/react-native/React/Fabric/Mounting/ComponentViews/View/RCTViewComponentView.mm +++ b/packages/react-native/React/Fabric/Mounting/ComponentViews/View/RCTViewComponentView.mm @@ -48,11 +48,6 @@ @implementation RCTViewComponentView { __weak CALayer *_borderLayer; CALayer *_outlineLayer; CALayer *_boxShadowLayer; -#if TARGET_OS_OSX // [macOS - UIImage *_boxShadowImage; // Strong reference to keep CGImage valid for layer.contents - UIImage *_borderImage; // Strong reference to keep CGImage valid for layer.contents - UIImage *_outlineImage; // Strong reference to keep CGImage valid for layer.contents -#endif // macOS] CALayer *_filterLayer; NSMutableArray *_backgroundImageLayers; BOOL _needsInvalidateLayer; @@ -837,29 +832,16 @@ static RCTCornerRadii RCTCreateOutlineCornerRadiiFromBorderRadii(const BorderRad } // To be used for CSS properties like `border` and `outline`. -// On macOS, outImage is set to the generated UIImage so the caller can keep a strong reference. static void RCTAddContourEffectToLayer( CALayer *layer, const RCTCornerRadii &cornerRadii, const RCTBorderColors &contourColors, const UIEdgeInsets &contourInsets, - const RCTBorderStyle &contourStyle -#if TARGET_OS_OSX - , UIImage * __strong *outImage // [macOS] -#endif - ) + const RCTBorderStyle &contourStyle) { UIImage *image = RCTGetBorderImage( contourStyle, layer.bounds.size, cornerRadii, contourInsets, contourColors, [RCTUIColor clearColor], NO); // [macOS] -#if TARGET_OS_OSX // [macOS - // Return the image to the caller so they can keep a strong reference. - // This prevents the CGImage from being deallocated while the layer uses it. - if (outImage) { - *outImage = image; - } -#endif // macOS] - if (image == nil) { layer.contents = nil; } else { @@ -872,7 +854,9 @@ static void RCTAddContourEffectToLayer( layer.contents = (id)image.CGImage; layer.contentsScale = image.scale; #else // [macOS - layer.contents = (__bridge id)UIImageGetCGImageRef(image); + // RCTUIImage caches its CGImage, so it stays valid as long as the image is alive. + // The image is retained by the layer.contents assignment. + layer.contents = (__bridge id)image.CGImage; layer.contentsScale = UIImageGetScale(image); #endif // macOS] @@ -1206,9 +1190,6 @@ - (void)invalidateLayer if (useCoreAnimationBorderRendering) { [_borderLayer removeFromSuperlayer]; _borderLayer = nil; -#if TARGET_OS_OSX - _borderImage = nil; // [macOS] Clear image reference when not using custom border layer -#endif layer.borderWidth = (CGFloat)borderMetrics.borderWidths.left; RCTUIColor *borderColor = RCTUIColorFromSharedColor(borderMetrics.borderColors.left); // [macOS] @@ -1243,19 +1224,12 @@ - (void)invalidateLayer RCTCornerRadiiFromBorderRadii(borderMetrics.borderRadii), borderColors, RCTUIEdgeInsetsFromEdgeInsets(borderMetrics.borderWidths), - RCTBorderStyleFromBorderStyle(borderMetrics.borderStyles.left) -#if TARGET_OS_OSX - , &_borderImage // [macOS] Keep strong reference to prevent CGImage deallocation -#endif - ); + RCTBorderStyleFromBorderStyle(borderMetrics.borderStyles.left)); } // outline [_outlineLayer removeFromSuperlayer]; _outlineLayer = nil; -#if TARGET_OS_OSX - _outlineImage = nil; // [macOS] Clear image reference when outline is removed -#endif if (_props->outlineWidth != 0) { if (!_outlineLayer) { CALayer *outlineLayer = [CALayer new]; @@ -1280,11 +1254,7 @@ - (void)invalidateLayer RCTCreateOutlineCornerRadiiFromBorderRadii(borderMetrics.borderRadii, _props->outlineWidth), RCTBorderColors{outlineColor, outlineColor, outlineColor, outlineColor}, UIEdgeInsets{_props->outlineWidth, _props->outlineWidth, _props->outlineWidth, _props->outlineWidth}, - RCTBorderStyleFromOutlineStyle(_props->outlineStyle) -#if TARGET_OS_OSX - , &_outlineImage // [macOS] Keep strong reference to prevent CGImage deallocation -#endif - ); + RCTBorderStyleFromOutlineStyle(_props->outlineStyle)); } } @@ -1339,9 +1309,6 @@ - (void)invalidateLayer // box shadow [_boxShadowLayer removeFromSuperlayer]; _boxShadowLayer = nil; -#if TARGET_OS_OSX // [macOS - _boxShadowImage = nil; // Release previous image -#endif // macOS] if (!_props->boxShadow.empty()) { _boxShadowLayer = [CALayer layer]; [self.layer addSublayer:_boxShadowLayer]; @@ -1358,10 +1325,8 @@ - (void)invalidateLayer #if !TARGET_OS_OSX // [macOS] _boxShadowLayer.contents = (id)boxShadowImage.CGImage; #else // [macOS - // Keep a strong reference to the NSImage so that the CGImage it provides - // (via UIImageGetCGImageRef) remains valid while the layer uses it. - _boxShadowImage = boxShadowImage; - _boxShadowLayer.contents = (__bridge id)UIImageGetCGImageRef(_boxShadowImage); + // RCTUIImage caches its CGImage, so it stays valid as long as the image is alive. + _boxShadowLayer.contents = (__bridge id)boxShadowImage.CGImage; #endif // macOS] }