From 126bb2114c31f2b42f407f663658784c93f0dae6 Mon Sep 17 00:00:00 2001 From: Isaac Israel Date: Wed, 27 May 2026 15:34:35 +0300 Subject: [PATCH 1/2] fix(ios): separate top bar buttons on iOS 26 with zero-width spacers On iOS 26 (Liquid Glass), multiple UIBarButtonItems in the navigation bar merge into a single visual platter when placed adjacent to each other. This makes it impossible for users to distinguish individual buttons. Insert zero-width fixed-space items between bar button items so the system renders each button as a separate element. Gated behind @available(iOS 26.0, *) so earlier versions are unaffected. Fixes #8203 --- ios/RNNButtonsPresenter.mm | 13 +++++++++++++ 1 file changed, 13 insertions(+) diff --git a/ios/RNNButtonsPresenter.mm b/ios/RNNButtonsPresenter.mm index 3132df061c3..d99dd2a1efd 100644 --- a/ios/RNNButtonsPresenter.mm +++ b/ios/RNNButtonsPresenter.mm @@ -91,6 +91,19 @@ - (void)setButtons:(NSArray *)buttons [barButtonItems addObject:barButtonItem]; } + if (@available(iOS 26.0, *)) { + if (barButtonItems.count > 1) { + NSMutableArray *separated = [NSMutableArray arrayWithCapacity:barButtonItems.count * 2 - 1]; + for (NSUInteger i = 0; i < barButtonItems.count; i++) { + [separated addObject:barButtonItems[i]]; + if (i < barButtonItems.count - 1) { + [separated addObject:[UIBarButtonItem fixedSpaceItemOfWidth:0]]; + } + } + barButtonItems = separated; + } + } + if ([side isEqualToString:@"left"]) { [self replaceCurrentButtons:self.viewController.navigationItem.leftBarButtonItems withButtons:barButtonItems]; From 3bae4dc02096e11b22fcb01bba3d7737b7cbc1ce Mon Sep 17 00:00:00 2001 From: Yedidya Kennard Date: Tue, 2 Jun 2026 14:06:12 +0300 Subject: [PATCH 2/2] test(ios): cover iOS 26 top bar button spacers --- ios/RNNButtonsPresenter.mm | 27 +- .../RNNButtonsPresenterTest.mm | 235 ++++++++++++------ 2 files changed, 183 insertions(+), 79 deletions(-) diff --git a/ios/RNNButtonsPresenter.mm b/ios/RNNButtonsPresenter.mm index d99dd2a1efd..374117cd0d6 100644 --- a/ios/RNNButtonsPresenter.mm +++ b/ios/RNNButtonsPresenter.mm @@ -48,26 +48,34 @@ - (void)applyRightButtons:(NSArray *)rightButtons } - (void)applyLeftButtonsColor:(Color *)color { - for (RNNUIBarButtonItem *button in self.viewController.navigationItem.leftBarButtonItems) { - [button mergeColor:color]; + for (UIBarButtonItem *button in self.viewController.navigationItem.leftBarButtonItems) { + if ([self isRNNUIBarButton:button]) { + [(RNNUIBarButtonItem *)button mergeColor:color]; + } } } - (void)applyRightButtonsColor:(Color *)color { - for (RNNUIBarButtonItem *button in self.viewController.navigationItem.rightBarButtonItems) { - [button mergeColor:color]; + for (UIBarButtonItem *button in self.viewController.navigationItem.rightBarButtonItems) { + if ([self isRNNUIBarButton:button]) { + [(RNNUIBarButtonItem *)button mergeColor:color]; + } } } - (void)applyRightButtonsBackgroundColor:(Color *)color { - for (RNNUIBarButtonItem *button in self.viewController.navigationItem.rightBarButtonItems) { - [button mergeBackgroundColor:color]; + for (UIBarButtonItem *button in self.viewController.navigationItem.rightBarButtonItems) { + if ([self isRNNUIBarButton:button]) { + [(RNNUIBarButtonItem *)button mergeBackgroundColor:color]; + } } } - (void)applyLeftButtonsBackgroundColor:(Color *)color { - for (RNNUIBarButtonItem *button in self.viewController.navigationItem.leftBarButtonItems) { - [button mergeBackgroundColor:color]; + for (UIBarButtonItem *button in self.viewController.navigationItem.leftBarButtonItems) { + if ([self isRNNUIBarButton:button]) { + [(RNNUIBarButtonItem *)button mergeBackgroundColor:color]; + } } } @@ -93,7 +101,8 @@ - (void)setButtons:(NSArray *)buttons if (@available(iOS 26.0, *)) { if (barButtonItems.count > 1) { - NSMutableArray *separated = [NSMutableArray arrayWithCapacity:barButtonItems.count * 2 - 1]; + NSMutableArray *separated = + [NSMutableArray arrayWithCapacity:barButtonItems.count * 2 - 1]; for (NSUInteger i = 0; i < barButtonItems.count; i++) { [separated addObject:barButtonItems[i]]; if (i < barButtonItems.count - 1) { diff --git a/playground/ios/NavigationTests/RNNButtonsPresenterTest.mm b/playground/ios/NavigationTests/RNNButtonsPresenterTest.mm index ff85e082e38..8a8009af8a1 100644 --- a/playground/ios/NavigationTests/RNNButtonsPresenterTest.mm +++ b/playground/ios/NavigationTests/RNNButtonsPresenterTest.mm @@ -1,12 +1,13 @@ #import "RNNComponentViewController+Utils.h" #import +#import #import static UIImage *createTestImage(void) { - UIGraphicsBeginImageContextWithOptions(CGSizeMake(1, 1), NO, 0.0); - UIImage *image = UIGraphicsGetImageFromCurrentImageContext(); - UIGraphicsEndImageContext(); - return image; + UIGraphicsBeginImageContextWithOptions(CGSizeMake(1, 1), NO, 0.0); + UIImage *image = UIGraphicsGetImageFromCurrentImageContext(); + UIGraphicsEndImageContext(); + return image; } @interface RNNButtonsPresenterTest : XCTestCase @@ -19,97 +20,191 @@ @interface RNNButtonsPresenterTest : XCTestCase @implementation RNNButtonsPresenterTest - (void)setUp { - _viewController = [UIViewController new]; - __unused UINavigationController *navigationController = - [[UINavigationController alloc] initWithRootViewController:_viewController]; - _uut = [[RNNButtonsPresenter alloc] initWithComponentRegistry:nil eventEmitter:nil]; - [_uut bindViewController:_viewController]; + _viewController = [UIViewController new]; + __unused UINavigationController *navigationController = + [[UINavigationController alloc] initWithRootViewController:_viewController]; + _uut = [[RNNButtonsPresenter alloc] initWithComponentRegistry:nil eventEmitter:nil]; + [_uut bindViewController:_viewController]; } - (void)testApplyButtons_shouldNotAddEmptyButton { - [_uut applyLeftButtons:@[ [self buttonWithDict:@{@"id" : @"buttonId"}] ] - defaultColor:nil - defaultDisabledColor:nil - animated:NO]; - XCTAssertTrue(_viewController.navigationItem.leftBarButtonItems.count == 0); - - [_uut applyRightButtons:@[ [self buttonWithDict:@{@"id" : @"buttonId"}] ] - defaultColor:nil - defaultDisabledColor:nil - animated:NO]; - XCTAssertTrue(_viewController.navigationItem.rightBarButtonItems.count == 0); + [_uut applyLeftButtons:@[ [self buttonWithDict:@{@"id" : @"buttonId"}] ] + defaultColor:nil + defaultDisabledColor:nil + animated:NO]; + XCTAssertTrue(_viewController.navigationItem.leftBarButtonItems.count == 0); + + [_uut applyRightButtons:@[ [self buttonWithDict:@{@"id" : @"buttonId"}] ] + defaultColor:nil + defaultDisabledColor:nil + animated:NO]; + XCTAssertTrue(_viewController.navigationItem.rightBarButtonItems.count == 0); } - (void)testApplyButtons_shouldAddButtonWithTitle { - [_uut applyLeftButtons:@[ [self buttonWithDict:@{@"id" : @"buttonId", @"text" : @"title"}] ] - defaultColor:nil - defaultDisabledColor:nil - animated:NO]; - XCTAssertTrue(_viewController.navigationItem.leftBarButtonItems.count == 1); + [_uut applyLeftButtons:@[ [self buttonWithDict:@{@"id" : @"buttonId", @"text" : @"title"}] ] + defaultColor:nil + defaultDisabledColor:nil + animated:NO]; + XCTAssertTrue(_viewController.navigationItem.leftBarButtonItems.count == 1); + + [_uut applyRightButtons:@[ [self buttonWithDict:@{@"id" : @"buttonId", @"text" : @"title"}] ] + defaultColor:nil + defaultDisabledColor:nil + animated:NO]; + XCTAssertTrue(_viewController.navigationItem.rightBarButtonItems.count == 1); +} + +- (void)testApplyButtonsOnIOS26_shouldSeparateMultipleLeftButtonsWithFixedSpaceItems { + if (@available(iOS 26.0, *)) { + [_uut applyLeftButtons:@[ + [self buttonWithDict:@{@"id" : @"buttonId1", @"text" : @"title1"}], + [self buttonWithDict:@{@"id" : @"buttonId2", @"text" : @"title2"}] + ] + defaultColor:nil + defaultDisabledColor:nil + animated:NO]; + + NSArray *items = _viewController.navigationItem.leftBarButtonItems; + [self assertSeparatedBarButtonItems:items]; + } +} + +- (void)testApplyButtonsOnIOS26_shouldSeparateMultipleRightButtonsWithFixedSpaceItems { + if (@available(iOS 26.0, *)) { + [_uut applyRightButtons:@[ + [self buttonWithDict:@{@"id" : @"buttonId1", @"text" : @"title1"}], + [self buttonWithDict:@{@"id" : @"buttonId2", @"text" : @"title2"}] + ] + defaultColor:nil + defaultDisabledColor:nil + animated:NO]; + + NSArray *items = _viewController.navigationItem.rightBarButtonItems; + [self assertSeparatedBarButtonItems:items]; + } +} - [_uut applyRightButtons:@[ [self buttonWithDict:@{@"id" : @"buttonId", @"text" : @"title"}] ] - defaultColor:nil - defaultDisabledColor:nil - animated:NO]; - XCTAssertTrue(_viewController.navigationItem.rightBarButtonItems.count == 1); +- (void)testApplyMultipleLeftButtonsOnIOS26_shouldSkipFixedSpaceItemsWhenApplyingColor { + if (@available(iOS 26.0, *)) { + [_uut applyLeftButtons:@[ + [self buttonWithDict:@{@"id" : @"buttonId1", @"text" : @"title1"}], + [self buttonWithDict:@{@"id" : @"buttonId2", @"text" : @"title2"}] + ] + defaultColor:nil + defaultDisabledColor:nil + animated:NO]; + + XCTAssertNoThrow([_uut applyLeftButtonsColor:[Color withValue:UIColor.redColor]]); + NSArray *items = _viewController.navigationItem.leftBarButtonItems; + XCTAssertEqual(items[0].tintColor, UIColor.redColor); + XCTAssertEqual(items[2].tintColor, UIColor.redColor); + } +} + +- (void)testApplyMultipleRightButtonsOnIOS26_shouldSkipFixedSpaceItemsWhenApplyingBackgroundColor { + if (@available(iOS 26.0, *)) { + RNNButtonOptions *firstButton = [self buttonWithDict:@{@"id" : @"buttonId1"}]; + firstButton.icon = [Image withValue:createTestImage()]; + firstButton.iconBackground.color = [Color withValue:UIColor.blackColor]; + RNNButtonOptions *secondButton = [self buttonWithDict:@{@"id" : @"buttonId2"}]; + secondButton.icon = [Image withValue:createTestImage()]; + secondButton.iconBackground.color = [Color withValue:UIColor.blackColor]; + + [_uut applyRightButtons:@[ firstButton, secondButton ] + defaultColor:nil + defaultDisabledColor:nil + animated:NO]; + + XCTAssertNoThrow( + [_uut applyRightButtonsBackgroundColor:[Color withValue:UIColor.redColor]]); + } +} + +- (void)testApplyMultipleButtonsBeforeIOS26_shouldNotAddFixedSpaceItems { + if (@available(iOS 26.0, *)) { + return; + } + + [_uut applyLeftButtons:@[ + [self buttonWithDict:@{@"id" : @"buttonId1", @"text" : @"title1"}], + [self buttonWithDict:@{@"id" : @"buttonId2", @"text" : @"title2"}] + ] + defaultColor:nil + defaultDisabledColor:nil + animated:NO]; + + XCTAssertEqual(_viewController.navigationItem.leftBarButtonItems.count, 2); + XCTAssertTrue([_viewController.navigationItem.leftBarButtonItems[0] + isKindOfClass:RNNUIBarButtonItem.class]); + XCTAssertTrue([_viewController.navigationItem.leftBarButtonItems[1] + isKindOfClass:RNNUIBarButtonItem.class]); } - (void)testApplyButtons_shouldCreateCustomButtonView { - RNNButtonOptions *button = [self buttonWithDict:@{@"id" : @"buttonId"}]; - button.icon = [Image withValue:createTestImage()]; - button.iconBackground.color = [Color withValue:UIColor.blackColor]; - [_uut applyLeftButtons:@[ button ] defaultColor:nil defaultDisabledColor:nil animated:NO]; - XCTAssertNotNil([_viewController.navigationItem.leftBarButtonItems.lastObject customView]); + RNNButtonOptions *button = [self buttonWithDict:@{@"id" : @"buttonId"}]; + button.icon = [Image withValue:createTestImage()]; + button.iconBackground.color = [Color withValue:UIColor.blackColor]; + [_uut applyLeftButtons:@[ button ] defaultColor:nil defaultDisabledColor:nil animated:NO]; + XCTAssertNotNil([_viewController.navigationItem.leftBarButtonItems.lastObject customView]); } - (void)testApplyLeftButtonColor_shouldApplyTintColor { - RNNButtonOptions *button = [self buttonWithDict:@{@"id" : @"buttonId"}]; - button.icon = [Image withValue:[UIImage new]]; - [_uut applyLeftButtons:@[ button ] defaultColor:nil defaultDisabledColor:nil animated:NO]; - [_uut applyLeftButtonsColor:[Color withValue:UIColor.redColor]]; - XCTAssertEqual(_viewController.navigationItem.leftBarButtonItems.firstObject.tintColor, - UIColor.redColor); + RNNButtonOptions *button = [self buttonWithDict:@{@"id" : @"buttonId"}]; + button.icon = [Image withValue:[UIImage new]]; + [_uut applyLeftButtons:@[ button ] defaultColor:nil defaultDisabledColor:nil animated:NO]; + [_uut applyLeftButtonsColor:[Color withValue:UIColor.redColor]]; + XCTAssertEqual(_viewController.navigationItem.leftBarButtonItems.firstObject.tintColor, + UIColor.redColor); } - (void)testApplyLeftButtonColor_shouldApplyTextAttributesColor { - RNNButtonOptions *button = [self buttonWithDict:@{@"id" : @"buttonId", @"text" : @"title"}]; - [_uut applyLeftButtons:@[ button ] defaultColor:nil defaultDisabledColor:nil animated:NO]; - [_uut applyLeftButtonsColor:[Color withValue:UIColor.redColor]]; - XCTAssertEqual([[_viewController.navigationItem.leftBarButtonItems.firstObject - titleTextAttributesForState:UIControlStateNormal] - valueForKey:NSForegroundColorAttributeName], - UIColor.redColor); - XCTAssertEqual([[_viewController.navigationItem.leftBarButtonItems.firstObject - titleTextAttributesForState:UIControlStateHighlighted] - valueForKey:NSForegroundColorAttributeName], - UIColor.redColor); + RNNButtonOptions *button = [self buttonWithDict:@{@"id" : @"buttonId", @"text" : @"title"}]; + [_uut applyLeftButtons:@[ button ] defaultColor:nil defaultDisabledColor:nil animated:NO]; + [_uut applyLeftButtonsColor:[Color withValue:UIColor.redColor]]; + XCTAssertEqual([[_viewController.navigationItem.leftBarButtonItems.firstObject + titleTextAttributesForState:UIControlStateNormal] + valueForKey:NSForegroundColorAttributeName], + UIColor.redColor); + XCTAssertEqual([[_viewController.navigationItem.leftBarButtonItems.firstObject + titleTextAttributesForState:UIControlStateHighlighted] + valueForKey:NSForegroundColorAttributeName], + UIColor.redColor); } - (void)testApplyRightButtonColor_shouldApplyTintColor { - RNNButtonOptions *button = [self buttonWithDict:@{@"id" : @"buttonId"}]; - button.icon = [Image withValue:[UIImage new]]; - [_uut applyRightButtons:@[ button ] defaultColor:nil defaultDisabledColor:nil animated:NO]; - [_uut applyRightButtonsColor:[Color withValue:UIColor.redColor]]; - XCTAssertEqual(_viewController.navigationItem.rightBarButtonItems.firstObject.tintColor, - UIColor.redColor); + RNNButtonOptions *button = [self buttonWithDict:@{@"id" : @"buttonId"}]; + button.icon = [Image withValue:[UIImage new]]; + [_uut applyRightButtons:@[ button ] defaultColor:nil defaultDisabledColor:nil animated:NO]; + [_uut applyRightButtonsColor:[Color withValue:UIColor.redColor]]; + XCTAssertEqual(_viewController.navigationItem.rightBarButtonItems.firstObject.tintColor, + UIColor.redColor); } - (void)testApplyRightButtonColor_shouldApplyTextAttributesColor { - RNNButtonOptions *button = [self buttonWithDict:@{@"id" : @"buttonId", @"text" : @"title"}]; - [_uut applyRightButtons:@[ button ] defaultColor:nil defaultDisabledColor:nil animated:NO]; - [_uut applyRightButtonsColor:[Color withValue:UIColor.redColor]]; - XCTAssertEqual([[_viewController.navigationItem.rightBarButtonItems.firstObject - titleTextAttributesForState:UIControlStateNormal] - valueForKey:NSForegroundColorAttributeName], - UIColor.redColor); - XCTAssertEqual([[_viewController.navigationItem.rightBarButtonItems.firstObject - titleTextAttributesForState:UIControlStateHighlighted] - valueForKey:NSForegroundColorAttributeName], - UIColor.redColor); + RNNButtonOptions *button = [self buttonWithDict:@{@"id" : @"buttonId", @"text" : @"title"}]; + [_uut applyRightButtons:@[ button ] defaultColor:nil defaultDisabledColor:nil animated:NO]; + [_uut applyRightButtonsColor:[Color withValue:UIColor.redColor]]; + XCTAssertEqual([[_viewController.navigationItem.rightBarButtonItems.firstObject + titleTextAttributesForState:UIControlStateNormal] + valueForKey:NSForegroundColorAttributeName], + UIColor.redColor); + XCTAssertEqual([[_viewController.navigationItem.rightBarButtonItems.firstObject + titleTextAttributesForState:UIControlStateHighlighted] + valueForKey:NSForegroundColorAttributeName], + UIColor.redColor); } - (RNNButtonOptions *)buttonWithDict:(NSDictionary *)buttonDict { - return [[RNNButtonOptions alloc] initWithDict:buttonDict]; + return [[RNNButtonOptions alloc] initWithDict:buttonDict]; +} + +- (void)assertSeparatedBarButtonItems:(NSArray *)items { + XCTAssertEqual(items.count, 3); + XCTAssertTrue([items[0] isKindOfClass:RNNUIBarButtonItem.class]); + XCTAssertFalse([items[1] isKindOfClass:RNNUIBarButtonItem.class]); + XCTAssertEqual(items[1].width, 0); + XCTAssertTrue([items[2] isKindOfClass:RNNUIBarButtonItem.class]); } @end