From 2dd5ec683dafa596851dfe8ae90edfc886f592d8 Mon Sep 17 00:00:00 2001 From: SzymczakJ Date: Tue, 21 Apr 2026 16:27:48 +0200 Subject: [PATCH 1/5] fix iOS new arch --- ios/RNLinearGradient.mm | 31 +++- ios/RNLinearGradientLayerNewArch.h | 16 ++ ios/RNLinearGradientLayerNewArch.m | 232 +++++++++++++++++++++++++++++ 3 files changed, 271 insertions(+), 8 deletions(-) create mode 100644 ios/RNLinearGradientLayerNewArch.h create mode 100644 ios/RNLinearGradientLayerNewArch.m diff --git a/ios/RNLinearGradient.mm b/ios/RNLinearGradient.mm index 509fc9bd..46640efc 100644 --- a/ios/RNLinearGradient.mm +++ b/ios/RNLinearGradient.mm @@ -8,7 +8,7 @@ #import #import -#import "RCTFabricComponentsPlugins.h" +#import #endif #import @@ -34,7 +34,11 @@ + (Class)layerClass - (RNLinearGradientLayer *)gradientLayer { - return (RNLinearGradientLayer *)self.layer; + #ifdef RCT_NEW_ARCH_ENABLED + return [RNLinearGradientLayerNewArch class]; + #else + return [RNLinearGradientLayer class]; + #endif } - (NSArray *)colors @@ -141,6 +145,15 @@ - (void)updateProps:(Props::Shared const &)props oldProps:(Props::Shared const & const auto &oldViewProps = *std::static_pointer_cast(_props); const auto &newViewProps = *std::static_pointer_cast(props); + if (newViewProps.yogaStyle.overflow() != oldViewProps.yogaStyle.overflow()) { + // 0 for visible, 1 for hidden + if ((int)newViewProps.yogaStyle.overflow() == 0) { + self.layer.masksToBounds = false; + } else { + self.layer.masksToBounds = true; + } + } + if(oldViewProps.startPoint.x != newViewProps.startPoint.x || oldViewProps.startPoint.y != newViewProps.startPoint.y) { self.startPoint = CGPointMake(newViewProps.startPoint.x, newViewProps.startPoint.y); } @@ -161,13 +174,15 @@ - (void)updateProps:(Props::Shared const &)props oldProps:(Props::Shared const & self.angleCenter = CGPointMake(newViewProps.angleCenter.x, newViewProps.angleCenter.y); } - NSArray *locations = convertCxxVectorNumberToNsArrayNumber(newViewProps.locations); - self.locations = locations; + if (oldViewProps.locations != newViewProps.locations) { + NSArray *locations = convertCxxVectorNumberToNsArrayNumber(newViewProps.locations); + self.locations = locations; + } - // We cannot compare SharedColor because it is shared value. - // We could compare color value, but it is more performant to just assign new value - NSArray *colors = convertCxxVectorColorsToNSArrayColors(newViewProps.colors); - self.colors = colors; + if (oldViewProps.colors != newViewProps.colors) { + NSArray *colors = convertCxxVectorColorsToNSArrayColors(newViewProps.colors); + self.colors = colors; + } [super updateProps:props oldProps:oldProps]; } diff --git a/ios/RNLinearGradientLayerNewArch.h b/ios/RNLinearGradientLayerNewArch.h new file mode 100644 index 00000000..616c3ccf --- /dev/null +++ b/ios/RNLinearGradientLayerNewArch.h @@ -0,0 +1,16 @@ +#import +#import + +@class UIColor; + +@interface RNLinearGradientLayerNewArch : CALayer + +@property(nullable, nonatomic, copy) NSArray *colors; +@property(nullable, nonatomic, copy) NSArray *locations; +@property(nonatomic) CGPoint startPoint; +@property(nonatomic) CGPoint endPoint; +@property(nonatomic) BOOL useAngle; +@property(nonatomic) CGPoint angleCenter; +@property(nonatomic) CGFloat angle; + +@end diff --git a/ios/RNLinearGradientLayerNewArch.m b/ios/RNLinearGradientLayerNewArch.m new file mode 100644 index 00000000..2a938aad --- /dev/null +++ b/ios/RNLinearGradientLayerNewArch.m @@ -0,0 +1,232 @@ +#import "RNLinearGradientLayer.h" + +#include +#import + +@implementation RNLinearGradientLayer +{ + CALayer *_clipLayer; + CAGradientLayer *_gradientLayer; +} + +- (instancetype)init +{ + self = [super init]; + + if (self) + { + // _clipLayer clips the gradient to the outer bounds + cornerRadius, + // independently of self.masksToBounds (which follows yoga overflow). + _clipLayer = [CALayer layer]; + _clipLayer.masksToBounds = YES; + // Push the gradient behind RN-child sublayers (default zPosition 0) + // but in front of RCTView's private background / border / shadow + // sublayers, which Fabric's RCTViewComponentView pins to + // BACKGROUND_COLOR_ZPOSITION = -1024. -CGFLOAT_MAX would sit behind + // those too, hiding the gradient whenever a consumer sets + // `backgroundColor` or `boxShadow` on . -512 picks a + // finite, comfortable midpoint. + _clipLayer.zPosition = -512.0; + [self addSublayer:_clipLayer]; + + _gradientLayer = [CAGradientLayer layer]; + // Our geometry math assumes rotation pivots around the gradient + // layer's center. CALayer's default anchorPoint is (0.5, 0.5); set + // it explicitly so a future subclass / animator can't silently move + // the pivot. + _gradientLayer.anchorPoint = CGPointMake(0.5, 0.5); + [_clipLayer addSublayer:_gradientLayer]; + + #ifndef RCT_NEW_ARCH_ENABLED + self.masksToBounds = YES; + #endif + _startPoint = CGPointMake(0.0, 0.0); + _endPoint = CGPointMake(0.0, 0.0); + _angleCenter = CGPointMake(0.0, 0.0); + _angle = 0.0; + _useAngle = NO; + } + + return self; +} + +- (void)layoutSublayers +{ + [super layoutSublayers]; + + [CATransaction begin]; + [CATransaction setDisableActions:YES]; + + _clipLayer.frame = self.bounds; + [self mirrorCornerShape]; + [self applyGradientGeometry]; + + [CATransaction commit]; +} + +- (void)setCornerRadius:(CGFloat)cornerRadius +{ + [super setCornerRadius:cornerRadius]; + // RCTView updates cornerRadius without changing bounds, so layoutSublayers + // won't fire — mirror it onto _clipLayer immediately or the gradient clip + // lags behind the outer view's rounded corners. + [self mirrorCornerShape]; +} + +- (void)mirrorCornerShape +{ + if (!_clipLayer) return; + [CATransaction begin]; + [CATransaction setDisableActions:YES]; + _clipLayer.cornerRadius = fmax(0.0, self.cornerRadius); + if (@available(iOS 13.0, *)) { + _clipLayer.cornerCurve = self.cornerCurve; + } + [CATransaction commit]; +} + +- (void)setColors:(NSArray *)colors +{ + _colors = [colors copy]; + + NSMutableArray *cgColors = [NSMutableArray arrayWithCapacity:colors.count]; + for (UIColor *color in colors) { + [cgColors addObject:(id)color.CGColor]; + } + + // _gradientLayer is a standalone sublayer (not a UIView's backing layer), + // so CAGradientLayer's animatable `colors` uses its default implicit + // action and crossfades on every assignment. Disable actions so JS prop + // updates snap. + [CATransaction begin]; + [CATransaction setDisableActions:YES]; + _gradientLayer.colors = cgColors; + [CATransaction commit]; +} + +- (void)setLocations:(NSArray *)locations +{ + _locations = [locations copy]; + [CATransaction begin]; + [CATransaction setDisableActions:YES]; + _gradientLayer.locations = locations; + [CATransaction commit]; +} + +- (void)setStartPoint:(CGPoint)startPoint +{ + _startPoint = startPoint; + [self applyGradientGeometry]; +} + +- (void)setEndPoint:(CGPoint)endPoint +{ + _endPoint = endPoint; + [self applyGradientGeometry]; +} + +- (void)setUseAngle:(BOOL)useAngle +{ + _useAngle = useAngle; + [self applyGradientGeometry]; +} + +- (void)setAngleCenter:(CGPoint)angleCenter +{ + _angleCenter = angleCenter; + [self applyGradientGeometry]; +} + +- (void)setAngle:(CGFloat)angle +{ + _angle = angle; + [self applyGradientGeometry]; +} + +- (void)applyGradientGeometry +{ + CGSize size = self.bounds.size; + CGFloat W = size.width; + CGFloat H = size.height; + + // Bail if bounds aren't set yet — setters can fire before the first + // layout pass. layoutSublayers will re-run geometry once W/H are real. + if (W <= 0.0 || H <= 0.0) { + return; + } + + [CATransaction begin]; + [CATransaction setDisableActions:YES]; + + if (!_useAngle) { + _gradientLayer.bounds = CGRectMake(0, 0, W, H); + _gradientLayer.position = CGPointMake(W / 2.0, H / 2.0); + _gradientLayer.transform = CATransform3DIdentity; + _gradientLayer.startPoint = _startPoint; + _gradientLayer.endPoint = _endPoint; + } else { + // Guard numerics: NaN propagates into every CA property and wipes + // the gradient; large angles lose sin/cos precision via argument + // reduction. + CGFloat angle = isfinite(_angle) ? fmod(_angle, 360.0) : 0.0; + CGFloat acxNorm = isfinite(_angleCenter.x) ? _angleCenter.x : 0.5; + CGFloat acyNorm = isfinite(_angleCenter.y) ? _angleCenter.y : 0.5; + + // Render a bearing-0 (pointing-up / north) gradient in a square + // sublayer and rotate it into place, rather than letting + // CAGradientLayer handle the angle directly. CAGradientLayer's + // iso-color lines are perpendicular to startPoint→endPoint in + // unit space, which on a non-square layer gets stretched under the + // unit→pixel mapping and compresses the visible t-range. A square + // layer has W = H, so perpendicular-in-unit-space stays + // perpendicular-in-pixel-space after the rotation transform. + CGFloat bearingRad = angle * M_PI / 180.0; + // Half the projection of the parent rectangle onto the bearing + // direction — i.e., the CSS-spec distance from the rectangle's + // center to the 0% / 100% stop along the gradient line. Note this + // uses the rectangle's geometric center, not angleCenter, matching + // the CSS `linear-gradient` spec (which has no angleCenter concept). + CGFloat halfExtent = (W * fabs(sin(bearingRad)) + H * fabs(cos(bearingRad))) / 2.0; + + // The square must cover every parent corner in the rotated + // frame. A corner at (cx, cy) relative to angleCenter projects + // to (x', y') on the rotated axes; we need both |x'| and |y'| + // ≤ layerSize/2. Taking the max over all four corners of + // max(|x'|, |y'|) and doubling gives the tight square size. + // This is tighter than 2·√(x'² + y'²) (the distance bound) by + // up to ~3% linear at 45°; exact at axis angles. The +2 is + // slack for sub-pixel round-off at exact-fit angles. + CGFloat acx = acxNorm * W; + CGFloat acy = acyNorm * H; + CGFloat cosA = cos(bearingRad); + CGFloat sinA = sin(bearingRad); + CGFloat cornerDx[4] = { -acx, W - acx, -acx, W - acx }; + CGFloat cornerDy[4] = { -acy, -acy, H - acy, H - acy }; + CGFloat maxHalf = 0.0; + for (int i = 0; i < 4; i++) { + CGFloat xp = cornerDx[i] * cosA + cornerDy[i] * sinA; + CGFloat yp = -cornerDx[i] * sinA + cornerDy[i] * cosA; + maxHalf = fmax(maxHalf, fmax(fabs(xp), fabs(yp))); + } + CGFloat layerSize = 2.0 * maxHalf + 2.0; + + // Pin startPoint / endPoint to ±halfExtent pixels from the layer's + // center regardless of layerSize. That nails the 0% and 100% stops + // to the CSS-spec pixel offset from angleCenter; the layer itself + // is free to extend past the parent for full coverage. + // startPoint is the 0% stop (below center), endPoint the 100% stop + // (above center). Rotating a north-pointing gradient by `_angle` + // degrees clockwise produces the bearing direction directly — no + // offset needed. + CGFloat ratio = halfExtent / layerSize; + _gradientLayer.bounds = CGRectMake(0, 0, layerSize, layerSize); + _gradientLayer.position = CGPointMake(acx, acy); + _gradientLayer.startPoint = CGPointMake(0.5, 0.5 + ratio); + _gradientLayer.endPoint = CGPointMake(0.5, 0.5 - ratio); + _gradientLayer.transform = CATransform3DMakeRotation(bearingRad, 0, 0, 1); + } + + [CATransaction commit]; +} + +@end From 0d6b9b1b97c63ba290f94bec77c5903fc2185f54 Mon Sep 17 00:00:00 2001 From: SzymczakJ Date: Wed, 22 Apr 2026 10:35:58 +0200 Subject: [PATCH 2/5] fix build problems --- ios/RNLinearGradient.mm | 19 ++++++++++++++----- ios/RNLinearGradientLayerNewArch.m | 4 ++-- 2 files changed, 16 insertions(+), 7 deletions(-) diff --git a/ios/RNLinearGradient.mm b/ios/RNLinearGradient.mm index 46640efc..96b775c1 100644 --- a/ios/RNLinearGradient.mm +++ b/ios/RNLinearGradient.mm @@ -9,6 +9,8 @@ #import #import + +#import "RNLinearGradientLayerNewArch.h" #endif #import @@ -29,17 +31,24 @@ @implementation RNLinearGradient + (Class)layerClass { +#ifdef RCT_NEW_ARCH_ENABLED + return [RNLinearGradientLayerNewArch class]; +#else return [RNLinearGradientLayer class]; +#endif } +#ifdef RCT_NEW_ARCH_ENABLED +- (RNLinearGradientLayerNewArch *)gradientLayer +{ + return (RNLinearGradientLayerNewArch *)self.layer; +} +#else - (RNLinearGradientLayer *)gradientLayer { - #ifdef RCT_NEW_ARCH_ENABLED - return [RNLinearGradientLayerNewArch class]; - #else - return [RNLinearGradientLayer class]; - #endif + return (RNLinearGradientLayer *)self.layer; } +#endif - (NSArray *)colors { diff --git a/ios/RNLinearGradientLayerNewArch.m b/ios/RNLinearGradientLayerNewArch.m index 2a938aad..cde80a0a 100644 --- a/ios/RNLinearGradientLayerNewArch.m +++ b/ios/RNLinearGradientLayerNewArch.m @@ -1,9 +1,9 @@ -#import "RNLinearGradientLayer.h" +#import "RNLinearGradientLayerNewArch.h" #include #import -@implementation RNLinearGradientLayer +@implementation RNLinearGradientLayerNewArch { CALayer *_clipLayer; CAGradientLayer *_gradientLayer; From add76c87ed4431da4ceca789af26214c47bb313a Mon Sep 17 00:00:00 2001 From: SzymczakJ Date: Wed, 22 Apr 2026 13:00:51 +0200 Subject: [PATCH 3/5] remove redundant code --- ios/RNLinearGradient.mm | 9 --------- 1 file changed, 9 deletions(-) diff --git a/ios/RNLinearGradient.mm b/ios/RNLinearGradient.mm index 96b775c1..512f4e3e 100644 --- a/ios/RNLinearGradient.mm +++ b/ios/RNLinearGradient.mm @@ -154,15 +154,6 @@ - (void)updateProps:(Props::Shared const &)props oldProps:(Props::Shared const & const auto &oldViewProps = *std::static_pointer_cast(_props); const auto &newViewProps = *std::static_pointer_cast(props); - if (newViewProps.yogaStyle.overflow() != oldViewProps.yogaStyle.overflow()) { - // 0 for visible, 1 for hidden - if ((int)newViewProps.yogaStyle.overflow() == 0) { - self.layer.masksToBounds = false; - } else { - self.layer.masksToBounds = true; - } - } - if(oldViewProps.startPoint.x != newViewProps.startPoint.x || oldViewProps.startPoint.y != newViewProps.startPoint.y) { self.startPoint = CGPointMake(newViewProps.startPoint.x, newViewProps.startPoint.y); } From e9d3393183effe0f3d2cd64c26f4244ab70412de Mon Sep 17 00:00:00 2001 From: SzymczakJ Date: Wed, 22 Apr 2026 16:02:49 +0200 Subject: [PATCH 4/5] clean up comments --- ios/RNLinearGradientLayerNewArch.m | 3 --- 1 file changed, 3 deletions(-) diff --git a/ios/RNLinearGradientLayerNewArch.m b/ios/RNLinearGradientLayerNewArch.m index cde80a0a..7f5f035a 100644 --- a/ios/RNLinearGradientLayerNewArch.m +++ b/ios/RNLinearGradientLayerNewArch.m @@ -193,9 +193,6 @@ - (void)applyGradientGeometry // to (x', y') on the rotated axes; we need both |x'| and |y'| // ≤ layerSize/2. Taking the max over all four corners of // max(|x'|, |y'|) and doubling gives the tight square size. - // This is tighter than 2·√(x'² + y'²) (the distance bound) by - // up to ~3% linear at 45°; exact at axis angles. The +2 is - // slack for sub-pixel round-off at exact-fit angles. CGFloat acx = acxNorm * W; CGFloat acy = acyNorm * H; CGFloat cosA = cos(bearingRad); From df2e91bf60fa7e02d5af49b6db810fe86ed61c9c Mon Sep 17 00:00:00 2001 From: SzymczakJ Date: Wed, 22 Apr 2026 17:37:28 +0200 Subject: [PATCH 5/5] apply old patches --- index.android.js | 3 ++- index.d.ts | 6 ++++++ index.ios.js | 2 +- index.js | 3 +++ ios/RNLinearGradient.mm | 1 - ios/RNLinearGradientLayer.m | 9 +++++---- package.json | 7 ++++++- 7 files changed, 23 insertions(+), 8 deletions(-) diff --git a/index.android.js b/index.android.js index 63d59811..fef10c4e 100644 --- a/index.android.js +++ b/index.android.js @@ -85,7 +85,8 @@ export default class LinearGradient extends Component { { + startPoint?: { x: number; y: number }, + endPoint?: { x: number; y: number }, + } + export class LinearGradient extends React.Component {} + export class LinearGradientNativeComponent extends React.Component {} export default LinearGradient; } diff --git a/index.ios.js b/index.ios.js index 489f2146..2e153519 100644 --- a/index.ios.js +++ b/index.ios.js @@ -56,7 +56,7 @@ export default class LinearGradient extends Component { {...otherProps} startPoint={convertPoint('start', start)} endPoint={convertPoint('end', end)} - colors={colors.map(processColor)} + colors={global.RN$Bridgeless ? colors : colors.map(processColor)} locations={locations ? locations.slice(0, colors.length) : null} useAngle={useAngle} angleCenter={convertPoint('angleCenter', angleCenter)} diff --git a/index.js b/index.js index d0403550..d6721d37 100644 --- a/index.js +++ b/index.js @@ -2,9 +2,12 @@ import { Platform } from "react-native"; import LinearGradientIos from "./index.ios.js"; import LinearGradientAndroid from "./index.android.js"; import LinearGradientWindows from "./index.windows.js"; +import LinearGradientNativeComponentRN from "./src/index.js"; export const LinearGradient = Platform.OS === "ios" ? LinearGradientIos : Platform.OS === "android" ? LinearGradientAndroid : LinearGradientWindows; +export const LinearGradientNativeComponent = LinearGradientNativeComponentRN; + export default LinearGradient; diff --git a/ios/RNLinearGradient.mm b/ios/RNLinearGradient.mm index 512f4e3e..c181bd4e 100644 --- a/ios/RNLinearGradient.mm +++ b/ios/RNLinearGradient.mm @@ -190,7 +190,6 @@ - (void)updateProps:(Props::Shared const &)props oldProps:(Props::Shared const & static NSArray *convertCxxVectorColorsToNSArrayColors(const std::vector &colors) { size_t size = colors.size(); - NSLog(@"%zu", size); NSMutableArray *result = [NSMutableArray new]; for(size_t i = 0; i < size; i++) { UIColor *color = RCTUIColorFromSharedColor(colors[i]); diff --git a/ios/RNLinearGradientLayer.m b/ios/RNLinearGradientLayer.m index a2ff8799..4ea0ce9c 100644 --- a/ios/RNLinearGradientLayer.m +++ b/ios/RNLinearGradientLayer.m @@ -13,10 +13,11 @@ - (instancetype)init { self.needsDisplayOnBoundsChange = YES; self.masksToBounds = YES; - _startPoint = CGPointMake(0.5, 0.0); - _endPoint = CGPointMake(0.5, 1.0); - _angleCenter = CGPointMake(0.5, 0.5); - _angle = 45.0; + _startPoint = CGPointMake(0.0, 0.0); + _endPoint = CGPointMake(0.0, 0.0); + _angleCenter = CGPointMake(0.0, 0.0); + _angle = 0.0; + _useAngle = false; } return self; diff --git a/package.json b/package.json index 9806319d..d2bc0f17 100644 --- a/package.json +++ b/package.json @@ -45,6 +45,11 @@ "codegenConfig": { "name": "RNLinearGradientSpec", "type": "components", - "jsSrcsDir": "src" + "jsSrcsDir": "src", + "ios": { + "componentProvider": { + "RNLinearGradient": "RNLinearGradient" + } + } } }