first commit

This commit is contained in:
2026-03-10 16:18:05 +00:00
commit 11f9c069b5
31635 changed files with 3187747 additions and 0 deletions

View File

@@ -0,0 +1,97 @@
#pragma once
#import "RNSBottomTabsHostComponentViewManager.h"
#import "RNSBottomTabsHostEventEmitter.h"
#import "RNSDefines.h"
#import "RNSEnums.h"
#import "RNSReactBaseView.h"
#import "RNSScreenContainer.h"
#ifdef RCT_NEW_ARCH_ENABLED
#import "RNSViewControllerInvalidating.h"
#else
#import <React/RCTInvalidating.h>
#endif
NS_ASSUME_NONNULL_BEGIN
@class RNSBottomTabsScreenComponentView;
@class RNSTabBarController;
@class RCTImageLoader;
/**
* Component view. Lifecycle is managed by React Native.
*
* This component serves as:
* 1. host for UITabBarController
* 2. provider of React state & props for the tab bar controller
* 3. two way communication channel with React (commands & events)
*/
@interface RNSBottomTabsHostComponentView : RNSReactBaseView <
RNSScreenContainerDelegate,
#ifdef RCT_NEW_ARCH_ENABLED
RNSViewControllerInvalidating
#else
RCTInvalidating
#endif
>
#if !RCT_NEW_ARCH_ENABLED
- (instancetype)initWithFrame:(CGRect)frame reactImageLoader:(RCTImageLoader *)imageLoader;
#endif // !RCT_NEW_ARCH_ENABLED
@property (nonatomic, nonnull, strong, readonly) RNSTabBarController *controller;
@end
#pragma mark - Props
@interface RNSBottomTabsHostComponentView ()
@property (nonatomic, strong, readonly, nullable) UIColor *tabBarTintColor;
@property (nonatomic, readonly) BOOL tabBarHidden;
@property (nonatomic, strong, readonly, nullable) UIColor *nativeContainerBackgroundColor;
@property (nonatomic, readonly) BOOL experimental_controlNavigationStateInJS;
#if RNS_IPHONE_OS_VERSION_AVAILABLE(26_0)
@property (nonatomic, readonly) UITabBarMinimizeBehavior tabBarMinimizeBehavior API_AVAILABLE(ios(26.0));
#endif // Check for iOS >= 26
#if RNS_IPHONE_OS_VERSION_AVAILABLE(18_0)
@property (nonatomic, readonly) UITabBarControllerMode tabBarControllerMode API_AVAILABLE(ios(18.0));
#endif // Check for iOS >= 18
@end
#pragma mark - React Events
@interface RNSBottomTabsHostComponentView ()
/**
* Use returned object to emit appropriate React Events to Element Tree.
*/
- (nonnull RNSBottomTabsHostEventEmitter *)reactEventEmitter;
- (BOOL)emitOnNativeFocusChangeRequestSelectedTabScreen:(nonnull RNSBottomTabsScreenComponentView *)tabScreen
repeatedSelectionHandledBySpecialEffect:(BOOL)repeatedSelectionHandledBySpecialEffect;
#if !RCT_NEW_ARCH_ENABLED
#pragma mark - LEGACY Event blocks
@property (nonatomic, copy) RCTDirectEventBlock onNativeFocusChange;
#endif
@end
#pragma mark - React Image Loader
@interface RNSBottomTabsHostComponentView ()
- (nullable RCTImageLoader *)reactImageLoader;
@end
NS_ASSUME_NONNULL_END

View File

