From 0e7375ae3656b54d4fb52a657db0d1ad1455bcba Mon Sep 17 00:00:00 2001 From: Douglas Lowder Date: Thu, 10 Aug 2017 05:26:04 -0700 Subject: [PATCH] Apple TV: RCTTabBar selection controlled by native after render (fix #15081) Summary: **Motivation** Fix flickering in TabBarIOS on Apple TV... issue #15081 After this change, on Apple TV, TabBarIOS item selections will be controlled purely from the native side after initial render with the `selected` prop. This is necessary because the `UITabBar` implementation in tvOS moves the selection before calling `shouldSelectViewController:`; this issue does not occur on iOS. **Test plan** Existing CI should still pass. Issue is resolved when testing the example code in #15081 . Closes https://github.com/facebook/react-native/pull/15220 Differential Revision: D5601671 Pulled By: javache fbshipit-source-id: c18e7d3482d6c07d534ff40a443a6f642d4267bb --- React/Views/RCTTabBar.m | 22 ++++++++++++++++++++++ React/Views/RCTTabBarItem.h | 4 ++++ React/Views/RCTTabBarItem.m | 15 +++++++++++++++ docs/BuildingForAppleTV.md | 2 ++ 4 files changed, 43 insertions(+) diff --git a/React/Views/RCTTabBar.m b/React/Views/RCTTabBar.m index 28e623c40..b9e186f0b 100644 --- a/React/Views/RCTTabBar.m +++ b/React/Views/RCTTabBar.m @@ -115,9 +115,17 @@ RCT_NOT_IMPLEMENTED(- (instancetype)initWithCoder:(NSCoder *)aDecoder) [tab.barItem setTitleTextAttributes:@{NSForegroundColorAttributeName: self.tintColor} forState:UIControlStateSelected]; controller.tabBarItem = tab.barItem; +#if TARGET_OS_TV +// On Apple TV, disable JS control of selection after initial render + if (tab.selected && !tab.wasSelectedInJS) { + self->_tabController.selectedViewController = controller; + } + tab.wasSelectedInJS = YES; +#else if (tab.selected) { self->_tabController.selectedViewController = controller; } +#endif }]; } @@ -175,6 +183,18 @@ RCT_NOT_IMPLEMENTED(- (instancetype)initWithCoder:(NSCoder *)aDecoder) #pragma mark - UITabBarControllerDelegate +#if TARGET_OS_TV + +- (void)tabBarController:(UITabBarController *)tabBarController didSelectViewController:(nonnull UIViewController *)viewController +{ + NSUInteger index = [tabBarController.viewControllers indexOfObject:viewController]; + RCTTabBarItem *tab = (RCTTabBarItem *)self.reactSubviews[index]; + if (tab.onPress) tab.onPress(nil); + return; +} + +#else + - (BOOL)tabBarController:(UITabBarController *)tabBarController shouldSelectViewController:(UIViewController *)viewController { NSUInteger index = [tabBarController.viewControllers indexOfObject:viewController]; @@ -183,6 +203,8 @@ RCT_NOT_IMPLEMENTED(- (instancetype)initWithCoder:(NSCoder *)aDecoder) return NO; } +#endif + #if TARGET_OS_TV - (BOOL)isUserInteractionEnabled diff --git a/React/Views/RCTTabBarItem.h b/React/Views/RCTTabBarItem.h index 7d880a580..19f7af36c 100644 --- a/React/Views/RCTTabBarItem.h +++ b/React/Views/RCTTabBarItem.h @@ -29,4 +29,8 @@ @property (nonatomic, readonly) UITabBarItem *barItem; @property (nonatomic, copy) RCTBubblingEventBlock onPress; +#if TARGET_OS_TV +@property (nonatomic, assign) BOOL wasSelectedInJS; +#endif + @end diff --git a/React/Views/RCTTabBarItem.m b/React/Views/RCTTabBarItem.m index e3c530785..bfb1a531f 100644 --- a/React/Views/RCTTabBarItem.m +++ b/React/Views/RCTTabBarItem.m @@ -42,6 +42,9 @@ RCT_ENUM_CONVERTER(UITabBarSystemItem, (@{ { if ((self = [super initWithFrame:frame])) { _systemIcon = NSNotFound; +#if TARGET_OS_TV + _wasSelectedInJS = NO; +#endif } return self; } @@ -118,4 +121,16 @@ RCT_ENUM_CONVERTER(UITabBarSystemItem, (@{ return self.superview.reactViewController; } +#if TARGET_OS_TV + +// On Apple TV, we let native control the tab bar selection after initial render +- (void)setSelected:(BOOL)selected +{ + if (!_wasSelectedInJS) { + _selected = selected; + } +} + +#endif + @end diff --git a/docs/BuildingForAppleTV.md b/docs/BuildingForAppleTV.md index 08f0cf996..0dc6128cb 100644 --- a/docs/BuildingForAppleTV.md +++ b/docs/BuildingForAppleTV.md @@ -89,6 +89,8 @@ class Game2048 extends React.Component { - *Back navigation with the TV remote menu button*: The `BackHandler` component, originally written to support the Android back button, now also supports back navigation on the Apple TV using the menu button on the TV remote. +- *TabBarIOS behavior*: The `TabBarIOS` component wraps the native `UITabBar` API, which works differently on Apple TV. To avoid jittery rerendering of the tab bar in tvOS (see [this issue](https://github.com/facebook/react-native/issues/15081)), the selected tab bar item can only be set from Javascript on initial render, and is controlled after that by the user through native code. + - *Known issues*: - [ListView scrolling](https://github.com/facebook/react-native/issues/12793). The issue can be easily worked around by setting `removeClippedSubviews` to false in ListView and similar components. For more discussion of this issue, see [this PR](https://github.com/facebook/react-native/pull/12944).