#import "RNSBottomTabsHostComponentView.h" #if RCT_NEW_ARCH_ENABLED #import #import #import #import #import #import #import #import #import "RNSBottomTabsHostComponentView+RNSImageLoader.h" #import "RNSInvalidatedComponentsRegistry.h" #import "RNSViewControllerInvalidator.h" #endif // RCT_NEW_ARCH_ENABLED #import "RNSBottomTabsScreenComponentView.h" #import "RNSConversions.h" #import "RNSConvert.h" #import "RNSDefines.h" #import "RNSLog.h" #import "RNSTabBarController.h" #import "RNSTabBarControllerDelegate.h" namespace react = facebook::react; #pragma mark - View implementation @interface RNSBottomTabsHostComponentView () #if RCT_NEW_ARCH_ENABLED #endif // RCT_NEW_ARCH_ENABLED @end @implementation RNSBottomTabsHostComponentView { RNSTabBarController *_Nonnull _controller; RNSTabBarControllerDelegate *_controllerDelegate; RNSBottomTabsHostEventEmitter *_Nonnull _reactEventEmitter; RCTImageLoader *_Nullable _imageLoader; #if RCT_NEW_ARCH_ENABLED RNSInvalidatedComponentsRegistry *_Nonnull _invalidatedComponentsRegistry; #endif // RCT_NEW_ARCH_ENABLED // RCTViewComponentView does not expose this field, therefore we maintain // it on our side. NSMutableArray *_reactSubviews; BOOL _hasModifiedReactSubviewsInCurrentTransaction; BOOL _needsTabBarAppearanceUpdate; } - (instancetype)initWithFrame:(CGRect)frame { if (self = [super initWithFrame:frame]) { [self initState]; } return self; } #if !RCT_NEW_ARCH_ENABLED - (instancetype)initWithFrame:(CGRect)frame reactImageLoader:(RCTImageLoader *)imageLoader { if (self = [self initWithFrame:frame]) { _imageLoader = imageLoader; } return self; } #endif // !RCT_NEW_ARCH_ENABLED - (nonnull RNSTabBarController *)controller { RCTAssert(_controller != nil, @"[RNScreens] Controller must not be nil"); return _controller; } - (void)initState { [self resetProps]; _controller = [[RNSTabBarController alloc] initWithTabsHostComponentView:self]; _controllerDelegate = [RNSTabBarControllerDelegate new]; _controller.delegate = _controllerDelegate; _reactSubviews = [NSMutableArray new]; _reactEventEmitter = [RNSBottomTabsHostEventEmitter new]; #if RCT_NEW_ARCH_ENABLED _invalidatedComponentsRegistry = [RNSInvalidatedComponentsRegistry new]; #endif // RCT_NEW_ARCH_ENABLED _hasModifiedReactSubviewsInCurrentTransaction = NO; _needsTabBarAppearanceUpdate = NO; } - (void)resetProps { #if RCT_NEW_ARCH_ENABLED static const auto defaultProps = std::make_shared(); _props = defaultProps; #endif _tabBarTintColor = nil; } #pragma mark - UIView methods - (void)willMoveToWindow:(UIWindow *)newWindow { #if RCT_NEW_ARCH_ENABLED if (newWindow == nil) { [_invalidatedComponentsRegistry flushInvalidViews]; } #endif // RCT_NEW_ARCH_ENABLED } - (void)didMoveToWindow { if ([self window] != nil) { [self reactAddControllerToClosestParent:_controller]; #if !RCT_NEW_ARCH_ENABLED // This is required on legacy architecture to prevent a bug with doubled size of UIViewControllerWrapperView. _controller.view.frame = self.bounds; #endif // !RCT_NEW_ARCH_ENABLED } } - (void)reactAddControllerToClosestParent:(UIViewController *)controller { if (!controller.parentViewController) { UIView *parentView = (UIView *)self.reactSuperview; while (parentView) { if (parentView.reactViewController) { [parentView.reactViewController addChildViewController:controller]; [self addSubview:controller.view]; [controller didMoveToParentViewController:parentView.reactViewController]; break; } parentView = (UIView *)parentView.reactSuperview; } return; } } #pragma mark - RNSScreenContainerDelegate - (void)updateContainer { NSMutableArray *tabControllers = [[NSMutableArray alloc] initWithCapacity:_reactSubviews.count]; for (RNSBottomTabsScreenComponentView *childView in _reactSubviews) { [tabControllers addObject:childView.controller]; } RNSLog(@"updateContainer: tabControllers: %@", tabControllers); [_controller childViewControllersHaveChangedTo:tabControllers]; } - (void)markChildUpdated { [self updateContainer]; } #if RCT_NEW_ARCH_ENABLED #pragma mark - RNSViewControllerInvalidating - (void)invalidateController { _controller = nil; } - (BOOL)shouldInvalidateOnMutation:(const facebook::react::ShadowViewMutation &)mutation { return (mutation.oldChildShadowView.tag == self.tag && mutation.type == facebook::react::ShadowViewMutation::Delete); } #else #pragma mark - RCTInvalidating - (void)invalidate { // We assume that bottom tabs host is removed from view hierarchy **only** when // whole component is destroyed & therefore we do the necessary cleanup here. // If at some point that statement does not hold anymore, this cleanup // should be moved to a different place. for (RNSBottomTabsScreenComponentView *subview in _reactSubviews) { [subview invalidate]; } _controller = nil; } #endif #pragma mark - React events - (nonnull RNSBottomTabsHostEventEmitter *)reactEventEmitter { RCTAssert(_reactEventEmitter != nil, @"[RNScreens] Attempt to access uninitialized _reactEventEmitter"); return _reactEventEmitter; } - (BOOL)emitOnNativeFocusChangeRequestSelectedTabScreen:(RNSBottomTabsScreenComponentView *)tabScreen { return [_reactEventEmitter emitOnNativeFocusChange:OnNativeFocusChangePayload{.tabKey = tabScreen.tabKey}]; } #pragma mark - RCTViewComponentViewProtocol #if RCT_NEW_ARCH_ENABLED - (void)mountChildComponentView:(UIView *)childComponentView index:(NSInteger)index { RCTAssert( [childComponentView isKindOfClass:RNSBottomTabsScreenComponentView.class], @"BottomTabsView only accepts children of type BottomTabScreen. Attempted to mount %@", childComponentView); auto *childScreen = static_cast(childComponentView); childScreen.reactSuperview = self; [_reactSubviews insertObject:childScreen atIndex:index]; _hasModifiedReactSubviewsInCurrentTransaction = YES; } - (void)unmountChildComponentView:(UIView *)childComponentView index:(NSInteger)index { RCTAssert( [childComponentView isKindOfClass:RNSBottomTabsScreenComponentView.class], @"BottomTabsView only accepts children of type BottomTabScreen. Attempted to unmount %@", childComponentView); auto *childScreen = static_cast(childComponentView); childScreen.reactSuperview = nil; [_reactSubviews removeObject:childScreen]; _hasModifiedReactSubviewsInCurrentTransaction = YES; } - (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); if (newComponentProps.controlNavigationStateInJS != oldComponentProps.controlNavigationStateInJS) { _experimental_controlNavigationStateInJS = newComponentProps.controlNavigationStateInJS; } if (newComponentProps.tabBarTintColor != oldComponentProps.tabBarTintColor) { _needsTabBarAppearanceUpdate = YES; _tabBarTintColor = RCTUIColorFromSharedColor(newComponentProps.tabBarTintColor); } if (newComponentProps.tabBarMinimizeBehavior != oldComponentProps.tabBarMinimizeBehavior) { #if RNS_IPHONE_OS_VERSION_AVAILABLE(26_0) if (@available(iOS 26.0, *)) { _tabBarMinimizeBehavior = rnscreens::conversion::UITabBarMinimizeBehaviorFromRNSBottomTabsTabBarMinimizeBehavior( newComponentProps.tabBarMinimizeBehavior); _controller.tabBarMinimizeBehavior = _tabBarMinimizeBehavior; } else #endif // Check for iOS >= 26 if (newComponentProps.tabBarMinimizeBehavior != react::RNSBottomTabsTabBarMinimizeBehavior::Automatic) { RCTLogWarn(@"[RNScreens] tabBarMinimizeBehavior is supported for iOS >= 26"); } } // Super call updates _props pointer. We should NOT update it before calling super. [super updateProps:props oldProps:oldProps]; } - (void)updateState:(const facebook::react::State::Shared &)state oldState:(const facebook::react::State::Shared &)oldState { react::RNSBottomTabsShadowNode::ConcreteState::Shared receivedState = std::static_pointer_cast(state); _imageLoader = [self retrieveImageLoaderFromState:receivedState]; } - (void)updateEventEmitter:(const facebook::react::EventEmitter::Shared &)eventEmitter { [super updateEventEmitter:eventEmitter]; const auto &castedEventEmitter = std::static_pointer_cast(eventEmitter); [_reactEventEmitter updateEventEmitter:castedEventEmitter]; } - (void)finalizeUpdates:(RNComponentViewUpdateMask)updateMask { if (_needsTabBarAppearanceUpdate) { _needsTabBarAppearanceUpdate = NO; [_controller setNeedsUpdateOfTabBarAppearance:true]; } [super finalizeUpdates:updateMask]; } + (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; } #pragma mark - RCTMountingTransactionObserving - (void)mountingTransactionWillMount:(const facebook::react::MountingTransaction &)transaction withSurfaceTelemetry:(const facebook::react::SurfaceTelemetry &)surfaceTelemetry { _hasModifiedReactSubviewsInCurrentTransaction = NO; [_controller reactMountingTransactionWillMount]; #if RCT_NEW_ARCH_ENABLED for (const auto &mutation : transaction.getMutations()) { if ([self shouldInvalidateOnMutation:mutation]) { for (RNSBottomTabsScreenComponentView *childView in _reactSubviews) { [RNSViewControllerInvalidator invalidateViewIfDetached:childView forRegistry:_invalidatedComponentsRegistry]; } [RNSViewControllerInvalidator invalidateViewIfDetached:self forRegistry:_invalidatedComponentsRegistry]; } } #endif // RCT_NEW_ARCH_ENABLED } - (void)mountingTransactionDidMount:(const facebook::react::MountingTransaction &)transaction withSurfaceTelemetry:(const facebook::react::SurfaceTelemetry &)surfaceTelemetry { if (_hasModifiedReactSubviewsInCurrentTransaction) { [self updateContainer]; } [_controller reactMountingTransactionDidMount]; } #else #pragma mark - LEGACY architecture implementation #pragma mark - LEGACY RCTComponent protocol RNS_IGNORE_SUPER_CALL_BEGIN - (void)insertReactSubview:(UIView *)subview atIndex:(NSInteger)index { RCTAssert( [subview isKindOfClass:RNSBottomTabsScreenComponentView.class], @"BottomTabsView only accepts children of type BottomTabScreen. Attempted to mount %@", subview); auto *childScreen = static_cast(subview); childScreen.reactSuperview = self; [_reactSubviews insertObject:childScreen atIndex:index]; } - (void)removeReactSubview:(UIView *)subview { RCTAssert( [subview isKindOfClass:RNSBottomTabsScreenComponentView.class], @"BottomTabsView only accepts children of type BottomTabScreen. Attempted to unmount %@", subview); auto *childScreen = static_cast(subview); childScreen.reactSuperview = nil; [_reactSubviews removeObject:childScreen]; } RNS_IGNORE_SUPER_CALL_END - (void)didUpdateReactSubviews { [super didUpdateReactSubviews]; _hasModifiedReactSubviewsInCurrentTransaction = YES; [self invalidateFlagsOnControllerIfNeeded]; } - (void)didSetProps:(NSArray *)changedProps { [super didSetProps:changedProps]; _needsTabBarAppearanceUpdate = YES; [self invalidateFlagsOnControllerIfNeeded]; } #pragma mark - LEGACY update methods - (void)invalidateFlagsOnControllerIfNeeded { if (_needsTabBarAppearanceUpdate) { _needsTabBarAppearanceUpdate = NO; [_controller setNeedsUpdateOfTabBarAppearance:true]; } if (_hasModifiedReactSubviewsInCurrentTransaction) { _hasModifiedReactSubviewsInCurrentTransaction = NO; [self updateContainer]; } } - (void)invalidateTabBarAppearance { _needsTabBarAppearanceUpdate = YES; [self invalidateFlagsOnControllerIfNeeded]; } #pragma mark - LEGACY prop setters // Paper will call property setters - (void)setTabBarTintColor:(UIColor *_Nullable)tabBarTintColor { _tabBarTintColor = tabBarTintColor; [self invalidateTabBarAppearance]; } // 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)setTabBarMinimizeBehaviorFromRNSTabBarMinimizeBehavior:(RNSTabBarMinimizeBehavior)tabBarMinimizeBehavior { #if RNS_IPHONE_OS_VERSION_AVAILABLE(26_0) if (@available(iOS 26.0, *)) { _tabBarMinimizeBehavior = rnscreens::conversion::UITabBarMinimizeBehaviorFromRNSTabBarMinimizeBehavior(tabBarMinimizeBehavior); _controller.tabBarMinimizeBehavior = _tabBarMinimizeBehavior; } else #endif // Check for iOS >= 26 if (tabBarMinimizeBehavior != RNSTabBarMinimizeBehaviorAutomatic) { RCTLogWarn(@"[RNScreens] tabBarMinimizeBehavior is supported for iOS >= 26"); } } - (void)setOnNativeFocusChange:(RCTDirectEventBlock)onNativeFocusChange { [self.reactEventEmitter setOnNativeFocusChange:onNativeFocusChange]; } #endif // RCT_NEW_ARCH_ENABLED #pragma mark - React Image Loader - (nullable RCTImageLoader *)reactImageLoader { return _imageLoader; } @end #if RCT_NEW_ARCH_ENABLED #pragma mark - View class exposure Class RNSBottomTabsCls(void) { return RNSBottomTabsHostComponentView.class; } #endif // RCT_NEW_ARCH_ENABLED