@@ -0,0 +1,613 @@
#import "RNSBottomTabsHostComponentView.h"
#if RCT_NEW_ARCH_ENABLED
#import <React/RCTConversions.h>
#import <React/RCTImageLoader.h>
#import <React/RCTMountingTransactionObserving.h>
#import <react/renderer/components/rnscreens/ComponentDescriptors.h>
#import <react/renderer/components/rnscreens/EventEmitters.h>
#import <react/renderer/components/rnscreens/Props.h>
#import <react/renderer/components/rnscreens/RCTComponentViewHelpers.h>
#import <rnscreens/RNSBottomTabsComponentDescriptor.h>
#import "RNSBottomTabsHostComponentView+RNSImageLoader.h"
#import "RNSInvalidatedComponentsRegistry.h"
#import "RNSViewControllerInvalidator.h"
#endif // RCT_NEW_ARCH_ENABLED
#import "RNSBottomAccessoryHelper.h"
#import "RNSBottomTabsAccessoryComponentView.h"
#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 - Modified React Subviews extension
@interface RNSBottomTabsHostComponentView ()
@property (nonatomic, readonly) BOOL hasModifiedReactSubviewsInCurrentTransaction;
@end
#pragma mark - View implementation
@interface RNSBottomTabsHostComponentView ()
#if RCT_NEW_ARCH_ENABLED
<RCTMountingTransactionObserving>
#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<UIView *> *_reactSubviews;
BOOL _hasModifiedTabsScreensInCurrentTransaction;
BOOL _hasModifiedBottomAccessoryInCurrentTransation;
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
_hasModifiedTabsScreensInCurrentTransaction = NO;
_hasModifiedBottomAccessoryInCurrentTransation = NO;
_needsTabBarAppearanceUpdate = NO;
}
- (void)resetProps
{
#if RCT_NEW_ARCH_ENABLED
static const auto defaultProps = std::make_shared<const react::RNSBottomTabsProps>();
_props = defaultProps;
#endif
_tabBarTintColor = nil;
#if !TARGET_OS_TV
_nativeContainerBackgroundColor = [UIColor systemBackgroundColor];
#else // !TARGET_OS_TV
_nativeContainerBackgroundColor = nil;
#endif // !TARGET_OS_TV
}
#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];
// Enable auto-layout to ensure valid size of tabBarController.view.
// In host tree, tabBarController.view is the only child of HostComponentView.
controller.view.translatesAutoresizingMaskIntoConstraints = NO;
[NSLayoutConstraint activateConstraints:@[
[controller.view.topAnchor constraintEqualToAnchor:self.topAnchor],
[controller.view.bottomAnchor constraintEqualToAnchor:self.bottomAnchor],
[controller.view.leadingAnchor constraintEqualToAnchor:self.leadingAnchor],
[controller.view.trailingAnchor constraintEqualToAnchor:self.trailingAnchor]
]];
[controller didMoveToParentViewController:parentView.reactViewController];
break;
}
parentView = (UIView *)parentView.reactSuperview;
}
return;
}
}
#pragma mark - RNSScreenContainerDelegate
- (void)updateContainer
{
if (!self.hasModifiedReactSubviewsInCurrentTransaction) {
return;
}
NSMutableArray<RNSTabsScreenViewController *> *tabControllers =
[[NSMutableArray alloc] initWithCapacity:_reactSubviews.count];
RNSBottomTabsAccessoryComponentView *bottomAccessory = nil;
for (UIView *childView in _reactSubviews) {
if ([childView isKindOfClass:[RNSBottomTabsScreenComponentView class]]) {
RNSBottomTabsScreenComponentView *childScreen = static_cast<RNSBottomTabsScreenComponentView *>(childView);
[tabControllers addObject:childScreen.controller];
} else if ([childView isKindOfClass:[RNSBottomTabsAccessoryComponentView class]]) {
RCTAssert(bottomAccessory == nil, @"[RNScreens] There can only be one child RNSBottomTabsAccessoryComponentView");
bottomAccessory = static_cast<RNSBottomTabsAccessoryComponentView *>(childView);
} else {
RCTLogError(
@"[RNScreens] BottomTabs only accepts children of type BottomTabScreen and BottomTabsAccessory. Detected %@ instead.",
childView);
}
}
if (_hasModifiedTabsScreensInCurrentTransaction) {
RNSLog(@"updateContainer: tabControllers: %@", tabControllers);
[_controller childViewControllersHaveChangedTo:tabControllers];
}
if (_hasModifiedBottomAccessoryInCurrentTransation) {
RNSLog(@"updateContainer: bottomAccessory: %@", bottomAccessory);
#if RNS_IPHONE_OS_VERSION_AVAILABLE(26_0) && !TARGET_OS_TV && !TARGET_OS_VISION
if (@available(iOS 26.0, *)) {
if (bottomAccessory != nil) {
// We wrap RNSBottomTabsAccessoryComponentView in plain UIView to maintain native
// corner radius. RCTViewComponentView overrides it to 0 by default and we're unable
// to restore default value in an easy way. By wrapping it in UIView, it is clipped
// to default corner radius.
UIView *wrapperView = [UIView new];
[wrapperView addSubview:bottomAccessory];
[_controller setBottomAccessory:[[UITabAccessory alloc] initWithContentView:wrapperView] animated:YES];
} else {
[_controller setBottomAccessory:nil animated:YES];
}
}
#endif // RNS_IPHONE_OS_VERSION_AVAILABLE(26_0) && !TARGET_OS_TV && !TARGET_OS_VISION
}
}
- (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 (UIView<RCTInvalidating> *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:(nonnull RNSBottomTabsScreenComponentView *)tabScreen
repeatedSelectionHandledBySpecialEffect:(BOOL)repeatedSelectionHandledBySpecialEffect
{
return [_reactEventEmitter
emitOnNativeFocusChange:OnNativeFocusChangePayload{
.tabKey = tabScreen.tabKey,
.repeatedSelectionHandledBySpecialEffect = repeatedSelectionHandledBySpecialEffect}];
}
#pragma mark - RCTComponentViewProtocol
#if RCT_NEW_ARCH_ENABLED
- (void)mountChildComponentView:(UIView<RCTComponentViewProtocol> *)childComponentView index:(NSInteger)index
{
[self validateAndHandleReactSubview:childComponentView atIndex:index shouldMount:YES];
}
- (void)unmountChildComponentView:(UIView<RCTComponentViewProtocol> *)childComponentView index:(NSInteger)index
{
[self validateAndHandleReactSubview:childComponentView atIndex:index shouldMount:NO];
}
- (void)updateProps:(const facebook::react::Props::Shared &)props
oldProps:(const facebook::react::Props::Shared &)oldProps
{
const auto &oldComponentProps = *std::static_pointer_cast<const react::RNSBottomTabsProps>(_props);
const auto &newComponentProps = *std::static_pointer_cast<const react::RNSBottomTabsProps>(props);
if (newComponentProps.controlNavigationStateInJS != oldComponentProps.controlNavigationStateInJS) {
_experimental_controlNavigationStateInJS = newComponentProps.controlNavigationStateInJS;
}
if (newComponentProps.tabBarTintColor != oldComponentProps.tabBarTintColor) {
_needsTabBarAppearanceUpdate = YES;
_tabBarTintColor = RCTUIColorFromSharedColor(newComponentProps.tabBarTintColor);
}
if (newComponentProps.tabBarHidden != oldComponentProps.tabBarHidden) {
_tabBarHidden = newComponentProps.tabBarHidden;
#if RNS_IPHONE_OS_VERSION_AVAILABLE(18_0)
if (@available(iOS 18.0, *)) {
[_controller setTabBarHidden:_tabBarHidden animated:NO];
} else
#endif // RNS_IPHONE_OS_VERSION_AVAILABLE(18_0)
{
_controller.tabBar.hidden = _tabBarHidden;
}
}
if (newComponentProps.nativeContainerBackgroundColor != oldComponentProps.nativeContainerBackgroundColor) {
_nativeContainerBackgroundColor = RCTUIColorFromSharedColor(newComponentProps.nativeContainerBackgroundColor);
#if !TARGET_OS_TV
if (_nativeContainerBackgroundColor == nil) {
_nativeContainerBackgroundColor = [UIColor systemBackgroundColor];
}
#endif // !TARGET_OS_TV
_controller.view.backgroundColor = _nativeContainerBackgroundColor;
}
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");
}
}
if (newComponentProps.tabBarControllerMode != oldComponentProps.tabBarControllerMode) {
#if RNS_IPHONE_OS_VERSION_AVAILABLE(18_0)
if (@available(iOS 18.0, *)) {
_tabBarControllerMode = rnscreens::conversion::UITabBarControllerModeFromRNSBottomTabsTabBarControllerMode(
newComponentProps.tabBarControllerMode);
_controller.mode = _tabBarControllerMode;
} else
#endif // Check for iOS >= 18
if (newComponentProps.tabBarControllerMode != react::RNSBottomTabsTabBarControllerMode::Automatic) {
RCTLogWarn(@"[RNScreens] tabBarControllerMode is supported for iOS >= 18");
}
}
// 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<const react::RNSBottomTabsShadowNode::ConcreteState>(state);
_imageLoader = [self retrieveImageLoaderFromState:receivedState];
}
- (void)updateEventEmitter:(const facebook::react::EventEmitter::Shared &)eventEmitter
{
[super updateEventEmitter:eventEmitter];
const auto &castedEventEmitter = std::static_pointer_cast<const react::RNSBottomTabsEventEmitter>(eventEmitter);
[_reactEventEmitter updateEventEmitter:castedEventEmitter];
}
- (void)finalizeUpdates:(RNComponentViewUpdateMask)updateMask
{
if (_needsTabBarAppearanceUpdate) {
_needsTabBarAppearanceUpdate = NO;
[_controller setNeedsUpdateOfTabBarAppearance:true];
}
[super finalizeUpdates:updateMask];
}
+ (react::ComponentDescriptorProvider)componentDescriptorProvider
{
return react::concreteComponentDescriptorProvider<react::RNSBottomTabsComponentDescriptor>();
}
+ (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
{
_hasModifiedTabsScreensInCurrentTransaction = NO;
_hasModifiedBottomAccessoryInCurrentTransation = NO;
[_controller reactMountingTransactionWillMount];
#if RCT_NEW_ARCH_ENABLED
for (const auto &mutation : transaction.getMutations()) {
if ([self shouldInvalidateOnMutation:mutation]) {
for (UIView<RNSViewControllerInvalidating> *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 (self.hasModifiedReactSubviewsInCurrentTransaction) {
[self updateContainer];
}
[_controller reactMountingTransactionDidMount];
}
#else
#pragma mark - LEGACY architecture implementation
#pragma mark - LEGACY RCTComponent protocol
- (void)insertReactSubview:(UIView *)subview atIndex:(NSInteger)index
{
[super insertReactSubview:subview atIndex:index];
[self validateAndHandleReactSubview:subview atIndex:index shouldMount:YES];
}
- (void)removeReactSubview:(UIView *)subview
{
[super removeReactSubview:subview];
// index is not used for unmount
[self validateAndHandleReactSubview:subview atIndex:-1 shouldMount:NO];
}
RNS_IGNORE_SUPER_CALL_BEGIN
- (void)didUpdateReactSubviews
{
[self invalidateFlagsOnControllerIfNeeded];
}
RNS_IGNORE_SUPER_CALL_END
- (void)didSetProps:(NSArray<NSString *> *)changedProps
{
[super didSetProps:changedProps];
_needsTabBarAppearanceUpdate = YES;
[self invalidateFlagsOnControllerIfNeeded];
}
#pragma mark - LEGACY update methods
- (void)invalidateFlagsOnControllerIfNeeded
{
if (_needsTabBarAppearanceUpdate) {
_needsTabBarAppearanceUpdate = NO;
[_controller setNeedsUpdateOfTabBarAppearance:true];
}
if (self.hasModifiedReactSubviewsInCurrentTransaction) {
[self updateContainer];
_hasModifiedTabsScreensInCurrentTransaction = NO;
_hasModifiedBottomAccessoryInCurrentTransation = NO;
}
}
- (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];
}
- (void)setTabBarHidden:(BOOL)tabBarHidden
{
_tabBarHidden = tabBarHidden;
#if RNS_IPHONE_OS_VERSION_AVAILABLE(18_0)
if (@available(iOS 18.0, *)) {
[_controller setTabBarHidden:_tabBarHidden animated:NO];
} else
#endif // RNS_IPHONE_OS_VERSION_AVAILABLE(18_0)
{
_controller.tabBar.hidden = _tabBarHidden;
}
}
- (void)setNativeContainerBackgroundColor:(UIColor *_Nullable)nativeContainerBackgroundColor
{
_nativeContainerBackgroundColor = nativeContainerBackgroundColor;
#if !TARGET_OS_TV
if (_nativeContainerBackgroundColor == nil) {
_nativeContainerBackgroundColor = [UIColor systemBackgroundColor];
}
#endif // !TARGET_OS_TV
_controller.view.backgroundColor = _nativeContainerBackgroundColor;
}
// 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)setTabBarControllerModeFromRNSTabBarControllerMode:(RNSTabBarControllerMode)tabBarControllerMode
{
#if RNS_IPHONE_OS_VERSION_AVAILABLE(18_0)
if (@available(iOS 18.0, *)) {
_tabBarControllerMode =
rnscreens::conversion::UITabBarControllerModeFromRNSTabBarControllerMode(tabBarControllerMode);
_controller.mode = _tabBarControllerMode;
} else
#endif // Check for iOS >= 18
if (tabBarControllerMode != RNSTabBarControllerModeAutomatic) {
RCTLogWarn(@"[RNScreens] tabBarControllerMode is supported for iOS >= 18");
}
}
- (void)setOnNativeFocusChange:(RCTDirectEventBlock)onNativeFocusChange
{
[self.reactEventEmitter setOnNativeFocusChange:onNativeFocusChange];
}
#endif // RCT_NEW_ARCH_ENABLED
#pragma mark - Common
- (void)validateAndHandleReactSubview:(UIView *)subview atIndex:(NSInteger)index shouldMount:(BOOL)mount
{
BOOL isBottomAccessory = [subview isKindOfClass:[RNSBottomTabsAccessoryComponentView class]];
BOOL isTabsScreen = [subview isKindOfClass:[RNSBottomTabsScreenComponentView class]];
RCTAssert(
isBottomAccessory || isTabsScreen,
@"%@",
[NSString
stringWithFormat:
@"BottomTabs only accepts children of type BottomTabScreen and BottomTabsAccessory. Attempted to %@ %@",
mount ? @"mount" : @"unmount",
subview]);
if (isTabsScreen) {
auto *childScreen = static_cast<RNSBottomTabsScreenComponentView *>(subview);
childScreen.reactSuperview = mount ? self : nil;
_hasModifiedTabsScreensInCurrentTransaction = YES;
} else if (isBottomAccessory) {
auto *bottomAccessory = static_cast<RNSBottomTabsAccessoryComponentView *>(subview);
bottomAccessory.bottomTabsHostView = mount ? self : nil;
_hasModifiedBottomAccessoryInCurrentTransation = YES;
}
if (mount) {
[_reactSubviews insertObject:subview atIndex:index];
} else {
[_reactSubviews removeObject:subview];
}
}
#pragma mark - React Image Loader
- (nullable RCTImageLoader *)reactImageLoader
{
return _imageLoader;
}
@end
#pragma mark - Modified React Subviews implementation
@implementation RNSBottomTabsHostComponentView (ModifiedReactSubviews)
- (BOOL)hasModifiedReactSubviewsInCurrentTransaction
{
return _hasModifiedTabsScreensInCurrentTransaction || _hasModifiedBottomAccessoryInCurrentTransation;
}
@end
#if RCT_NEW_ARCH_ENABLED
#pragma mark - View class exposure
Class<RCTComponentViewProtocol> RNSBottomTabsCls(void)
{
return RNSBottomTabsHostComponentView.class;
}
#endif // RCT_NEW_ARCH_ENABLED

View File

@@ -0,0 +1,11 @@
#pragma once
#import <React/RCTViewManager.h>
NS_ASSUME_NONNULL_BEGIN
@interface RNSBottomTabsHostComponentViewManager : RCTViewManager
@end
NS_ASSUME_NONNULL_END

View File

@@ -0,0 +1,47 @@
#import "RNSBottomTabsHostComponentViewManager.h"
#if !RCT_NEW_ARCH_ENABLED
#import <React/RCTImageLoader.h>
#import "RNSBottomTabsHostComponentView.h"
#endif
@implementation RNSBottomTabsHostComponentViewManager
// TODO: This seems to be legacy arch only - test & remove when no longer needed
RCT_EXPORT_MODULE(RNSBottomTabsManager)
#if !RCT_NEW_ARCH_ENABLED
- (UIView *)view
{
// For Paper, we need to initialize TabsHost with RCTImageLoader from bridge
return [[RNSBottomTabsHostComponentView alloc] initWithFrame:CGRectZero
reactImageLoader:[self.bridge moduleForClass:[RCTImageLoader class]]];
}
#pragma mark - LEGACY Props
RCT_EXPORT_VIEW_PROPERTY(tabBarTintColor, UIColor);
RCT_EXPORT_VIEW_PROPERTY(tabBarHidden, BOOL);
RCT_EXPORT_VIEW_PROPERTY(nativeContainerBackgroundColor, UIColor);
// This remapping allows us to store UITabBarMinimizeBehavior in the component while accepting a custom enum as input
// from JS.
RCT_REMAP_VIEW_PROPERTY(
tabBarMinimizeBehavior,
tabBarMinimizeBehaviorFromRNSTabBarMinimizeBehavior,
RNSTabBarMinimizeBehavior);
// This remapping allows us to store UITabBarControllerMode in the component while accepting a custom enum as input
// from JS.
RCT_REMAP_VIEW_PROPERTY(tabBarControllerMode, tabBarControllerModeFromRNSTabBarControllerMode, RNSTabBarControllerMode);
// TODO: Missing prop
//@property (nonatomic, readonly) BOOL experimental_controlNavigationStateInJS;
#pragma mark - LEGACY Events
RCT_EXPORT_VIEW_PROPERTY(onNativeFocusChange, RCTDirectEventBlock);
#endif
@end

View File

@@ -0,0 +1,57 @@
#pragma once
#import <Foundation/Foundation.h>
// Hide C++ symbols from C compiler used when building Swift module
#if defined(__cplusplus) && RCT_NEW_ARCH_ENABLED
#import <react/renderer/components/rnscreens/EventEmitters.h>
namespace react = facebook::react;
#endif // __cplusplus
#if !RCT_NEW_ARCH_ENABLED
#import <React/RCTComponent.h>
#endif // !RCT_NEW_ARCH_ENABLED
NS_ASSUME_NONNULL_BEGIN
#if defined(__cplusplus)
struct OnNativeFocusChangePayload {
NSString *_Nonnull tabKey;
BOOL repeatedSelectionHandledBySpecialEffect;
};
#else
typedef struct {
NSString *_Nonnull tabKey;
BOOL repeatedSelectionHandledBySpecialEffect;
} OnNativeFocusChangePayload;
#endif
@interface RNSBottomTabsHostEventEmitter : NSObject
- (BOOL)emitOnNativeFocusChange:(OnNativeFocusChangePayload)payload;
@end
#pragma mark - Hidden from Swift
#if defined(__cplusplus)
@interface RNSBottomTabsHostEventEmitter ()
#if RCT_NEW_ARCH_ENABLED
- (void)updateEventEmitter:(const std::shared_ptr<const react::RNSBottomTabsEventEmitter> &)emitter;
#else
#pragma mark - LEGACY Event emitter blocks
@property (nonatomic, copy) RCTDirectEventBlock onNativeFocusChange;
#endif // RCT_NEW_ARCH_ENABLED
@end
#endif // __cplusplus
NS_ASSUME_NONNULL_END

View File

@@ -0,0 +1,63 @@
#import "RNSBottomTabsHostEventEmitter.h"
#import <React/RCTLog.h>
#if RCT_NEW_ARCH_ENABLED
#import <React/RCTConversions.h>
#import <react/renderer/components/rnscreens/EventEmitters.h>
#endif // RCT_NEW_ARCH_ENABLED
#if RCT_NEW_ARCH_ENABLED
namespace react = facebook::react;
#endif // RCT_NEW_ARCH_ENABLED
@implementation RNSBottomTabsHostEventEmitter {
#if RCT_NEW_ARCH_ENABLED
std::shared_ptr<const react::RNSBottomTabsEventEmitter> _reactEventEmitter;
#endif // RCT_NEW_ARCH_ENABLED
}
- (instancetype)init
{
if (self = [super init]) {
#if RCT_NEW_ARCH_ENABLED
_reactEventEmitter = nullptr;
#endif // RCT_NEW_ARCH_ENABLED
}
return self;
}
#if RCT_NEW_ARCH_ENABLED
- (void)updateEventEmitter:(const std::shared_ptr<const react::RNSBottomTabsEventEmitter> &)emitter
{
_reactEventEmitter = emitter;
}
#endif // RCT_NEW_ARCH_ENABLED
- (BOOL)emitOnNativeFocusChange:(OnNativeFocusChangePayload)payload
{
#if RCT_NEW_ARCH_ENABLED
if (_reactEventEmitter != nullptr) {
_reactEventEmitter->onNativeFocusChange(
{.tabKey = RCTStringFromNSString(payload.tabKey),
.repeatedSelectionHandledBySpecialEffect =
static_cast<bool>(payload.repeatedSelectionHandledBySpecialEffect)});
return YES;
} else {
RCTLogWarn(@"[RNScreens] Skipped OnNativeFocusChange event emission due to nullish emitter");
return NO;
}
#else
if (self.onNativeFocusChange) {
self.onNativeFocusChange(@{
@"tabKey" : payload.tabKey,
@"repeatedSelectionHandledBySpecialEffect" : @(payload.repeatedSelectionHandledBySpecialEffect)
});
return YES;
} else {
RCTLogWarn(@"[RNScreens] Skipped OnNativeFocusChange event emission due to nullish emitter");
return NO;
}
#endif // RCT_NEW_ARCH_ENABLED
}
@end

View File

@@ -0,0 +1,166 @@
#pragma once
#import <UIKit/UIKit.h>
#import "RNSTabBarAppearanceCoordinator.h"
#import "RNSTabsScreenViewController.h"
#if !TARGET_OS_TV
#import "RNSOrientationProviding.h"
#endif // !TARGET_OS_TV
NS_ASSUME_NONNULL_BEGIN
@protocol RNSReactTransactionObserving
- (void)reactMountingTransactionWillMount;
- (void)reactMountingTransactionDidMount;
@end
/**
* This controller is responsible for tab management & all other responsibilities coming from the fact of inheritance
* from `UITabBarController`. It is limited only to the child view controllers of type `RNSTabsScreenViewController`,
* however.
*
* Updates made by this controller are synchronized by `RNSReactTransactionObserving` protocol,
* i.e. if you made changes through one of signals method, unless you flush them immediately (not needed atm), they will
* be executed only after react finishes the transaction (from within transaction execution block).
*/
@interface RNSTabBarController : UITabBarController <
RNSReactTransactionObserving
#if !TARGET_OS_TV
,
RNSOrientationProviding
#endif // !TARGET_OS_TV
>
- (instancetype)initWithTabsHostComponentView:(nullable RNSBottomTabsHostComponentView *)tabsHostComponentView;
/**
* Get reference to the host component view that owns this tab bar controller.
*
* Might return null in cases where the controller view hierararchy is not attached to parent.
*/
@property (nonatomic, readonly, nullable) RNSBottomTabsHostComponentView *tabsHostComponentView;
/**
* Tab bar appearance coordinator. If you need to update tab bar appearance avoid using this one directly. Send the
* controller a signal, invalidate the tab bar appearance & either wait for the update flush or flush it manually.
*/
@property (nonatomic, readonly, strong, nonnull) RNSTabBarAppearanceCoordinator *tabBarAppearanceCoordinator;
/**
* Update tab controller state with previously provided children.
*
* This method does nothing if the children have not been changed / update has not been requested before.
* The requested update is performed immediately. If you do not need this, consider just raising an appropriate
* invalidation signal & let the controller decide when to flush the updates.
*/
- (void)updateReactChildrenControllersIfNeeded;
/**
* Force update of the tab controller state with previously provided children.
*
* The requested update is performed immediately. If you do not need this, consider just raising an appropriate
* invalidation signal & let the controller decide when to flush the updates.
*/
- (void)updateReactChildrenControllers;
/**
* Find out which tab bar controller is currently focused & select it.
*
* This method does nothing if the update has not been previously requested.
* If needed, the requested update is performed immediately. If you do not need this, consider just raising an
* appropriate invalidation signal & let the controller decide when to flush the updates.
*/
- (void)updateSelectedViewControllerIfNeeded;
/**
* Find out which tab bar controller is currently focused & select it.
*
* The requested update is performed immediately. If you do not need this, consider just raising an appropriate
* invalidation signal & let the controller decide when to flush the updates.
*/
- (void)updateSelectedViewController;
/**
* Updates the tab bar appearance basing on configuration sources (host view, tab screens).
*
* This method does nothing if the update has not been previously requested.
* If needed, the requested update is performed immediately. If you do not need this, consider just raising an
* appropriate invalidation signal & let the controller decide when to flush the updates.
*/
- (void)updateTabBarAppearanceIfNeeded;
/**
* Updates the tab bar appearance basing on configuration sources (host view, tab screens).
*
* The requested update is performed immediately. If you do not need this, consider just raising an appropriate
* invalidation signal & let the controller decide when to flush the updates.
*/
- (void)updateTabBarAppearance;
/**
* Updates the interface orientation based on selected tab screen and its children.
*
* This method does nothing if the update has not been previously requested.
* If needed, the requested update is performed immediately. If you do not need this, consider just raising an
* appropriate invalidation signal & let the controller decide when to flush the updates.
*/
- (void)updateOrientationIfNeeded;
/**
* Updates the interface orientation based on selected tab screen and its children.
*
* The requested update is performed immediately. If you do not need this, consider just raising an appropriate
* invalidation signal & let the controller decide when to flush the updates.
*/
- (void)updateOrientation;
@end
#pragma mark - Signals
/**
* This extension defines various invalidation signals that you can send to the controller, to notify it that it needs
* to take some action.
*/
@interface RNSTabBarController ()
/**
* Tell the controller that react provided tabs have changed (count / instances) & the child view controllers need to be
* updated.
*
* This also automatically raises `needsReactChildrenUpdate` flag, no need to call it manually.
*/
- (void)childViewControllersHaveChangedTo:(nonnull NSArray<RNSTabsScreenViewController *> *)childViewControllers;
/**
* Tell the controller that react provided tabs have changed (count / instances) & the child view controllers need to be
* updated.
*
* Do not raise this signal only when focused state of the tab has changed - use `needsSelectedTabUpdate` instead.
*/
@property (nonatomic, readwrite) bool needsUpdateOfReactChildrenControllers;
/**
* Tell the controller that react provided tabs have changed (count / instances) & the child view controllers need to be
* updated.
*/
@property (nonatomic, readwrite) bool needsUpdateOfSelectedTab;
/**
* Tell the controller that some configuration regarding the tab bar apperance has changed & the appearance requires
* update.
*/
@property (nonatomic, readwrite) bool needsUpdateOfTabBarAppearance;
/**
* Tell the controller that some configuration regarding interface orientation has changed & it requires update.
*/
@property (nonatomic, readwrite) bool needsOrientationUpdate;
@end
NS_ASSUME_NONNULL_END

View File

@@ -0,0 +1,273 @@
#import "RNSTabBarController.h"
#import <React/RCTAssert.h>
#import <React/RCTLog.h>
#import "RNSLog.h"
#import "RNSScreenWindowTraits.h"
@implementation RNSTabBarController {
NSArray<RNSTabsScreenViewController *> *_Nullable _tabScreenControllers;
#if !RCT_NEW_ARCH_ENABLED
BOOL _isControllerFlushBlockScheduled;
#endif // !RCT_NEW_ARCH_ENABLED
}
- (instancetype)init
{
if (self = [super init]) {
_tabScreenControllers = nil;
_tabBarAppearanceCoordinator = [RNSTabBarAppearanceCoordinator new];
_tabsHostComponentView = nil;
#if !RCT_NEW_ARCH_ENABLED
_isControllerFlushBlockScheduled = NO;
#endif // !RCT_NEW_ARCH_ENABLED
}
return self;
}
- (instancetype)initWithTabsHostComponentView:(nullable RNSBottomTabsHostComponentView *)tabsHostComponentView
{
if (self = [self init]) {
_tabsHostComponentView = tabsHostComponentView;
}
return self;
}
- (void)tabBar:(UITabBar *)tabBar didSelectItem:(UITabBarItem *)item
{
RNSLog(@"TabBar: %@ didSelectItem: %@", tabBar, item);
}
#pragma mark-- Signals
- (void)childViewControllersHaveChangedTo:(NSArray<RNSTabsScreenViewController *> *)reactChildControllers
{
_tabScreenControllers = reactChildControllers;
self.needsUpdateOfReactChildrenControllers = true;
}
- (void)setNeedsUpdateOfReactChildrenControllers:(bool)needsReactChildrenUpdate
{
_needsUpdateOfReactChildrenControllers = true;
self.needsUpdateOfSelectedTab = true;
#if !RCT_NEW_ARCH_ENABLED
[self scheduleControllerUpdateIfNeeded];
#endif // !RCT_NEW_ARCH_ENABLED
}
- (void)setNeedsUpdateOfSelectedTab:(bool)needsSelectedTabUpdate
{
_needsUpdateOfSelectedTab = needsSelectedTabUpdate;
if (needsSelectedTabUpdate) {
_needsOrientationUpdate = true;
}
#if !RCT_NEW_ARCH_ENABLED
[self scheduleControllerUpdateIfNeeded];
#endif // !RCT_NEW_ARCH_ENABLED
}
- (void)setNeedsUpdateOfTabBarAppearance:(bool)needsUpdateOfTabBarAppearance
{
_needsUpdateOfTabBarAppearance = needsUpdateOfTabBarAppearance;
#if !RCT_NEW_ARCH_ENABLED
[self scheduleControllerUpdateIfNeeded];
#endif // !RCT_NEW_ARCH_ENABLED
}
- (void)setNeedsOrientationUpdate:(bool)needsOrientationUpdate
{
_needsOrientationUpdate = needsOrientationUpdate;
#if !RCT_NEW_ARCH_ENABLED
[self scheduleControllerUpdateIfNeeded];
#endif // !RCT_NEW_ARCH_ENABLED
}
#pragma mark-- RNSReactTransactionObserving
- (void)reactMountingTransactionWillMount
{
RNSLog(@"TabBarCtrl mountintTransactionWillMount");
}
- (void)reactMountingTransactionDidMount
{
RNSLog(@"TabBarCtrl mountintTransactionDidMount running updates");
[self updateReactChildrenControllersIfNeeded];
[self updateSelectedViewControllerIfNeeded];
[self updateTabBarAppearanceIfNeeded];
[self updateTabBarA11yIfNeeded];
[self updateOrientationIfNeeded];
}
#pragma mark-- Signals related
- (void)updateReactChildrenControllersIfNeeded
{
if (_needsUpdateOfReactChildrenControllers) {
[self updateReactChildrenControllers];
}
}
- (void)updateReactChildrenControllers
{
RNSLog(@"TabBarCtrl updateReactChildrenControllers");
_needsUpdateOfReactChildrenControllers = false;
if (_tabScreenControllers == nil) {
RCTLogWarn(@"[RNScreens] Attempt to update react children while the _updatedChildren array is nil!");
return;
}
[self setViewControllers:_tabScreenControllers animated:[[self viewControllers] count] != 0];
}
- (void)updateSelectedViewControllerIfNeeded
{
if (_needsUpdateOfSelectedTab) {
[self updateSelectedViewController];
}
}
- (void)updateSelectedViewController
{
RNSLog(@"TabBarCtrl updateSelectedViewController");
_needsUpdateOfSelectedTab = false;
#if !defined(NDEBUG)
[self assertExactlyOneFocusedTab];
#endif
RNSTabsScreenViewController *_Nullable selectedViewController = nil;
for (RNSTabsScreenViewController *tabViewController in self.viewControllers) {
RNSLog(
@"Update Selected View Controller [%ld] isFocused %d",
tabViewController.tabScreenComponentView.tag,
tabViewController.tabScreenComponentView.isSelectedScreen);
if (tabViewController.tabScreenComponentView.isSelectedScreen == true) {
selectedViewController = tabViewController;
break;
}
}
RCTAssert(selectedViewController != nil, @"[RNScreens] No selected view controller!");
RNSLog(@"Change selected view controller to: %@", selectedViewController);
if (@available(iOS 26.0, *)) {
// On iOS 26, we need to set user interface style 2 parent views above the tab bar
// for this prop to take effect.
self.tabBar.superview.superview.overrideUserInterfaceStyle =
selectedViewController.tabScreenComponentView.userInterfaceStyle;
} else {
self.tabBar.overrideUserInterfaceStyle = selectedViewController.tabScreenComponentView.userInterfaceStyle;
}
[self setSelectedViewController:selectedViewController];
}
- (void)updateTabBarAppearanceIfNeeded
{
if (_needsUpdateOfTabBarAppearance) {
[self updateTabBarAppearance];
}
}
- (void)updateTabBarAppearance
{
RNSLog(@"TabBarCtrl updateTabBarAppearance");
_needsUpdateOfTabBarAppearance = false;
[_tabBarAppearanceCoordinator updateAppearanceOfTabBar:[self tabBar]
withHostComponentView:self.tabsHostComponentView
tabScreenControllers:_tabScreenControllers
imageLoader:[self.tabsHostComponentView reactImageLoader]];
}
- (void)updateTabBarA11yIfNeeded
{
for (UIViewController *tabViewController in self.viewControllers) {
auto screenView = static_cast<RNSTabsScreenViewController *>(tabViewController).tabScreenComponentView;
if (!screenView.tabBarItemNeedsA11yUpdate) {
continue;
}
screenView.tabBarItemNeedsA11yUpdate = NO;
tabViewController.tabBarItem.accessibilityIdentifier = screenView.tabItemTestID;
tabViewController.tabBarItem.accessibilityLabel = screenView.tabItemAccessibilityLabel;
}
}
#if !defined(NDEBUG)
- (void)assertExactlyOneFocusedTab
{
int selectedCount = 0;
for (RNSTabsScreenViewController *tabViewController in _tabScreenControllers) {
if (tabViewController.tabScreenComponentView.isSelectedScreen) {
++selectedCount;
}
}
RCTAssert(
selectedCount == 1, @"[RNScreens] Invariant violation. Expected exactly 1 focused tab, got: %d", selectedCount);
}
#endif
#if !RCT_NEW_ARCH_ENABLED
#pragma mark - LEGACY Paper scheduling methods
// TODO: These could be moved to separate scheduler class
- (void)scheduleControllerUpdateIfNeeded
{
if (_isControllerFlushBlockScheduled) {
return;
}
_isControllerFlushBlockScheduled = YES;
auto *__weak weakSelf = self;
dispatch_async(dispatch_get_main_queue(), ^{
auto *strongSelf = weakSelf;
if (strongSelf == nil) {
return;
}
strongSelf->_isControllerFlushBlockScheduled = NO;
[strongSelf reactMountingTransactionWillMount];
[strongSelf reactMountingTransactionDidMount];
});
}
#endif // !RCT_NEW_ARCH_ENABLED
- (void)updateOrientationIfNeeded
{
if (_needsOrientationUpdate) {
[self updateOrientation];
}
}
- (void)updateOrientation
{
_needsOrientationUpdate = false;
[RNSScreenWindowTraits enforceDesiredDeviceOrientation];
}
#pragma mark - RNSOrientationProviding
#if !TARGET_OS_TV
- (RNSOrientation)evaluateOrientation
{
if ([self.selectedViewController respondsToSelector:@selector(evaluateOrientation)]) {
id<RNSOrientationProviding> selected = static_cast<id<RNSOrientationProviding>>(self.selectedViewController);
return [selected evaluateOrientation];
}
return RNSOrientationInherit;
}
#endif // !TARGET_OS_TV
@end

View File

@@ -0,0 +1,11 @@
#pragma once
#import <UIKit/UIKit.h>
NS_ASSUME_NONNULL_BEGIN
@interface RNSTabBarControllerDelegate : NSObject <UITabBarControllerDelegate>
@end
NS_ASSUME_NONNULL_END

View File

@@ -0,0 +1,87 @@
#import "RNSTabBarControllerDelegate.h"
#import <React/RCTAssert.h>
#import "RNSTabBarController.h"
#import "RNSTabsScreenViewController.h"
@implementation RNSTabBarControllerDelegate
- (BOOL)tabBarController:(UITabBarController *)tabBarController
shouldSelectViewController:(UIViewController *)viewController
{
RCTAssert(
[tabBarController isKindOfClass:RNSTabBarController.class],
@"[RNScreens] Unexpected type of controller: %@",
tabBarController.class);
// Can be UINavigationController in case of MoreNavigationController
RCTAssert(
[viewController isKindOfClass:RNSTabsScreenViewController.class] ||
[viewController isKindOfClass:UINavigationController.class],
@"[RNScreens] Unexpected type of controller: %@",
viewController.class);
RNSTabBarController *tabBarCtrl = static_cast<RNSTabBarController *>(tabBarController);
RNSTabsScreenViewController *tabScreenCtrl = static_cast<RNSTabsScreenViewController *>(viewController);
#if !TARGET_OS_TV
// When the moreNavigationController is selected, we want to show it
// TODO: this solution only works for uncontrolled mode. Add support for controlled mode as well.
if ([self shouldAllowMoreControllerSelection:tabBarCtrl] &&
viewController == tabBarController.moreNavigationController) {
return YES;
}
#endif // !TARGET_OS_TV
// TODO: handle enforcing orientation with natively-driven tabs
// Detect repeated selection and inform tabScreenController
BOOL repeatedSelection = [tabBarCtrl selectedViewController] == tabScreenCtrl;
BOOL repeatedSelectionHandledBySpecialEffect =
repeatedSelection ? [tabScreenCtrl tabScreenSelectedRepeatedly] : false;
[tabBarCtrl.tabsHostComponentView
emitOnNativeFocusChangeRequestSelectedTabScreen:tabScreenCtrl.tabScreenComponentView
repeatedSelectionHandledBySpecialEffect:repeatedSelectionHandledBySpecialEffect];
// On repeated selection we return false to prevent native *pop to root* effect that works only starting from iOS 26
// and interferes with our implementation (which is necessary for controlled tabs).
return repeatedSelection ? false : ![self shouldPreventNativeTabChangeWithinTabBarController:tabBarCtrl];
}
- (void)tabBarController:(UITabBarController *)tabBarController
didSelectViewController:(UIViewController *)viewController
{
RCTAssert(
[tabBarController isKindOfClass:RNSTabBarController.class],
@"[RNScreens] Unexpected type of controller: %@",
tabBarController.class);
// Can be UINavigationController in case of MoreNavigationController
RCTAssert(
[viewController isKindOfClass:RNSTabsScreenViewController.class] ||
[viewController isKindOfClass:UINavigationController.class],
@"[RNScreens] Unexpected type of controller: %@",
viewController.class);
#if !TARGET_OS_TV
// When the moreNavigationController is selected, we want to show it
if ([self shouldAllowMoreControllerSelection:static_cast<RNSTabBarController *>(tabBarController)] &&
viewController == tabBarController.moreNavigationController) {
// Hide the navigation bar for the more controller
[tabBarController.moreNavigationController setNavigationBarHidden:YES animated:NO];
}
#endif // !TARGET_OS_TV
}
- (BOOL)shouldPreventNativeTabChangeWithinTabBarController:(nonnull RNSTabBarController *)tabBarCtrl
{
// This handles the tabsHostComponentView nullability
return [tabBarCtrl.tabsHostComponentView experimental_controlNavigationStateInJS] ?: NO;
}
- (BOOL)shouldAllowMoreControllerSelection:(nonnull RNSTabBarController *)tabBarCtrl
{
return ![tabBarCtrl.tabsHostComponentView experimental_controlNavigationStateInJS] ?: YES;
}
@end