From cb030e2f1fc811517537cee44ead3002fe6891d9 Mon Sep 17 00:00:00 2001 From: Yedidya Kennard Date: Thu, 19 Mar 2026 18:34:26 +0200 Subject: [PATCH 1/4] Fix iOS 26 bottom tab test IDs --- ios/RNNBottomTabsController.mm | 6 +++++ ios/UITabBarController+RNNOptions.h | 2 ++ ios/UITabBarController+RNNOptions.mm | 38 ++++++++++++++++++++++++++++ 3 files changed, 46 insertions(+) diff --git a/ios/RNNBottomTabsController.mm b/ios/RNNBottomTabsController.mm index 7f0a944d508..3134bc63f9d 100644 --- a/ios/RNNBottomTabsController.mm +++ b/ios/RNNBottomTabsController.mm @@ -1,4 +1,5 @@ #import "RNNBottomTabsController.h" +#import "UITabBarController+RNNOptions.h" #import "UITabBarController+RNNUtils.h" @interface RNNBottomTabsController () @@ -100,6 +101,8 @@ - (void)createTabBarItems:(NSArray *)childViewControllers { for (UIViewController *child in childViewControllers) { [_bottomTabPresenter applyOptions:child.resolveOptions child:child]; } + + [self syncTabBarItemTestIDs]; } - (void)mergeChildOptions:(RNNNavigationOptions *)options child:(UIViewController *)child { @@ -111,6 +114,8 @@ - (void)mergeChildOptions:(RNNNavigationOptions *)options child:(UIViewControlle [_dotIndicatorPresenter mergeOptions:options resolvedOptions:childViewController.resolveOptions child:childViewController]; + + [self syncTabBarItemTestIDs]; } - (id)delegate { @@ -123,6 +128,7 @@ - (void)render { - (void)viewDidLayoutSubviews { [super viewDidLayoutSubviews]; + [self syncTabBarItemTestIDs]; [self.presenter viewDidLayoutSubviews]; [_dotIndicatorPresenter bottomTabsDidLayoutSubviews:self]; } diff --git a/ios/UITabBarController+RNNOptions.h b/ios/UITabBarController+RNNOptions.h index 806a647cc50..fb50bc194f9 100644 --- a/ios/UITabBarController+RNNOptions.h +++ b/ios/UITabBarController+RNNOptions.h @@ -20,4 +20,6 @@ - (void)hideTabBar:(BOOL)animated; +- (void)syncTabBarItemTestIDs; + @end diff --git a/ios/UITabBarController+RNNOptions.mm b/ios/UITabBarController+RNNOptions.mm index 265841c4d61..83a95981a9f 100644 --- a/ios/UITabBarController+RNNOptions.mm +++ b/ios/UITabBarController+RNNOptions.mm @@ -4,6 +4,29 @@ @implementation UITabBarController (RNNOptions) +- (void)rnn_applyTestID:(NSString *)testID toTabView:(UIView *)tabView { + tabView.accessibilityIdentifier = testID; + if (testID) + tabView.isAccessibilityElement = YES; +} + +- (BOOL)rnn_applyTabBarItemTestIDs { + NSArray *items = self.tabBar.items ?: @[]; + BOOL appliedAllKnownTestIDs = YES; + + for (NSUInteger tabIndex = 0; tabIndex < items.count; tabIndex++) { + UITabBarItem *item = items[tabIndex]; + UIView *tabView = [self.tabBar tabBarItemViewAtIndex:tabIndex]; + if (tabView) { + [self rnn_applyTestID:item.accessibilityIdentifier toTabView:tabView]; + } else if (item.accessibilityIdentifier) { + appliedAllKnownTestIDs = NO; + } + } + + return appliedAllKnownTestIDs; +} + - (void)setCurrentTabIndex:(NSUInteger)currentTabIndex { [self setSelectedIndex:currentTabIndex]; } @@ -16,6 +39,21 @@ - (void)setTabBarTestID:(NSString *)testID { self.tabBar.accessibilityIdentifier = testID; } +- (void)syncTabBarItemTestIDs { + if ([self rnn_applyTabBarItemTestIDs]) + return; + + __weak UITabBarController *weakSelf = self; + dispatch_after(dispatch_time(DISPATCH_TIME_NOW, (int64_t)(0.15 * NSEC_PER_SEC)), + dispatch_get_main_queue(), ^{ + UITabBarController *controller = weakSelf; + if (!controller) + return; + + [controller rnn_applyTabBarItemTestIDs]; + }); +} + - (void)setTabBarStyle:(UIBarStyle)barStyle { self.tabBar.barStyle = barStyle; } From 32c6b9eb37b712042a6a65f1e4be13419c7d5767 Mon Sep 17 00:00:00 2001 From: Yedidya Kennard Date: Thu, 19 Mar 2026 18:41:18 +0200 Subject: [PATCH 2/4] Address tab testID review feedback --- ios/UITabBarController+RNNOptions.mm | 24 +++++++++++++++++++++--- 1 file changed, 21 insertions(+), 3 deletions(-) diff --git a/ios/UITabBarController+RNNOptions.mm b/ios/UITabBarController+RNNOptions.mm index 83a95981a9f..f40f7a11139 100644 --- a/ios/UITabBarController+RNNOptions.mm +++ b/ios/UITabBarController+RNNOptions.mm @@ -1,13 +1,23 @@ #import "RNNBottomTabsController.h" #import "UITabBar+utils.h" #import "UITabBarController+RNNOptions.h" +#import + +static const void *RNNTabBarTestIDRetryScheduledKey = &RNNTabBarTestIDRetryScheduledKey; @implementation UITabBarController (RNNOptions) - (void)rnn_applyTestID:(NSString *)testID toTabView:(UIView *)tabView { tabView.accessibilityIdentifier = testID; - if (testID) - tabView.isAccessibilityElement = YES; +} + +- (BOOL)rnn_isTabBarTestIDRetryScheduled { + return [objc_getAssociatedObject(self, RNNTabBarTestIDRetryScheduledKey) boolValue]; +} + +- (void)rnn_setTabBarTestIDRetryScheduled:(BOOL)scheduled { + objc_setAssociatedObject(self, RNNTabBarTestIDRetryScheduledKey, @(scheduled), + OBJC_ASSOCIATION_RETAIN_NONATOMIC); } - (BOOL)rnn_applyTabBarItemTestIDs { @@ -40,8 +50,15 @@ - (void)setTabBarTestID:(NSString *)testID { } - (void)syncTabBarItemTestIDs { - if ([self rnn_applyTabBarItemTestIDs]) + if ([self rnn_applyTabBarItemTestIDs]) { + [self rnn_setTabBarTestIDRetryScheduled:NO]; return; + } + + if ([self rnn_isTabBarTestIDRetryScheduled]) + return; + + [self rnn_setTabBarTestIDRetryScheduled:YES]; __weak UITabBarController *weakSelf = self; dispatch_after(dispatch_time(DISPATCH_TIME_NOW, (int64_t)(0.15 * NSEC_PER_SEC)), @@ -50,6 +67,7 @@ - (void)syncTabBarItemTestIDs { if (!controller) return; + [controller rnn_setTabBarTestIDRetryScheduled:NO]; [controller rnn_applyTabBarItemTestIDs]; }); } From bc76c250a55ebb381fba9cdd9b96538ff81f0666 Mon Sep 17 00:00:00 2001 From: Yedidya Kennard Date: Thu, 19 Mar 2026 18:49:42 +0200 Subject: [PATCH 3/4] Preserve system tab accessibility identifiers --- ios/UITabBarController+RNNOptions.mm | 7 ++++--- 1 file changed, 4 insertions(+), 3 deletions(-) diff --git a/ios/UITabBarController+RNNOptions.mm b/ios/UITabBarController+RNNOptions.mm index f40f7a11139..d89f79c4715 100644 --- a/ios/UITabBarController+RNNOptions.mm +++ b/ios/UITabBarController+RNNOptions.mm @@ -26,10 +26,11 @@ - (BOOL)rnn_applyTabBarItemTestIDs { for (NSUInteger tabIndex = 0; tabIndex < items.count; tabIndex++) { UITabBarItem *item = items[tabIndex]; + NSString *testID = item.accessibilityIdentifier; UIView *tabView = [self.tabBar tabBarItemViewAtIndex:tabIndex]; - if (tabView) { - [self rnn_applyTestID:item.accessibilityIdentifier toTabView:tabView]; - } else if (item.accessibilityIdentifier) { + if (testID.length > 0 && tabView) { + [self rnn_applyTestID:testID toTabView:tabView]; + } else if (testID.length > 0) { appliedAllKnownTestIDs = NO; } } From a0f70d98a1dac693098d813dca7ab9d35d591544 Mon Sep 17 00:00:00 2001 From: Yedidya Kennard Date: Thu, 19 Mar 2026 18:59:33 +0200 Subject: [PATCH 4/4] Harden bottom tab testID syncing --- ios/UITabBarController+RNNOptions.mm | 55 +++++++++++++++++++++++++++- 1 file changed, 53 insertions(+), 2 deletions(-) diff --git a/ios/UITabBarController+RNNOptions.mm b/ios/UITabBarController+RNNOptions.mm index d89f79c4715..8d8c5004ada 100644 --- a/ios/UITabBarController+RNNOptions.mm +++ b/ios/UITabBarController+RNNOptions.mm @@ -3,14 +3,44 @@ #import "UITabBarController+RNNOptions.h" #import +static const NSTimeInterval RNNTabBarTestIDRetryDelay = 0.15; +static const NSUInteger RNNTabBarTestIDMaxRetryAttempts = 5; static const void *RNNTabBarTestIDRetryScheduledKey = &RNNTabBarTestIDRetryScheduledKey; +static const void *RNNTabBarTestIDRetryAttemptsKey = &RNNTabBarTestIDRetryAttemptsKey; +static const void *RNNOriginalTabBarViewAccessibilityIdentifierKey = + &RNNOriginalTabBarViewAccessibilityIdentifierKey; @implementation UITabBarController (RNNOptions) +- (void)rnn_storeOriginalAccessibilityIdentifierIfNeededForTabView:(UIView *)tabView { + if (objc_getAssociatedObject(tabView, RNNOriginalTabBarViewAccessibilityIdentifierKey)) + return; + + id originalAccessibilityIdentifier = tabView.accessibilityIdentifier ?: [NSNull null]; + objc_setAssociatedObject(tabView, RNNOriginalTabBarViewAccessibilityIdentifierKey, + originalAccessibilityIdentifier, + OBJC_ASSOCIATION_RETAIN_NONATOMIC); +} + - (void)rnn_applyTestID:(NSString *)testID toTabView:(UIView *)tabView { + [self rnn_storeOriginalAccessibilityIdentifierIfNeededForTabView:tabView]; tabView.accessibilityIdentifier = testID; } +- (void)rnn_restoreOriginalAccessibilityIdentifierForTabView:(UIView *)tabView { + id originalAccessibilityIdentifier = + objc_getAssociatedObject(tabView, RNNOriginalTabBarViewAccessibilityIdentifierKey); + if (!originalAccessibilityIdentifier) + return; + + tabView.accessibilityIdentifier = + [originalAccessibilityIdentifier isKindOfClass:NSNull.class] + ? nil + : originalAccessibilityIdentifier; + objc_setAssociatedObject(tabView, RNNOriginalTabBarViewAccessibilityIdentifierKey, nil, + OBJC_ASSOCIATION_RETAIN_NONATOMIC); +} + - (BOOL)rnn_isTabBarTestIDRetryScheduled { return [objc_getAssociatedObject(self, RNNTabBarTestIDRetryScheduledKey) boolValue]; } @@ -20,6 +50,16 @@ - (void)rnn_setTabBarTestIDRetryScheduled:(BOOL)scheduled { OBJC_ASSOCIATION_RETAIN_NONATOMIC); } +- (NSUInteger)rnn_tabBarTestIDRetryAttempts { + NSNumber *retryAttempts = objc_getAssociatedObject(self, RNNTabBarTestIDRetryAttemptsKey); + return retryAttempts.unsignedIntegerValue; +} + +- (void)rnn_setTabBarTestIDRetryAttempts:(NSUInteger)retryAttempts { + objc_setAssociatedObject(self, RNNTabBarTestIDRetryAttemptsKey, @(retryAttempts), + OBJC_ASSOCIATION_RETAIN_NONATOMIC); +} + - (BOOL)rnn_applyTabBarItemTestIDs { NSArray *items = self.tabBar.items ?: @[]; BOOL appliedAllKnownTestIDs = YES; @@ -30,6 +70,8 @@ - (BOOL)rnn_applyTabBarItemTestIDs { UIView *tabView = [self.tabBar tabBarItemViewAtIndex:tabIndex]; if (testID.length > 0 && tabView) { [self rnn_applyTestID:testID toTabView:tabView]; + } else if (tabView) { + [self rnn_restoreOriginalAccessibilityIdentifierForTabView:tabView]; } else if (testID.length > 0) { appliedAllKnownTestIDs = NO; } @@ -53,23 +95,32 @@ - (void)setTabBarTestID:(NSString *)testID { - (void)syncTabBarItemTestIDs { if ([self rnn_applyTabBarItemTestIDs]) { [self rnn_setTabBarTestIDRetryScheduled:NO]; + [self rnn_setTabBarTestIDRetryAttempts:0]; return; } if ([self rnn_isTabBarTestIDRetryScheduled]) return; + NSUInteger retryAttempts = [self rnn_tabBarTestIDRetryAttempts]; + if (retryAttempts >= RNNTabBarTestIDMaxRetryAttempts) { + [self rnn_setTabBarTestIDRetryAttempts:0]; + return; + } + [self rnn_setTabBarTestIDRetryScheduled:YES]; + [self rnn_setTabBarTestIDRetryAttempts:retryAttempts + 1]; __weak UITabBarController *weakSelf = self; - dispatch_after(dispatch_time(DISPATCH_TIME_NOW, (int64_t)(0.15 * NSEC_PER_SEC)), + dispatch_after(dispatch_time(DISPATCH_TIME_NOW, + (int64_t)(RNNTabBarTestIDRetryDelay * NSEC_PER_SEC)), dispatch_get_main_queue(), ^{ UITabBarController *controller = weakSelf; if (!controller) return; [controller rnn_setTabBarTestIDRetryScheduled:NO]; - [controller rnn_applyTabBarItemTestIDs]; + [controller syncTabBarItemTestIDs]; }); }