diff --git a/ios/RNNBottomTabsController.mm b/ios/RNNBottomTabsController.mm index 7f0a944d50..3134bc63f9 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 806a647cc5..fb50bc194f 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 265841c4d6..8d8c5004ad 100644 --- a/ios/UITabBarController+RNNOptions.mm +++ b/ios/UITabBarController+RNNOptions.mm @@ -1,9 +1,85 @@ #import "RNNBottomTabsController.h" #import "UITabBar+utils.h" #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]; +} + +- (void)rnn_setTabBarTestIDRetryScheduled:(BOOL)scheduled { + objc_setAssociatedObject(self, RNNTabBarTestIDRetryScheduledKey, @(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; + + for (NSUInteger tabIndex = 0; tabIndex < items.count; tabIndex++) { + UITabBarItem *item = items[tabIndex]; + NSString *testID = item.accessibilityIdentifier; + 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; + } + } + + return appliedAllKnownTestIDs; +} + - (void)setCurrentTabIndex:(NSUInteger)currentTabIndex { [self setSelectedIndex:currentTabIndex]; } @@ -16,6 +92,38 @@ - (void)setTabBarTestID:(NSString *)testID { self.tabBar.accessibilityIdentifier = 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)(RNNTabBarTestIDRetryDelay * NSEC_PER_SEC)), + dispatch_get_main_queue(), ^{ + UITabBarController *controller = weakSelf; + if (!controller) + return; + + [controller rnn_setTabBarTestIDRetryScheduled:NO]; + [controller syncTabBarItemTestIDs]; + }); +} + - (void)setTabBarStyle:(UIBarStyle)barStyle { self.tabBar.barStyle = barStyle; }