From fd43bcc8c42961d17b65f7fe6a1f9421d47f40a8 Mon Sep 17 00:00:00 2001 From: Isaac Israel Date: Wed, 13 May 2026 18:44:02 +0300 Subject: [PATCH 1/4] fix(ios): add Auto Layout constraints to RNNReactButtonView on iOS 26 MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit On iOS 26 the Liquid Glass navigation bar wraps custom-view bar button items in several internal layout containers. Without explicit size constraints the wrapper views can collapse to zero height after a pop → tab-switch → tab-switch → push cycle, making the React button invisible. - Guard all new logic behind @available(iOS 26.0, *) AND a runtime check for UIDesignRequiresCompatibility (compatibility mode disables Liquid Glass and uses the legacy view hierarchy) - Set translatesAutoresizingMaskIntoConstraints = NO - Add width/height constraints at UILayoutPriorityDefaultHigh - Update constraints in didMountComponentsWithRootTag: after sizeToFit Co-authored-by: Cursor --- ios/RNNReactButtonView.mm | 43 +++++++++++++++++++++++++++++++++++++-- 1 file changed, 41 insertions(+), 2 deletions(-) diff --git a/ios/RNNReactButtonView.mm b/ios/RNNReactButtonView.mm index 607d91286bf..1da4c2735b4 100644 --- a/ios/RNNReactButtonView.mm +++ b/ios/RNNReactButtonView.mm @@ -1,6 +1,10 @@ #import "RNNReactButtonView.h" +#import -@implementation RNNReactButtonView +@implementation RNNReactButtonView { + NSLayoutConstraint *_widthConstraint; + NSLayoutConstraint *_heightConstraint; +} - (instancetype)initWithHost:(RCTHost *)host moduleName:(NSString *)moduleName @@ -10,15 +14,50 @@ - (instancetype)initWithHost:(RCTHost *)host reactViewReadyBlock:(RNNReactViewReadyCompletionBlock)reactViewReadyBlock { self = [super initWithHost:host moduleName:moduleName initialProperties:initialProperties eventEmitter:eventEmitter sizeMeasureMode:convertToSurfaceSizeMeasureMode(RCTRootViewSizeFlexibilityWidthAndHeight) reactViewReadyBlock:reactViewReadyBlock]; [host.surfacePresenter addObserver:self]; - self.backgroundColor = UIColor.clearColor; + self.backgroundColor = [UIColor clearColor]; + + if (@available(iOS 26.0, *)) { + if (![self designRequiresCompatibility]) { + self.translatesAutoresizingMaskIntoConstraints = NO; + _widthConstraint = [self.widthAnchor constraintEqualToConstant:0]; + _heightConstraint = [self.heightAnchor constraintEqualToConstant:0]; + _widthConstraint.priority = UILayoutPriorityDefaultHigh; + _heightConstraint.priority = UILayoutPriorityDefaultHigh; + _widthConstraint.active = YES; + _heightConstraint.active = YES; + } + } return self; } +- (BOOL)designRequiresCompatibility { + static BOOL checked = NO; + static BOOL result = NO; + if (!checked) { + checked = YES; + result = [[[NSBundle mainBundle] objectForInfoDictionaryKey:@"UIDesignRequiresCompatibility"] boolValue]; + } + return result; +} + - (void)didMountComponentsWithRootTag:(NSInteger)rootTag { if (self.surface.rootTag == rootTag) { [super didMountComponentsWithRootTag:rootTag]; [self sizeToFit]; + if (@available(iOS 26.0, *)) { + if (![self designRequiresCompatibility]) { + [self updateConstraintsToFitSize]; + } + } + } +} + +- (void)updateConstraintsToFitSize { + CGSize size = self.frame.size; + if (size.width > 0 && size.height > 0) { + _widthConstraint.constant = size.width; + _heightConstraint.constant = size.height; } } From db70f4c3088267ef9590aaf5289695fbbeb281d0 Mon Sep 17 00:00:00 2001 From: Isaac Israel Date: Wed, 13 May 2026 19:05:28 +0300 Subject: [PATCH 2/4] fix(ios): center RNNReactButtonView inside navigation bar platter On iOS 26 the internal _UITAMICAdaptorView wrapper is wider than the React button content and UIKit pins the custom view to the leading edge. Apply a one-time horizontal CGAffineTransform in layoutSubviews to center the view. Guarded by @available(iOS 26.0, *) and UIDesignRequiresCompatibility check. Co-authored-by: Cursor --- ios/RNNReactButtonView.mm | 18 ++++++++++++++++++ 1 file changed, 18 insertions(+) diff --git a/ios/RNNReactButtonView.mm b/ios/RNNReactButtonView.mm index 1da4c2735b4..8f466fcd275 100644 --- a/ios/RNNReactButtonView.mm +++ b/ios/RNNReactButtonView.mm @@ -4,6 +4,7 @@ @implementation RNNReactButtonView { NSLayoutConstraint *_widthConstraint; NSLayoutConstraint *_heightConstraint; + BOOL _didCenter; } - (instancetype)initWithHost:(RCTHost *)host @@ -25,6 +26,7 @@ - (instancetype)initWithHost:(RCTHost *)host _heightConstraint.priority = UILayoutPriorityDefaultHigh; _widthConstraint.active = YES; _heightConstraint.active = YES; + _didCenter = NO; } } @@ -61,6 +63,22 @@ - (void)updateConstraintsToFitSize { } } +- (void)layoutSubviews { + [super layoutSubviews]; + if (@available(iOS 26.0, *)) { + if ([self designRequiresCompatibility]) return; + if (!_didCenter && self.superview && self.frame.size.width > 0) { + CGFloat wrapperWidth = self.superview.bounds.size.width; + CGFloat selfWidth = self.frame.size.width; + if (wrapperWidth > selfWidth) { + _didCenter = YES; + CGFloat tx = (wrapperWidth - selfWidth) / 2.0; + self.layer.affineTransform = CGAffineTransformMakeTranslation(tx, 0); + } + } + } +} + - (NSString *)componentType { return ComponentTypeButton; } From 70207b6790641951ae1e4ed16321e33d05edcb80 Mon Sep 17 00:00:00 2001 From: Isaac Israel Date: Wed, 13 May 2026 19:09:42 +0300 Subject: [PATCH 3/4] feat(ios): add backgroundColor prop to top bar buttons MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Add a new `backgroundColor` option to `OptionsTopBarButton` that sets a solid color on the iOS 26 Liquid Glass circular platter behind React component bar buttons. All logic is guarded by @available(iOS 26.0, *) and a runtime check for UIDesignRequiresCompatibility — when compatibility mode is enabled the prop is ignored and the button renders as before. Co-authored-by: Cursor --- ios/RNNButtonOptions.h | 1 + ios/RNNReactButtonView.mm | 21 +++++++++++++++++++++ src/interfaces/Options.ts | 7 +++++++ 3 files changed, 29 insertions(+) diff --git a/ios/RNNButtonOptions.h b/ios/RNNButtonOptions.h index 753cc49f95e..cebe7a4e8ff 100644 --- a/ios/RNNButtonOptions.h +++ b/ios/RNNButtonOptions.h @@ -24,6 +24,7 @@ @property(nonatomic, strong) RNNIconBackgroundOptions *iconBackground; @property(nonatomic, strong) Bool *disableIconTint; @property(nonatomic, strong) Bool *hideSharedBackground; +@property(nonatomic, strong) Color *backgroundColor; - (RNNButtonOptions *)withDefault:(RNNButtonOptions *)defaultOptions; diff --git a/ios/RNNReactButtonView.mm b/ios/RNNReactButtonView.mm index 8f466fcd275..59e76a7bcf3 100644 --- a/ios/RNNReactButtonView.mm +++ b/ios/RNNReactButtonView.mm @@ -63,6 +63,15 @@ - (void)updateConstraintsToFitSize { } } +- (void)didMoveToWindow { + [super didMoveToWindow]; + if (@available(iOS 26.0, *)) { + if (![self designRequiresCompatibility] && self.window) { + [self syncButtonBackground]; + } + } +} + - (void)layoutSubviews { [super layoutSubviews]; if (@available(iOS 26.0, *)) { @@ -76,9 +85,21 @@ - (void)layoutSubviews { self.layer.affineTransform = CGAffineTransformMakeTranslation(tx, 0); } } + [self syncButtonBackground]; } } +- (void)syncButtonBackground { + if (!_buttonBackgroundColor) return; + + UIView *target = self.superview.superview.superview; + if (!target || target.bounds.size.height <= 0) return; + + target.backgroundColor = _buttonBackgroundColor; + target.layer.cornerRadius = target.bounds.size.height / 2.0; + target.clipsToBounds = YES; +} + - (NSString *)componentType { return ComponentTypeButton; } diff --git a/src/interfaces/Options.ts b/src/interfaces/Options.ts index bebca9b8ab5..081463ac2fe 100644 --- a/src/interfaces/Options.ts +++ b/src/interfaces/Options.ts @@ -658,6 +658,13 @@ export interface OptionsTopBarButton { * @see {@link https://developer.android.com/guide/topics/resources/menu-resource|Android developer guide: Menu resource} */ showAsAction?: 'ifRoom' | 'withText' | 'always' | 'never'; + /** + * (iOS 26+ only) Set a solid background color on the circular Liquid Glass + * platter behind the button. Ignored when UIDesignRequiresCompatibility is + * enabled or on iOS < 26. + * #### (iOS specific) + */ + backgroundColor?: Color; } export interface OptionsSearchBar { From 891a9598c0b4b9bb49fbf510de45cfee59531828 Mon Sep 17 00:00:00 2001 From: Isaac Israel Date: Wed, 27 May 2026 14:53:20 +0300 Subject: [PATCH 4/4] feat(ios): add missing files for backgroundColor button prop Add the implementation files that were missing from the initial commit: - RNNButtonOptions.mm: parse, copy, and merge backgroundColor - RNNReactButtonView.h: declare buttonBackgroundColor property - RNNButtonBuilder.mm: wire button.backgroundColor to the view Also add: - Native XCTest suite (RNNButtonOptionsTest) for parse/copy/merge/withDefault - JS OptionsProcessor tests for button backgroundColor color processing - Playground ButtonsScreen example with "Set Button Background Color" action - API documentation in options-button.mdx Co-authored-by: Cursor --- ios/RNNButtonBuilder.mm | 1 + ios/RNNButtonOptions.mm | 4 + ios/RNNReactButtonView.h | 2 + .../NavigationTests/RNNButtonOptionsTest.mm | 84 +++++++++++++++++++ .../ios/playground.xcodeproj/project.pbxproj | 4 + playground/src/screens/ButtonsScreen.tsx | 28 +++++++ playground/src/testIDs.ts | 1 + src/commands/OptionsProcessor.test.ts | 40 +++++++++ website/docs/api/options-button.mdx | 8 ++ 9 files changed, 172 insertions(+) create mode 100644 playground/ios/NavigationTests/RNNButtonOptionsTest.mm diff --git a/ios/RNNButtonBuilder.mm b/ios/RNNButtonBuilder.mm index 07319487470..a6e22a79031 100644 --- a/ios/RNNButtonBuilder.mm +++ b/ios/RNNButtonBuilder.mm @@ -30,6 +30,7 @@ - (RNNUIBarButtonItem *)build:(RNNButtonOptions *)button parentComponentId:parentComponentId componentType:RNNComponentTypeTopBarButton reactViewReadyBlock:nil]; + view.buttonBackgroundColor = [button.backgroundColor withDefault:nil]; return [[RNNUIBarButtonItem alloc] initWithCustomView:view buttonOptions:button onPress:onPress]; diff --git a/ios/RNNButtonOptions.mm b/ios/RNNButtonOptions.mm index 23b2d267a07..c9681fda49a 100644 --- a/ios/RNNButtonOptions.mm +++ b/ios/RNNButtonOptions.mm @@ -26,6 +26,7 @@ - (instancetype)initWithDict:(NSDictionary *)dict { self.systemItem = [TextParser parse:dict key:@"systemItem"]; self.disableIconTint = [BoolParser parse:dict key:@"disableIconTint"]; self.hideSharedBackground = [BoolParser parse:dict key:@"hideSharedBackground"]; + self.backgroundColor = [ColorParser parse:dict key:@"backgroundColor"]; return self; } @@ -51,6 +52,7 @@ - (RNNButtonOptions *)copy { newOptions.systemItem = self.systemItem.copy; newOptions.disableIconTint = self.disableIconTint.copy; newOptions.hideSharedBackground = self.hideSharedBackground.copy; + newOptions.backgroundColor = self.backgroundColor.copy; return newOptions; } @@ -92,6 +94,8 @@ - (void)mergeOptions:(RNNButtonOptions *)options { self.disableIconTint = options.disableIconTint; if (options.hideSharedBackground.hasValue) self.hideSharedBackground = options.hideSharedBackground; + if (options.backgroundColor.hasValue) + self.backgroundColor = options.backgroundColor; } - (BOOL)shouldCreateCustomView { diff --git a/ios/RNNReactButtonView.h b/ios/RNNReactButtonView.h index 6dd7d74f5ce..99f563e8904 100644 --- a/ios/RNNReactButtonView.h +++ b/ios/RNNReactButtonView.h @@ -6,4 +6,6 @@ @interface RNNReactButtonView : RNNComponentView +@property(nonatomic, strong) UIColor *buttonBackgroundColor; + @end diff --git a/playground/ios/NavigationTests/RNNButtonOptionsTest.mm b/playground/ios/NavigationTests/RNNButtonOptionsTest.mm new file mode 100644 index 00000000000..96a99354df7 --- /dev/null +++ b/playground/ios/NavigationTests/RNNButtonOptionsTest.mm @@ -0,0 +1,84 @@ +#import +#import + +@interface RNNButtonOptionsTest : XCTestCase + +@end + +@implementation RNNButtonOptionsTest + +- (void)testInitWithDict_shouldParseBackgroundColor { + RNNButtonOptions *options = [[RNNButtonOptions alloc] initWithDict:@{ + @"id" : @"buttonId", + @"backgroundColor" : @(0xffffff00), + }]; + + XCTAssertTrue(options.backgroundColor.hasValue); + XCTAssertTrue([options.backgroundColor.get isEqual:UIColor.yellowColor]); +} + +- (void)testInitWithDict_shouldNotRequireBackgroundColor { + RNNButtonOptions *options = [[RNNButtonOptions alloc] initWithDict:@{ + @"id" : @"buttonId", + @"text" : @"title", + }]; + + XCTAssertFalse(options.backgroundColor.hasValue); +} + +- (void)testCopy_shouldCopyBackgroundColor { + RNNButtonOptions *options = [[RNNButtonOptions alloc] initWithDict:@{ + @"id" : @"buttonId", + @"backgroundColor" : @(0xffffff00), + }]; + + RNNButtonOptions *copied = [options copy]; + + XCTAssertTrue(copied.backgroundColor.hasValue); + XCTAssertTrue([copied.backgroundColor.get isEqual:UIColor.yellowColor]); +} + +- (void)testMergeOptions_shouldMergeBackgroundColor { + RNNButtonOptions *options = [[RNNButtonOptions alloc] initWithDict:@{ + @"id" : @"buttonId", + @"backgroundColor" : @(0xffffff00), + }]; + RNNButtonOptions *mergeOptions = [[RNNButtonOptions alloc] initWithDict:@{ + @"id" : @"buttonId", + @"backgroundColor" : @(0xffff0000), + }]; + + [options mergeOptions:mergeOptions]; + + XCTAssertTrue([options.backgroundColor.get isEqual:UIColor.redColor]); +} + +- (void)testMergeOptions_shouldNotOverwriteBackgroundColorWhenAbsent { + RNNButtonOptions *options = [[RNNButtonOptions alloc] initWithDict:@{ + @"id" : @"buttonId", + @"backgroundColor" : @(0xffffff00), + }]; + RNNButtonOptions *mergeOptions = [[RNNButtonOptions alloc] initWithDict:@{ + @"id" : @"buttonId", + }]; + + [options mergeOptions:mergeOptions]; + + XCTAssertTrue([options.backgroundColor.get isEqual:UIColor.yellowColor]); +} + +- (void)testWithDefault_shouldFallBackToDefaultBackgroundColor { + RNNButtonOptions *options = [[RNNButtonOptions alloc] initWithDict:@{ + @"id" : @"buttonId", + }]; + RNNButtonOptions *defaults = [[RNNButtonOptions alloc] initWithDict:@{ + @"id" : @"buttonId", + @"backgroundColor" : @(0xff00ff00), + }]; + + RNNButtonOptions *result = [options withDefault:defaults]; + + XCTAssertTrue([result.backgroundColor.get isEqual:UIColor.greenColor]); +} + +@end diff --git a/playground/ios/playground.xcodeproj/project.pbxproj b/playground/ios/playground.xcodeproj/project.pbxproj index 4e76a4d4b3f..8d212028f6e 100644 --- a/playground/ios/playground.xcodeproj/project.pbxproj +++ b/playground/ios/playground.xcodeproj/project.pbxproj @@ -48,6 +48,7 @@ 50C9A8D4240FB9D000BD699F /* RNNComponentViewController+Utils.mm in Sources */ = {isa = PBXBuildFile; fileRef = 50C9A8D3240FB9D000BD699F /* RNNComponentViewController+Utils.mm */; }; 50CF233D240695B10098042D /* RNNBottomTabsController+Helpers.mm in Sources */ = {isa = PBXBuildFile; fileRef = 50CF233C240695B10098042D /* RNNBottomTabsController+Helpers.mm */; }; 50FDEFBC258F5C5D008C9C3C /* RNNSearchBarOptionsTest.mm in Sources */ = {isa = PBXBuildFile; fileRef = 50FDEFBB258F5C5D008C9C3C /* RNNSearchBarOptionsTest.mm */; }; + A1B2C3D4E5F60718A1B2C3D5 /* RNNButtonOptionsTest.mm in Sources */ = {isa = PBXBuildFile; fileRef = A1B2C3D4E5F60718A1B2C3D4 /* RNNButtonOptionsTest.mm */; }; 6B102251DCC578519C2DC6A4 /* libPods-NavigationIOS12Tests.a in Frameworks */ = {isa = PBXBuildFile; fileRef = C10F72071A488F801E1F1116 /* libPods-NavigationIOS12Tests.a */; }; 8EB60A6CB93C527CC6A870A2 /* libPods-SnapshotTests.a in Frameworks */ = {isa = PBXBuildFile; fileRef = E8B4CFA18A5ACE953124E129 /* libPods-SnapshotTests.a */; }; 9F9A3A9625260DA900AAAF37 /* LaunchScreen.storyboard in Resources */ = {isa = PBXBuildFile; fileRef = 9F9A3A9525260DA900AAAF37 /* LaunchScreen.storyboard */; }; @@ -166,6 +167,7 @@ 50CF233C240695B10098042D /* RNNBottomTabsController+Helpers.mm */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.cpp.objcpp; path = "RNNBottomTabsController+Helpers.mm"; sourceTree = ""; }; 50E4888A2427DA4800B11A8E /* StackOptionsTest.mm */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.cpp.objcpp; path = StackOptionsTest.mm; sourceTree = ""; }; 50FDEFBB258F5C5D008C9C3C /* RNNSearchBarOptionsTest.mm */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.cpp.objcpp; path = RNNSearchBarOptionsTest.mm; sourceTree = ""; }; + A1B2C3D4E5F60718A1B2C3D4 /* RNNButtonOptionsTest.mm */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.cpp.objcpp; path = RNNButtonOptionsTest.mm; sourceTree = ""; }; 60BCFCC02B7F812F00FCDB38 /* libLLVM.dylib */ = {isa = PBXFileReference; lastKnownFileType = "compiled.mach-o.dylib"; name = libLLVM.dylib; path = usr/lib/libLLVM.dylib; sourceTree = SDKROOT; }; 60BCFCCA2B7F817400FCDB38 /* libReactNativeNavigation.a */ = {isa = PBXFileReference; explicitFileType = archive.ar; path = libReactNativeNavigation.a; sourceTree = BUILT_PRODUCTS_DIR; }; 7F8E255E2E08F6ECE7DF6FE3 /* Pods-playground.release.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-playground.release.xcconfig"; path = "Target Support Files/Pods-playground/Pods-playground.release.xcconfig"; sourceTree = ""; }; @@ -416,6 +418,7 @@ E58D26322385888B003F36BA /* RNNNavigationOptionsTest.mm */, E58D263C2385888C003F36BA /* RNNNavigationStackManagerTest.mm */, 50FDEFBB258F5C5D008C9C3C /* RNNSearchBarOptionsTest.mm */, + A1B2C3D4E5F60718A1B2C3D4 /* RNNButtonOptionsTest.mm */, 502734AE24F3E9110022163C /* ColorParserTest.mm */, E58D262C2385888B003F36BA /* RNNOptionsTest.h */, E58D262D2385888B003F36BA /* RNNOverlayManagerTest.mm */, @@ -968,6 +971,7 @@ 50C9A8D4240FB9D000BD699F /* RNNComponentViewController+Utils.mm in Sources */, 50647FE323E3196800B92025 /* RNNExternalViewControllerTests.mm in Sources */, 50FDEFBC258F5C5D008C9C3C /* RNNSearchBarOptionsTest.mm in Sources */, + A1B2C3D4E5F60718A1B2C3D5 /* RNNButtonOptionsTest.mm in Sources */, E58D264D2385888C003F36BA /* RNNOverlayManagerTest.mm in Sources */, 501C86B9239FE9C400E0B631 /* UIImage+Utils.mm in Sources */, E58D265F2385888C003F36BA /* RNNBasePresenterTest.mm in Sources */, diff --git a/playground/src/screens/ButtonsScreen.tsx b/playground/src/screens/ButtonsScreen.tsx index 38701df80ea..30e5aa8f300 100644 --- a/playground/src/screens/ButtonsScreen.tsx +++ b/playground/src/screens/ButtonsScreen.tsx @@ -26,6 +26,7 @@ const { RESET_BUTTONS, CHANGE_BUTTON_PROPS, CHANGE_LEFT_RIGHT_COLORS, + SET_BUTTON_BACKGROUND_COLOR, } = testIDs; export default class ButtonOptions extends NavigationComponent { @@ -130,6 +131,11 @@ export default class ButtonOptions extends NavigationComponent { label="Set leftButtons default Color" onPress={this.changeButtonsColor} /> +