#import "RNSBottomTabsScreenComponentView.h" #import "NSString+RNSUtility.h" #import "RNSConversions.h" #import "RNSDefines.h" #import "RNSLog.h" #import "RNSScrollViewHelper.h" #import "RNSTabBarAppearanceCoordinator.h" #import "RNSTabBarController.h" #if RCT_NEW_ARCH_ENABLED #import #import #import #import #import #import #endif // RCT_NEW_ARCH_ENABLED #if RCT_NEW_ARCH_ENABLED namespace react = facebook::react; #endif // RCT_NEW_ARCH_ENABLED #pragma mark - View implementation @implementation RNSBottomTabsScreenComponentView { RNSTabsScreenViewController *_controller; RNSBottomTabsHostComponentView *__weak _Nullable _reactSuperview; RNSBottomTabsScreenEventEmitter *_Nonnull _reactEventEmitter; // We need this information to warn users about dynamic changes to behavior being currently unsupported. BOOL _isOverrideScrollViewContentInsetAdjustmentBehaviorSet; #if !RCT_NEW_ARCH_ENABLED BOOL _tabItemNeedsAppearanceUpdate; BOOL _tabScreenOrientationNeedsUpdate; BOOL _tabBarItemNeedsUpdate; #endif // !RCT_NEW_ARCH_ENABLED } - (instancetype)initWithFrame:(CGRect)frame { if (self = [super initWithFrame:frame]) { [self initState]; } return self; } - (void)initState { #if RCT_NEW_ARCH_ENABLED static const auto defaultProps = std::make_shared(); _props = defaultProps; #endif // RCT_NEW_ARCH_ENABLED _controller = [RNSTabsScreenViewController new]; _controller.view = self; _reactSuperview = nil; _reactEventEmitter = [RNSBottomTabsScreenEventEmitter new]; #if !RCT_NEW_ARCH_ENABLED _tabItemNeedsAppearanceUpdate = NO; _tabScreenOrientationNeedsUpdate = NO; _tabBarItemNeedsUpdate = NO; #endif // This is a temporary workaround to avoid UIScrollEdgeEffect glitch // when changing tabs when ScrollView is present. // TODO: don't hardcode color here self.backgroundColor = [UIColor whiteColor]; [self resetProps]; } - (void)resetProps { _isSelectedScreen = NO; _badgeValue = nil; _title = nil; _orientation = RNSOrientationInherit; _standardAppearance = [UITabBarAppearance new]; _scrollEdgeAppearance = nil; _shouldUseRepeatedTabSelectionPopToRootSpecialEffect = YES; _shouldUseRepeatedTabSelectionScrollToTopSpecialEffect = YES; _overrideScrollViewContentInsetAdjustmentBehavior = YES; _isOverrideScrollViewContentInsetAdjustmentBehaviorSet = NO; _iconType = RNSBottomTabsIconTypeSfSymbol; _iconImageSource = nil; _iconSfSymbolName = nil; _selectedIconImageSource = nil; _selectedIconSfSymbolName = nil; _systemItem = RNSBottomTabsScreenSystemItemNone; } RNS_IGNORE_SUPER_CALL_BEGIN - (nullable RNSBottomTabsHostComponentView *)reactSuperview { return _reactSuperview; } RNS_IGNORE_SUPER_CALL_END #ifdef RCT_NEW_ARCH_ENABLED #pragma mark - RNSViewControllerInvalidating - (void)invalidateController { _controller = nil; } #else #pragma mark - RCTInvalidating - (void)invalidate { _controller = nil; } #endif #pragma mark - Events - (nonnull RNSBottomTabsScreenEventEmitter *)reactEventEmitter { RCTAssert(_reactEventEmitter != nil, @"[RNScreens] Attempt to access uninitialized _reactEventEmitter"); return _reactEventEmitter; } - (nullable RNSTabBarController *)findTabBarController { return static_cast(_controller.tabBarController); } #pragma mark - RNSScrollViewBehaviorOverriding - (BOOL)shouldOverrideScrollViewContentInsetAdjustmentBehavior { return self.overrideScrollViewContentInsetAdjustmentBehavior; } - (void)overrideScrollViewBehaviorInFirstDescendantChainIfNeeded { if ([self shouldOverrideScrollViewContentInsetAdjustmentBehavior]) { [RNSScrollViewHelper overrideScrollViewBehaviorInFirstDescendantChainFrom:self]; } } #pragma mark - Prop update utils - (void)updateTabBarItem { UITabBarItem *tabBarItem = nil; if (_systemItem != RNSBottomTabsScreenSystemItemNone) { UITabBarSystemItem systemItem = rnscreens::conversion::RNSBottomTabsScreenSystemItemToUITabBarSystemItem(_systemItem); tabBarItem = [[UITabBarItem alloc] initWithTabBarSystemItem:systemItem tag:0]; } else { tabBarItem = [[UITabBarItem alloc] init]; tabBarItem.title = _title; } tabBarItem.badgeValue = _badgeValue; _controller.tabBarItem = tabBarItem; } #if RCT_NEW_ARCH_ENABLED #pragma mark - RCTViewComponentViewProtocol - (void)updateProps:(const facebook::react::Props::Shared &)props oldProps:(const facebook::react::Props::Shared &)oldProps { const auto &oldComponentProps = *std::static_pointer_cast(_props); const auto &newComponentProps = *std::static_pointer_cast(props); bool tabItemNeedsAppearanceUpdate{false}; bool tabScreenOrientationNeedsUpdate{false}; bool tabBarItemNeedsUpdate{false}; if (newComponentProps.title != oldComponentProps.title) { _title = RCTNSStringFromStringNilIfEmpty(newComponentProps.title); _controller.title = _title; } if (newComponentProps.orientation != oldComponentProps.orientation) { _orientation = rnscreens::conversion::RNSOrientationFromRNSBottomTabsScreenOrientation(newComponentProps.orientation); tabScreenOrientationNeedsUpdate = YES; } if (newComponentProps.tabKey != oldComponentProps.tabKey) { RCTAssert(!newComponentProps.tabKey.empty(), @"[RNScreens] tabKey must not be empty!"); _tabKey = RCTNSStringFromString(newComponentProps.tabKey); } if (newComponentProps.isFocused != oldComponentProps.isFocused) { _isSelectedScreen = newComponentProps.isFocused; [_controller tabScreenFocusHasChanged]; } if (newComponentProps.badgeValue != oldComponentProps.badgeValue) { _badgeValue = RCTNSStringFromStringNilIfEmpty(newComponentProps.badgeValue); tabBarItemNeedsUpdate = YES; } if (newComponentProps.standardAppearance != oldComponentProps.standardAppearance) { _standardAppearance = [UITabBarAppearance new]; [RNSTabBarAppearanceCoordinator configureTabBarAppearance:_standardAppearance fromAppearanceProps:rnscreens::conversion::RNSConvertFollyDynamicToId( newComponentProps.standardAppearance)]; tabItemNeedsAppearanceUpdate = YES; } if (newComponentProps.scrollEdgeAppearance != oldComponentProps.scrollEdgeAppearance) { if (newComponentProps.scrollEdgeAppearance.type() == folly::dynamic::OBJECT) { _scrollEdgeAppearance = [UITabBarAppearance new]; [RNSTabBarAppearanceCoordinator configureTabBarAppearance:_scrollEdgeAppearance fromAppearanceProps:rnscreens::conversion::RNSConvertFollyDynamicToId( newComponentProps.scrollEdgeAppearance)]; } else { _scrollEdgeAppearance = nil; } tabItemNeedsAppearanceUpdate = YES; } if (newComponentProps.iconType != oldComponentProps.iconType) { _iconType = rnscreens::conversion::RNSBottomTabsIconTypeFromIcon(newComponentProps.iconType); tabItemNeedsAppearanceUpdate = YES; tabBarItemNeedsUpdate = YES; } if (newComponentProps.iconImageSource != oldComponentProps.iconImageSource) { _iconImageSource = rnscreens::conversion::RCTImageSourceFromImageSourceAndIconType(&newComponentProps.iconImageSource, _iconType); tabItemNeedsAppearanceUpdate = YES; tabBarItemNeedsUpdate = YES; } if (newComponentProps.iconSfSymbolName != oldComponentProps.iconSfSymbolName) { _iconSfSymbolName = RCTNSStringFromStringNilIfEmpty(newComponentProps.iconSfSymbolName); tabItemNeedsAppearanceUpdate = YES; tabBarItemNeedsUpdate = YES; } if (newComponentProps.selectedIconImageSource != oldComponentProps.selectedIconImageSource) { _selectedIconImageSource = rnscreens::conversion::RCTImageSourceFromImageSourceAndIconType( &newComponentProps.selectedIconImageSource, _iconType); tabItemNeedsAppearanceUpdate = YES; tabBarItemNeedsUpdate = YES; } if (newComponentProps.selectedIconSfSymbolName != oldComponentProps.selectedIconSfSymbolName) { _selectedIconSfSymbolName = RCTNSStringFromStringNilIfEmpty(newComponentProps.selectedIconSfSymbolName); tabItemNeedsAppearanceUpdate = YES; tabBarItemNeedsUpdate = YES; } if (newComponentProps.specialEffects.repeatedTabSelection.popToRoot != oldComponentProps.specialEffects.repeatedTabSelection.popToRoot) { _shouldUseRepeatedTabSelectionPopToRootSpecialEffect = newComponentProps.specialEffects.repeatedTabSelection.popToRoot; } if (newComponentProps.specialEffects.repeatedTabSelection.scrollToTop != oldComponentProps.specialEffects.repeatedTabSelection.scrollToTop) { _shouldUseRepeatedTabSelectionScrollToTopSpecialEffect = newComponentProps.specialEffects.repeatedTabSelection.scrollToTop; } if (newComponentProps.overrideScrollViewContentInsetAdjustmentBehavior != oldComponentProps.overrideScrollViewContentInsetAdjustmentBehavior) { _overrideScrollViewContentInsetAdjustmentBehavior = newComponentProps.overrideScrollViewContentInsetAdjustmentBehavior; if (_isOverrideScrollViewContentInsetAdjustmentBehaviorSet) { RCTLogWarn( @"[RNScreens] changing overrideScrollViewContentInsetAdjustmentBehavior dynamically is currently unsupported"); } } // This flag is set to YES when overrideScrollViewContentInsetAdjustmentBehavior prop // is assigned for the first time. This allows us to identify any subsequent changes to this prop, // enabling us to warn users that dynamic changes are not supported. _isOverrideScrollViewContentInsetAdjustmentBehaviorSet = YES; if (newComponentProps.systemItem != oldComponentProps.systemItem) { _systemItem = rnscreens::conversion::RNSBottomTabsScreenSystemItemFromReactRNSBottomTabsScreenSystemItem( newComponentProps.systemItem); tabBarItemNeedsUpdate = YES; } if (tabBarItemNeedsUpdate) { [self updateTabBarItem]; // Force appearance update to make sure correct image for tab bar item is used tabItemNeedsAppearanceUpdate = YES; } if (tabItemNeedsAppearanceUpdate) { [_controller tabItemAppearanceHasChanged]; } if (tabScreenOrientationNeedsUpdate) { [_controller tabScreenOrientationHasChanged]; } [super updateProps:props oldProps:oldProps]; } - (void)updateLayoutMetrics:(const facebook::react::LayoutMetrics &)layoutMetrics oldLayoutMetrics:(const facebook::react::LayoutMetrics &)oldLayoutMetrics { RNSLog( @"TabScreen [%ld] updateLayoutMetrics: %@", self.tag, NSStringFromCGRect(RCTCGRectFromRect(layoutMetrics.frame))); [super updateLayoutMetrics:layoutMetrics oldLayoutMetrics:oldLayoutMetrics]; } - (void)updateEventEmitter:(const facebook::react::EventEmitter::Shared &)eventEmitter { [super updateEventEmitter:eventEmitter]; [_reactEventEmitter updateEventEmitter:std::static_pointer_cast(eventEmitter)]; } - (void)mountChildComponentView:(UIView *)childComponentView index:(NSInteger)index { RNSLog(@"TabScreen [%ld] mount [%ld] at %ld", self.tag, childComponentView.tag, index); [super mountChildComponentView:childComponentView index:index]; } - (void)unmountChildComponentView:(UIView *)childComponentView index:(NSInteger)index { RNSLog(@"TabScreen [%ld] unmount [%ld] from %ld", self.tag, childComponentView.tag, index); [super unmountChildComponentView:childComponentView index:index]; } + (react::ComponentDescriptorProvider)componentDescriptorProvider { return react::concreteComponentDescriptorProvider(); } + (BOOL)shouldBeRecycled { // There won't be tens of instances of this component usually & it's easier for now. // We could consider enabling it someday though. return NO; } #else #pragma mark - LEGACY RCTComponent protocol - (void)didSetProps:(NSArray *)changedProps { [super didSetProps:changedProps]; // This flag is set to YES when overrideScrollViewContentInsetAdjustmentBehavior prop // is assigned for the first time. This allows us to identify any subsequent changes to this prop, // enabling us to warn users that dynamic changes are not supported. // On Paper, setter for the prop may not be called (when it is undefined in JS). // Therefore we set the flag in didSetProps to make sure to handle this case as well. // didSetProps will always be called because tabKey prop is required. _isOverrideScrollViewContentInsetAdjustmentBehaviorSet = YES; if (_tabBarItemNeedsUpdate) { [self updateTabBarItem]; _tabBarItemNeedsUpdate = NO; // Force appearance update to make sure correct image for tab bar item is used _tabItemNeedsAppearanceUpdate = YES; } if (_tabItemNeedsAppearanceUpdate) { [_controller tabItemAppearanceHasChanged]; _tabItemNeedsAppearanceUpdate = NO; } if (_tabScreenOrientationNeedsUpdate) { [_controller tabScreenOrientationHasChanged]; _tabScreenOrientationNeedsUpdate = NO; } } #pragma mark - LEGACY prop setters - (void)setIsSelectedScreen:(BOOL)isSelectedScreen { if (_isSelectedScreen != isSelectedScreen) { _isSelectedScreen = isSelectedScreen; [_controller tabScreenFocusHasChanged]; } } - (void)setTabKey:(NSString *)tabKey { RCTAssert([NSString rnscreens_isBlankOrNull:tabKey] == NO, @"[RNScreens] tabKey must not be empty"); _tabKey = tabKey; } - (void)setTitle:(NSString *)title { _title = title; _controller.title = title; } - (void)setBadgeValue:(NSString *)badgeValue { _badgeValue = [NSString rnscreens_stringOrNilIfBlank:badgeValue]; _tabBarItemNeedsUpdate = YES; } - (void)setIconType:(RNSBottomTabsIconType)iconType { _iconType = iconType; _tabItemNeedsAppearanceUpdate = YES; _tabBarItemNeedsUpdate = YES; } - (void)setIconImageSource:(RCTImageSource *)iconImageSource { _iconImageSource = iconImageSource; _tabItemNeedsAppearanceUpdate = YES; _tabBarItemNeedsUpdate = YES; } - (void)setIconSfSymbolName:(NSString *)iconSfSymbolName { _iconSfSymbolName = [NSString rnscreens_stringOrNilIfEmpty:iconSfSymbolName]; _tabItemNeedsAppearanceUpdate = YES; _tabBarItemNeedsUpdate = YES; } - (void)setSelectedIconImageSource:(RCTImageSource *)selectedIconImageSource { _selectedIconImageSource = selectedIconImageSource; _tabItemNeedsAppearanceUpdate = YES; _tabBarItemNeedsUpdate = YES; } - (void)setSelectedIconSfSymbolName:(NSString *)selectedIconSfSymbolName { _selectedIconSfSymbolName = [NSString rnscreens_stringOrNilIfEmpty:selectedIconSfSymbolName]; _tabItemNeedsAppearanceUpdate = YES; _tabBarItemNeedsUpdate = YES; } - (void)setOverrideScrollViewContentInsetAdjustmentBehavior:(BOOL)overrideScrollViewContentInsetAdjustmentBehavior { _overrideScrollViewContentInsetAdjustmentBehavior = overrideScrollViewContentInsetAdjustmentBehavior; if (_isOverrideScrollViewContentInsetAdjustmentBehaviorSet) { RCTLogWarn( @"[RNScreens] changing overrideScrollViewContentInsetAdjustmentBehavior dynamically is currently unsupported"); } // _isOverrideScrollViewContentInsetAdjustmentBehaviorSet flag is set in didSetProps to handle a case // when the prop is undefined in JS and default value is used instead of calling this setter. } - (void)setStandardAppearance:(NSDictionary *)standardAppearanceProps { _standardAppearance = [UITabBarAppearance new]; if (standardAppearanceProps != nil) { [RNSTabBarAppearanceCoordinator configureTabBarAppearance:_standardAppearance fromAppearanceProps:standardAppearanceProps]; } _tabItemNeedsAppearanceUpdate = YES; } - (void)setScrollEdgeAppearance:(NSDictionary *)scrollEdgeAppearanceProps { if (scrollEdgeAppearanceProps != nil) { _scrollEdgeAppearance = [UITabBarAppearance new]; [RNSTabBarAppearanceCoordinator configureTabBarAppearance:_scrollEdgeAppearance fromAppearanceProps:scrollEdgeAppearanceProps]; } else { _scrollEdgeAppearance = nil; } _tabItemNeedsAppearanceUpdate = YES; } // This is a Paper-only setter method that will be called by the mounting code. // It allows us to store UITabBarMinimizeBehavior in the component while accepting a custom enum as input from JS. - (void)setSystemItem:(RNSBottomTabsScreenSystemItem)systemItem { _systemItem = systemItem; _tabBarItemNeedsUpdate = YES; } - (void)setOrientation:(RNSOrientation)orientation { _orientation = orientation; _tabScreenOrientationNeedsUpdate = YES; } - (void)setOnWillAppear:(RCTDirectEventBlock)onWillAppear { [self.reactEventEmitter setOnWillAppear:onWillAppear]; } - (void)setOnWillDisappear:(RCTDirectEventBlock)onWillDisappear { [self.reactEventEmitter setOnWillDisappear:onWillDisappear]; } - (void)setOnDidAppear:(RCTDirectEventBlock)onDidAppear { [self.reactEventEmitter setOnDidAppear:onDidAppear]; } - (void)setOnDidDisappear:(RCTDirectEventBlock)onDidDisappear { [self.reactEventEmitter setOnDidDisappear:onDidDisappear]; } #define RNS_FAILING_EVENT_GETTER(eventName) \ -(RCTDirectEventBlock)eventName \ { \ RCTAssert(NO, @"[RNScreens] Events should be emitted through reactEventEmitter"); \ return nil; \ } RNS_FAILING_EVENT_GETTER(onWillAppear); RNS_FAILING_EVENT_GETTER(onDidAppear); RNS_FAILING_EVENT_GETTER(onWillDisappear); RNS_FAILING_EVENT_GETTER(onDidDisappear); #undef RNS_FAILING_EVENT_GETTER #endif // RCT_NEW_ARCH_ENABLED @end #if RCT_NEW_ARCH_ENABLED #pragma mark - View class exposure Class RNSBottomTabsScreen(void) { return RNSBottomTabsScreenComponentView.class; } #endif // RCT_NEW_ARCH_ENABLED