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,23 @@
#pragma once
#import <React/RCTConvert.h>
#import <UIKit/UIKit.h>
#if !RCT_NEW_ARCH_ENABLED
#import "RNSEnums.h"
#endif // !RCT_NEW_ARCH_ENABLED
NS_ASSUME_NONNULL_BEGIN
@interface RCTConvert (RNSBottomTabs)
+ (UIOffset)UIOffset:(nonnull id)json;
#if !RCT_NEW_ARCH_ENABLED
+ (RNSBottomTabsIconType)RNSBottomTabsIconType:(nonnull id)json;
+ (RNSOrientation)RNSOrientation:(nonnull id)json;
#endif // !RCT_NEW_ARCH_ENABLED
@end
NS_ASSUME_NONNULL_END

View File

@@ -0,0 +1,82 @@
#import "RCTConvert+RNSBottomTabs.h"
@implementation RCTConvert (RNSBottomTabs)
+ (UIOffset)UIOffset:(id)json;
{
json = [self NSDictionary:json];
return UIOffsetMake([json[@"horizontal"] floatValue], [json[@"vertical"] floatValue]);
}
#if !RCT_NEW_ARCH_ENABLED
RCT_ENUM_CONVERTER(
RNSBottomTabsIconType,
(@{
@"image" : @(RNSBottomTabsIconTypeImage),
@"template" : @(RNSBottomTabsIconTypeTemplate),
@"sfSymbol" : @(RNSBottomTabsIconTypeSfSymbol),
@"xcasset" : @(RNSBottomTabsIconTypeXcasset),
}),
RNSBottomTabsIconTypeSfSymbol,
integerValue)
RCT_ENUM_CONVERTER(
RNSTabBarMinimizeBehavior,
(@{
@"automatic" : @(RNSTabBarMinimizeBehaviorAutomatic),
@"never" : @(RNSTabBarMinimizeBehaviorNever),
@"onScrollDown" : @(RNSTabBarMinimizeBehaviorOnScrollDown),
@"onScrollUp" : @(RNSTabBarMinimizeBehaviorOnScrollUp),
}),
RNSTabBarMinimizeBehaviorAutomatic,
integerValue)
RCT_ENUM_CONVERTER(
RNSTabBarControllerMode,
(@{
@"automatic" : @(RNSTabBarControllerModeAutomatic),
@"tabBar" : @(RNSTabBarControllerModeTabBar),
@"tabSidebar" : @(RNSTabBarControllerModeTabSidebar),
}),
RNSTabBarControllerModeAutomatic,
integerValue)
RCT_ENUM_CONVERTER(
RNSOrientation,
(@{
@"inherit" : @(RNSOrientationInherit),
@"all" : @(RNSOrientationAll),
@"allButUpsideDown" : @(RNSOrientationAllButUpsideDown),
@"portrait" : @(RNSOrientationPortrait),
@"portraitUp" : @(RNSOrientationPortraitUp),
@"portraitDown" : @(RNSOrientationPortraitDown),
@"landscape" : @(RNSOrientationLandscape),
@"landscapeLeft" : @(RNSOrientationLandscapeLeft),
@"landscapeRight" : @(RNSOrientationLandscapeRight),
}),
RNSOrientationInherit,
integerValue)
RCT_ENUM_CONVERTER(
RNSBottomTabsScreenSystemItem,
(@{
@"none" : @(RNSBottomTabsScreenSystemItemNone),
@"bookmarks" : @(RNSBottomTabsScreenSystemItemBookmarks),
@"contacts" : @(RNSBottomTabsScreenSystemItemContacts),
@"downloads" : @(RNSBottomTabsScreenSystemItemDownloads),
@"favorites" : @(RNSBottomTabsScreenSystemItemFavorites),
@"featured" : @(RNSBottomTabsScreenSystemItemFeatured),
@"history" : @(RNSBottomTabsScreenSystemItemHistory),
@"more" : @(RNSBottomTabsScreenSystemItemMore),
@"mostRecent" : @(RNSBottomTabsScreenSystemItemMostRecent),
@"mostViewed" : @(RNSBottomTabsScreenSystemItemMostViewed),
@"recents" : @(RNSBottomTabsScreenSystemItemRecents),
@"search" : @(RNSBottomTabsScreenSystemItemSearch),
@"topRated" : @(RNSBottomTabsScreenSystemItemTopRated),
}),
RNSBottomTabsScreenSystemItemNone,
integerValue)
#endif // !RCT_NEW_ARCH_ENABLED
@end

View File

@@ -0,0 +1,19 @@
#pragma once
#import <UIKit/UIKit.h>
NS_ASSUME_NONNULL_BEGIN
@class RNSTabsScreenViewController;
@protocol RNSBottomTabsSpecialEffectsSupporting
/**
* Handle repeated tab selection (e.g. in order to pop UINavigationController to root).
* Returns boolean indicating whether the action has been handled.
*/
- (bool)onRepeatedTabSelectionOfTabScreenController:(nonnull RNSTabsScreenViewController *)tabScreenController;
@end
NS_ASSUME_NONNULL_END

View File

@@ -0,0 +1,51 @@
#pragma once
#import <Foundation/Foundation.h>
#import "RNSBottomTabsHostComponentView.h"
#import "RNSTabsScreenViewController.h"
NS_ASSUME_NONNULL_BEGIN
@class RCTImageLoader;
/**
* Responsible for creating & applying appearance to the tab bar.
*
* It does take into account all properties from host component view & tab screen controllers related to tab bar
* appearance and applies them accordingly in correct order.
*/
@interface RNSTabBarAppearanceCoordinator : NSObject
/**
* Applies the tab bar appearance props to the tab bar and respective tab bar items, basing on information contained in
* provided params.
*
* TODO: Do not take references to component view & controllers here. Put the tab bar appearance properites in single
* type & only take it here.
*/
- (void)updateAppearanceOfTabBar:(nullable UITabBar *)tabBar
withHostComponentView:(nullable RNSBottomTabsHostComponentView *)hostComponentView
tabScreenControllers:(nullable NSArray<RNSTabsScreenViewController *> *)tabScreenCtrls
imageLoader:(nullable RCTImageLoader *)imageLoader;
/**
* Configures UITabBarAppearance object using appearance props provided in the param.
*
* `appearanceProps` should be an NSDictionary with hierarchical structure that corresponds to UIKit's
* UITabBarAppearance object:
* - `appearanceProps` can contain:
* - `stacked`, `inline` and `compactInline` keys that map to dictionaries corresponding to UIKit's
* UITabBarItemAppearance objects (`itemAppearanceProps`),
* - entries that correspond to other props from UITabBarItemAppearance (UIBarAppearance) object, e.g.
* `backgroundColor`,
* - `itemAppearanceProps` can contain `normal`, `selected`, `disabled`, `focused` keys that map to dictionaries
* corresponding to UIKit's UITabBarItemStateAppearance objects (`itemStateAppearanceProps`),
* - `itemStateAppearanceProps` can contain entries that correspond to props from UITabBarItemStateAppearance object,
* e.g. `tabBarItemIconColor`.
*/
+ (void)configureTabBarAppearance:(nonnull UITabBarAppearance *)tabBarAppearance
fromAppearanceProps:(nonnull NSDictionary *)appearanceProps;
@end
NS_ASSUME_NONNULL_END

View File

@@ -0,0 +1,255 @@
#import "RNSTabBarAppearanceCoordinator.h"
#import <React/RCTFont.h>
#import <React/RCTImageLoader.h>
#import "RCTConvert+RNSBottomTabs.h"
#import "RNSConversions.h"
#import "RNSImageLoadingHelper.h"
#import "RNSTabBarController.h"
#import "RNSTabsScreenViewController.h"
@implementation RNSTabBarAppearanceCoordinator
- (void)updateAppearanceOfTabBar:(nullable UITabBar *)tabBar
withHostComponentView:(nullable RNSBottomTabsHostComponentView *)hostComponentView
tabScreenControllers:(nullable NSArray<RNSTabsScreenViewController *> *)tabScreenCtrls
imageLoader:(nullable RCTImageLoader *)imageLoader
{
if (tabBar == nil) {
return;
}
// Step 1 - configure host-specific appearance
tabBar.tintColor = hostComponentView.tabBarTintColor;
// Set tint color for iPadOS tab bar. This is the official way recommended by Apple:
// https://developer.apple.com/forums/thread/761056?answerId=798245022#798245022
hostComponentView.controller.view.tintColor = hostComponentView.tabBarTintColor;
if (tabScreenCtrls == nil) {
return;
}
// Step 2 - configure screen-specific appearance
for (RNSTabsScreenViewController *tabScreenCtrl in tabScreenCtrls) {
if (tabScreenCtrl == nil) {
// It should not be null here, something went wrong.
RCTLogWarn(@"[RNScreens] Nullish controller of TabScreen while tab bar appearance update!");
continue;
}
[self configureTabBarItemForTabScreenController:tabScreenCtrl imageLoader:imageLoader];
}
}
- (void)configureTabBarItemForTabScreenController:(nonnull RNSTabsScreenViewController *)tabScreenCtrl
imageLoader:(nullable RCTImageLoader *)imageLoader
{
UITabBarItem *tabBarItem = tabScreenCtrl.tabBarItem;
tabBarItem.standardAppearance = tabScreenCtrl.tabScreenComponentView.standardAppearance;
tabBarItem.scrollEdgeAppearance = tabScreenCtrl.tabScreenComponentView.scrollEdgeAppearance;
[self setIconsForTabBarItem:tabBarItem
fromScreenView:tabScreenCtrl.tabScreenComponentView
withImageLoader:imageLoader];
}
- (void)setIconsForTabBarItem:(UITabBarItem *)tabBarItem
fromScreenView:(RNSBottomTabsScreenComponentView *)screenView
withImageLoader:(RCTImageLoader *_Nullable)imageLoader
{
if (screenView.iconType == RNSBottomTabsIconTypeSfSymbol || screenView.iconType == RNSBottomTabsIconTypeXcasset) {
if (screenView.iconResourceName != nil) {
if (screenView.iconType == RNSBottomTabsIconTypeSfSymbol) {
tabBarItem.image = [UIImage systemImageNamed:screenView.iconResourceName];
} else {
tabBarItem.image = [UIImage imageNamed:screenView.iconResourceName];
}
} else if (screenView.systemItem != RNSBottomTabsScreenSystemItemNone) {
// Restore default system item icon
UITabBarSystemItem systemItem =
rnscreens::conversion::RNSBottomTabsScreenSystemItemToUITabBarSystemItem(screenView.systemItem);
tabBarItem.image = [[UITabBarItem alloc] initWithTabBarSystemItem:systemItem tag:0].image;
} else {
tabBarItem.image = nil;
}
if (screenView.selectedIconResourceName != nil) {
if (screenView.iconType == RNSBottomTabsIconTypeSfSymbol) {
tabBarItem.selectedImage = [UIImage systemImageNamed:screenView.selectedIconResourceName];
} else {
tabBarItem.selectedImage = [UIImage imageNamed:screenView.selectedIconResourceName];
}
} else if (screenView.systemItem != RNSBottomTabsScreenSystemItemNone) {
// Restore default system item icon
UITabBarSystemItem systemItem =
rnscreens::conversion::RNSBottomTabsScreenSystemItemToUITabBarSystemItem(screenView.systemItem);
tabBarItem.selectedImage = [[UITabBarItem alloc] initWithTabBarSystemItem:systemItem tag:0].selectedImage;
} else {
tabBarItem.selectedImage = nil;
}
} else if (imageLoader != nil) {
bool isTemplate = screenView.iconType == RNSBottomTabsIconTypeTemplate;
// Normal icon
if (screenView.iconImageSource != nil) {
[RNSImageLoadingHelper loadImageFromSource:screenView.iconImageSource
withImageLoader:imageLoader
asTemplate:isTemplate
completionBlock:^(UIImage *image) {
[self updateTabBarItem:tabBarItem
withImage:image
isSelected:NO
forScreenView:screenView];
}];
} else {
tabBarItem.image = nil;
}
// Selected icon
if (screenView.selectedIconImageSource != nil) {
[RNSImageLoadingHelper loadImageFromSource:screenView.selectedIconImageSource
withImageLoader:imageLoader
asTemplate:isTemplate
completionBlock:^(UIImage *image) {
[self updateTabBarItem:tabBarItem
withImage:image
isSelected:YES
forScreenView:screenView];
}];
} else {
tabBarItem.selectedImage = nil;
}
} else {
RCTLogWarn(@"[RNScreens] unable to load tab bar item icons: imageLoader should not be nil");
}
}
- (void)updateTabBarItem:(UITabBarItem *)tabBarItem
withImage:(UIImage *)image
isSelected:(BOOL)isSelected
forScreenView:(RNSBottomTabsScreenComponentView *)screenView
{
if (isSelected) {
tabBarItem.selectedImage = image;
} else {
tabBarItem.image = image;
}
// A layout pass is required because the image might be loaded asynchronously,
// after the tab bar has already been attached to the window.
// This code handles case where image passed by the user is not
// of appropriate size & needs to be readjusted. W/o additional
// layout here the icon would be displayed with original dimensions.
UIViewController *parent = screenView.controller.parentViewController;
if ([parent isKindOfClass:[UITabBarController class]]) {
UITabBarController *tabBarVC = (UITabBarController *)parent;
[tabBarVC.tabBar setNeedsLayout];
}
}
+ (void)configureTabBarAppearance:(nonnull UITabBarAppearance *)tabBarAppearance
fromAppearanceProps:(nonnull NSDictionary *)appearanceProps
{
if (appearanceProps[@"tabBarBackgroundColor"] != nil) {
tabBarAppearance.backgroundColor = [RCTConvert UIColor:appearanceProps[@"tabBarBackgroundColor"]];
}
if (appearanceProps[@"tabBarBlurEffect"] != nil) {
NSString *blurEffectString = [appearanceProps[@"tabBarBlurEffect"] isKindOfClass:[NSString class]]
? appearanceProps[@"tabBarBlurEffect"]
: @"none";
if (![blurEffectString isEqualToString:@"systemDefault"]) {
tabBarAppearance.backgroundEffect = rnscreens::conversion::RNSUIBlurEffectFromString(blurEffectString);
}
}
if (appearanceProps[@"tabBarShadowColor"] != nil) {
tabBarAppearance.shadowColor = [RCTConvert UIColor:appearanceProps[@"tabBarShadowColor"]];
}
if ([appearanceProps[@"stacked"] isKindOfClass:[NSDictionary class]]) {
[self configureTabBarItemAppearance:tabBarAppearance.stackedLayoutAppearance
fromItemAppearanceProps:appearanceProps[@"stacked"]];
}
if ([appearanceProps[@"inline"] isKindOfClass:[NSDictionary class]]) {
[self configureTabBarItemAppearance:tabBarAppearance.inlineLayoutAppearance
fromItemAppearanceProps:appearanceProps[@"inline"]];
}
if ([appearanceProps[@"compactInline"] isKindOfClass:[NSDictionary class]]) {
[self configureTabBarItemAppearance:tabBarAppearance.compactInlineLayoutAppearance
fromItemAppearanceProps:appearanceProps[@"compactInline"]];
}
}
+ (void)configureTabBarItemAppearance:(nonnull UITabBarItemAppearance *)tabBarItemAppearance
fromItemAppearanceProps:(nonnull NSDictionary *)itemAppearanceProps
{
if ([itemAppearanceProps[@"normal"] isKindOfClass:[NSDictionary class]]) {
[self configureTabBarItemStateAppearance:tabBarItemAppearance.normal
fromItemStateAppearanceProps:itemAppearanceProps[@"normal"]];
}
if ([itemAppearanceProps[@"selected"] isKindOfClass:[NSDictionary class]]) {
[self configureTabBarItemStateAppearance:tabBarItemAppearance.selected
fromItemStateAppearanceProps:itemAppearanceProps[@"selected"]];
}
if ([itemAppearanceProps[@"focused"] isKindOfClass:[NSDictionary class]]) {
[self configureTabBarItemStateAppearance:tabBarItemAppearance.focused
fromItemStateAppearanceProps:itemAppearanceProps[@"focused"]];
}
if ([itemAppearanceProps[@"disabled"] isKindOfClass:[NSDictionary class]]) {
[self configureTabBarItemStateAppearance:tabBarItemAppearance.disabled
fromItemStateAppearanceProps:itemAppearanceProps[@"disabled"]];
}
}
+ (void)configureTabBarItemStateAppearance:(nonnull UITabBarItemStateAppearance *)tabBarItemStateAppearance
fromItemStateAppearanceProps:(nonnull NSDictionary *)itemStateAppearanceProps
{
NSMutableDictionary *titleTextAttributes = [[NSMutableDictionary alloc] init];
if (itemStateAppearanceProps[@"tabBarItemTitleFontFamily"] != nil ||
itemStateAppearanceProps[@"tabBarItemTitleFontSize"] != nil ||
itemStateAppearanceProps[@"tabBarItemTitleFontWeight"] != nil ||
itemStateAppearanceProps[@"tabBarItemTitleFontStyle"] != nil) {
titleTextAttributes[NSFontAttributeName] =
[RCTFont updateFont:tabBarItemStateAppearance.titleTextAttributes[NSFontAttributeName]
withFamily:itemStateAppearanceProps[@"tabBarItemTitleFontFamily"]
size:itemStateAppearanceProps[@"tabBarItemTitleFontSize"]
weight:itemStateAppearanceProps[@"tabBarItemTitleFontWeight"]
style:itemStateAppearanceProps[@"tabBarItemTitleFontStyle"]
variant:nil
scaleMultiplier:1.0];
}
if (itemStateAppearanceProps[@"tabBarItemTitleFontColor"] != nil) {
titleTextAttributes[NSForegroundColorAttributeName] =
[RCTConvert UIColor:itemStateAppearanceProps[@"tabBarItemTitleFontColor"]];
}
if ([titleTextAttributes count] > 0) {
tabBarItemStateAppearance.titleTextAttributes = titleTextAttributes;
}
if (itemStateAppearanceProps[@"tabBarItemBadgeBackgroundColor"] != nil) {
tabBarItemStateAppearance.badgeBackgroundColor =
[RCTConvert UIColor:itemStateAppearanceProps[@"tabBarItemBadgeBackgroundColor"]];
}
if (itemStateAppearanceProps[@"tabBarItemIconColor"] != nil) {
tabBarItemStateAppearance.iconColor = [RCTConvert UIColor:itemStateAppearanceProps[@"tabBarItemIconColor"]];
}
if (itemStateAppearanceProps[@"tabBarItemTitlePositionAdjustment"] != nil) {
tabBarItemStateAppearance.titlePositionAdjustment =
[RCTConvert UIOffset:itemStateAppearanceProps[@"tabBarItemTitlePositionAdjustment"]];
}
}
@end

View File

@@ -0,0 +1,70 @@
#pragma once
#import "RNSDefines.h"
#if RNS_BOTTOM_ACCESSORY_AVAILABLE
#import <UIKit/UIKit.h>
#import "RNSBottomTabsAccessoryComponentView.h"
#import "RNSBottomTabsAccessoryContentComponentView.h"
#import "RNSEnums.h"
NS_ASSUME_NONNULL_BEGIN
/**
* @class RNSBottomAccessoryHelper
* @brief Class responsible for managing accessory size and environment changes for
* RNSBottomTabsAccessoryComponentView.
*/
API_AVAILABLE(ios(26.0))
@interface RNSBottomAccessoryHelper : NSObject
- (instancetype)initWithBottomAccessoryView:(RNSBottomTabsAccessoryComponentView *)bottomAccessoryView;
/**
* Registers KVO for frames of UIKit's bottom accessory wrapper view.
* It must be called after `RNSBottomTabsAccessoryComponentView` or its ancestor is set as `bottomAccessory` on
* `RNSTabBarController`.
*/
- (void)registerForAccessoryFrameChanges;
/**
* Invalidates observers, display link (if it is used); resets internal properties.
*/
- (void)invalidate;
@end
#pragma mark - Content view switching workaround
#if defined(__cplusplus) && REACT_NATIVE_VERSION_MINOR >= 82
/**
* Due to *synchronous* events not being actually *synchronous*, we are unable to handle layout modifications
* in reaction to environment change (e.g. subviews being mounted/unmounted; changes to size/origin are synchronous
* thanks to synchronous state updates and work correctly).
* In order to mitigate this, we introduced a workaround approach: 2 views are rendered all the time on top of each
* other. One is for `regular` environment and the second one is for `inline` environment. When environment changes, we
* swap which view is actually visible by changing opacity.
*/
@interface RNSBottomAccessoryHelper ()
/**
* Allows to set which `RNSBottomTabsAccessoryContentComponentView` instance should be visible
* for which accessory environment.
*/
- (void)setContentView:(nullable RNSBottomTabsAccessoryContentComponentView *)contentView
forEnvironment:(RNSBottomTabsAccessoryEnvironment)environment;
/**
* If `contentView` is set for both environments, sets opacity according to current tab accessory `environent`.
* Otherwise, it is a no-op.
*/
- (void)handleContentViewVisibilityForEnvironmentIfNeeded;
@end
#endif // defined(__cplusplus) && REACT_NATIVE_VERSION_MINOR >= 82
NS_ASSUME_NONNULL_END
#endif // RNS_BOTTOM_ACCESSORY_AVAILABLE

View File

@@ -0,0 +1,209 @@
#import "RNSBottomAccessoryHelper.h"
#import "RNSBottomTabsAccessoryShadowStateProxy.h"
#if RNS_BOTTOM_ACCESSORY_AVAILABLE
#import <React/RCTAssert.h>
#import <cxxreact/ReactNativeVersion.h>
namespace react = facebook::react;
@implementation RNSBottomAccessoryHelper {
RNSBottomTabsAccessoryComponentView *__weak _bottomAccessoryView;
#if REACT_NATIVE_VERSION_MINOR < 82
BOOL _initialStateUpdateSent;
CADisplayLink *_displayLink;
#else // REACT_NATIVE_VERSION_MINOR < 82
RNSBottomTabsAccessoryContentComponentView *__weak _regularContentView;
RNSBottomTabsAccessoryContentComponentView *__weak _inlineContentView;
#endif // REACT_NATIVE_VERSION_MINOR < 82
id<UITraitChangeRegistration> _traitChangeRegistration;
}
- (instancetype)initWithBottomAccessoryView:(RNSBottomTabsAccessoryComponentView *)bottomAccessoryView
{
if (self = [super init]) {
_bottomAccessoryView = bottomAccessoryView;
[self initState];
_traitChangeRegistration = [self registerForAccessoryEnvironmentChanges];
}
return self;
}
- (void)initState
{
#if REACT_NATIVE_VERSION_MINOR < 82
_initialStateUpdateSent = NO;
_displayLink = nil;
#else // REACT_NATIVE_VERSION_MINOR < 82
_regularContentView = nil;
_inlineContentView = nil;
#endif // REACT_NATIVE_VERSION_MINOR < 82
}
#pragma mark - Content view switching workaround
#if REACT_NATIVE_VERSION_MINOR >= 82
- (BOOL)isContentViewSwitchingWorkaroundActive
{
return _regularContentView != nil && _inlineContentView != nil;
}
- (void)setContentView:(RNSBottomTabsAccessoryContentComponentView *)contentView
forEnvironment:(RNSBottomTabsAccessoryEnvironment)environment
{
switch (environment) {
case RNSBottomTabsAccessoryEnvironmentRegular:
_regularContentView = contentView;
break;
case RNSBottomTabsAccessoryEnvironmentInline:
_inlineContentView = contentView;
break;
default:
RCTLogError(@"[RNScreens] Unsupported BottomTabsAccessory environment");
}
[self handleContentViewVisibilityForEnvironmentIfNeeded];
}
- (void)handleContentViewVisibilityForEnvironmentIfNeeded
{
if (!self.isContentViewSwitchingWorkaroundActive) {
return;
}
switch (self->_bottomAccessoryView.traitCollection.tabAccessoryEnvironment) {
case UITabAccessoryEnvironmentInline:
_regularContentView.layer.opacity = 0.0;
_inlineContentView.layer.opacity = 1.0;
break;
default:
_regularContentView.layer.opacity = 1.0;
_inlineContentView.layer.opacity = 0.0;
break;
}
}
#endif // REACT_NATIVE_VERSION_MINOR >= 82
#pragma mark - Observing environment changes
- (id<UITraitChangeRegistration>)registerForAccessoryEnvironmentChanges
{
return [_bottomAccessoryView
registerForTraitChanges:@[ [UITraitTabAccessoryEnvironment class] ]
withHandler:^(__kindof id<UITraitEnvironment>, UITraitCollection *previousTraitCollection) {
UITabAccessoryEnvironment environment =
self->_bottomAccessoryView.traitCollection.tabAccessoryEnvironment;
[self->_bottomAccessoryView.reactEventEmitter emitOnEnvironmentChangeIfNeeded:environment];
#if REACT_NATIVE_VERSION_MINOR >= 82
[self handleContentViewVisibilityForEnvironmentIfNeeded];
#endif // REACT_NATIVE_VERSION_MINOR >= 82
}];
}
#pragma mark - Observing frame changes
- (void)registerForAccessoryFrameChanges
{
[self.nativeWrapperView addObserver:self forKeyPath:@"center" options:NSKeyValueObservingOptionInitial context:nil];
}
- (void)observeValueForKeyPath:(NSString *)keyPath
ofObject:(id)object
change:(NSDictionary *)change
context:(void *)context
{
[self notifyWrapperViewFrameHasChanged];
}
- (UIView *)nativeWrapperView
{
RCTAssert(
_bottomAccessoryView.superview.superview != nil,
@"[RNScreens] RNSBottomTabsAccessoryComponentView must be the set as bottom accessory.");
return _bottomAccessoryView.superview.superview;
}
- (void)notifyWrapperViewFrameHasChanged
{
#if REACT_NATIVE_VERSION_MINOR < 82
// Make sure that bottom accessory's size is sent to ShadowNode as soon as possible.
// We set origin to (0,0) because initially self.nativeWrapperView's origin is incorrect.
// We want the enable the display link as well so that it takes over later with correct origin.
if (!_initialStateUpdateSent) {
CGRect frame = CGRectMake(0, 0, self.nativeWrapperView.frame.size.width, self.nativeWrapperView.frame.size.height);
[_bottomAccessoryView.shadowStateProxy updateShadowStateWithFrame:frame];
_initialStateUpdateSent = YES;
}
if (_displayLink == nil) {
[self setupDisplayLink];
}
#else // REACT_NATIVE_VERSION_MINOR < 82
// We use self.nativeWrapperView because it has both the size and the origin
// that we want to send to the ShadowNode.
[_bottomAccessoryView.shadowStateProxy updateShadowStateWithFrame:self.nativeWrapperView.frame];
#endif // REACT_NATIVE_VERSION_MINOR < 82
}
#if REACT_NATIVE_VERSION_MINOR < 82
- (void)setupDisplayLink
{
_displayLink = [CADisplayLink displayLinkWithTarget:self selector:@selector(handleDisplayLink:)];
[_displayLink addToRunLoop:[NSRunLoop mainRunLoop] forMode:NSRunLoopCommonModes];
}
- (void)handleDisplayLink:(CADisplayLink *)sender
{
// We use self.nativeWrapperView because it has both the size and the origin
// that we want to send to the ShadowNode.
CGRect presentationFrame = self.nativeWrapperView.layer.presentationLayer.frame;
if (CGRectEqualToRect(presentationFrame, CGRectZero)) {
return;
}
[_bottomAccessoryView.shadowStateProxy updateShadowStateWithFrame:presentationFrame];
// self.nativeWrapperView.frame is set to final value at the beginning of the transition.
// When frame from presentation layer matches self.nativeWrapperView.frame, it indicates that
// the transition is over and we can disable the display link.
if (CGRectEqualToRect(presentationFrame, self.nativeWrapperView.frame)) {
[self invalidateDisplayLink];
}
}
- (void)invalidateDisplayLink
{
[_displayLink invalidate];
_displayLink = nil;
}
#endif // REACT_NATIVE_VERSION_MINOR < 82
#pragma mark - Invalidation
- (void)invalidate
{
[_bottomAccessoryView unregisterForTraitChanges:_traitChangeRegistration];
_traitChangeRegistration = nil;
// Using nativeWrapperView directly here to avoid failing RCTAssert in self.nativeWrapperView.
// If we're called from didMoveToWindow, it's not a problem, but I'm not sure if this will always be the case.
[_bottomAccessoryView.superview.superview removeObserver:self forKeyPath:@"center"];
_bottomAccessoryView = nil;
#if REACT_NATIVE_VERSION_MINOR < 82
[self invalidateDisplayLink];
#else // REACT_NATIVE_VERSION_MINOR < 82
_regularContentView = nil;
_inlineContentView = nil;
#endif // REACT_NATIVE_VERSION_MINOR < 82
}
@end
#endif // RNS_BOTTOM_ACCESSORY_AVAILABLE

View File

@@ -0,0 +1,97 @@
#pragma once
#import "RNSBottomTabsAccessoryEventEmitter.h"
#import "RNSBottomTabsHostComponentView.h"
#import "RNSReactBaseView.h"
#if RCT_NEW_ARCH_ENABLED
#import "RNSViewControllerInvalidating.h"
#else
#import <React/RCTBridge.h>
#import <React/RCTInvalidating.h>
#endif
#if RCT_NEW_ARCH_ENABLED && defined(__cplusplus)
#import <rnscreens/RNSBottomTabsAccessoryComponentDescriptor.h>
#endif // RCT_NEW_ARCH_ENABLED && defined(__cplusplus)
NS_ASSUME_NONNULL_BEGIN
#if RNS_BOTTOM_ACCESSORY_AVAILABLE
@class RNSBottomAccessoryHelper;
@class RNSBottomTabsAccessoryShadowStateProxy;
#endif // RNS_BOTTOM_ACCESSORY_AVAILABLE
@interface RNSBottomTabsAccessoryComponentView : RNSReactBaseView <
#if RCT_NEW_ARCH_ENABLED
RNSViewControllerInvalidating
#else // RCT_NEW_ARCH_ENABLED
RCTInvalidating
#endif // RCT_NEW_ARCH_ENABLED
>
#if !RCT_NEW_ARCH_ENABLED
- (instancetype)initWithFrame:(CGRect)frame bridge:(RCTBridge *)bridge;
@property (nonatomic, weak, readonly, nullable) RCTBridge *bridge;
#endif // !RCT_NEW_ARCH_ENABLED
#if RNS_BOTTOM_ACCESSORY_AVAILABLE
/**
* If not null, the bottom accesory's helper that handles accessory size and environment changes.
* It also manages *content view switching workaround* for RN >= 0.82.
*/
@property (nonatomic, strong, readonly, nullable) RNSBottomAccessoryHelper *helper;
/**
* If not null, the bottom accesory's shadow state proxy that handles communication with ShadowTree.
*/
@property (nonatomic, strong, readonly, nullable) RNSBottomTabsAccessoryShadowStateProxy *shadowStateProxy;
#endif // RNS_BOTTOM_ACCESSORY_AVAILABLE
/**
* If not null, the bottom tabs host view that this accessory component view belongs to.
*/
@property (nonatomic, weak, nullable) RNSBottomTabsHostComponentView *bottomTabsHostView;
@end
#pragma mark - React Events
#if RNS_BOTTOM_ACCESSORY_AVAILABLE
@interface RNSBottomTabsAccessoryComponentView ()
/**
* Use returned object to emit appropriate React Events to Element Tree.
*/
- (nonnull RNSBottomTabsAccessoryEventEmitter *)reactEventEmitter;
#if !RCT_NEW_ARCH_ENABLED
#pragma mark - LEGACY Event blocks
@property (nonatomic, copy) RCTDirectEventBlock onEnvironmentChange;
#endif // !RCT_NEW_ARCH_ENABLED
@end
#endif // RNS_BOTTOM_ACCESSORY_AVAILABLE
#pragma mark - Hidden from Swift
#if RCT_NEW_ARCH_ENABLED && defined(__cplusplus)
@interface RNSBottomTabsAccessoryComponentView ()
- (facebook::react::RNSBottomTabsAccessoryShadowNode::ConcreteState::Shared)state;
@end
#endif // RCT_NEW_ARCH_ENABLED && defined(__cplusplus)
NS_ASSUME_NONNULL_END

View File

@@ -0,0 +1,199 @@
#import "RNSBottomTabsAccessoryComponentView.h"
#import "RNSBottomAccessoryHelper.h"
#import "RNSBottomTabsAccessoryShadowStateProxy.h"
#if RNS_BOTTOM_ACCESSORY_AVAILABLE
#if RCT_NEW_ARCH_ENABLED
#import <rnscreens/RNSBottomTabsAccessoryComponentDescriptor.h>
#endif // RCT_NEW_ARCH_ENABLED
namespace react = facebook::react;
#endif // RNS_BOTTOM_ACCESSORY_AVAILABLE
#pragma mark - View implementation
@implementation RNSBottomTabsAccessoryComponentView {
#if RNS_BOTTOM_ACCESSORY_AVAILABLE
RNSBottomAccessoryHelper *_helper API_AVAILABLE(ios(26.0));
RNSBottomTabsAccessoryShadowStateProxy *_shadowStateProxy API_AVAILABLE(ios(26.0));
RNSBottomTabsAccessoryEventEmitter *_Nonnull _reactEventEmitter;
#endif // RNS_BOTTOM_ACCESSORY_AVAILABLE
RNSBottomTabsHostComponentView *__weak _Nullable _bottomTabsHostView;
#if RCT_NEW_ARCH_ENABLED
react::RNSBottomTabsAccessoryShadowNode::ConcreteState::Shared _state;
#else // RCT_NEW_ARCH_ENABLED
__weak RCTBridge *_bridge;
#endif // RCT_NEW_ARCH_ENABLED
}
- (instancetype)initWithFrame:(CGRect)frame
{
if (self = [super initWithFrame:frame]) {
[self initState];
}
return self;
}
#if !RCT_NEW_ARCH_ENABLED
- (instancetype)initWithFrame:(CGRect)frame bridge:(RCTBridge *)bridge
{
if (self = [self initWithFrame:frame]) {
_bridge = bridge;
}
return self;
}
- (RCTBridge *)bridge
{
return _bridge;
}
#endif // !RCT_NEW_ARCH_ENABLED
- (void)initState
{
#if RNS_BOTTOM_ACCESSORY_AVAILABLE
if (@available(iOS 26, *)) {
_helper = [[RNSBottomAccessoryHelper alloc] initWithBottomAccessoryView:self];
_shadowStateProxy = [[RNSBottomTabsAccessoryShadowStateProxy alloc] initWithBottomAccessoryView:self];
_reactEventEmitter = [RNSBottomTabsAccessoryEventEmitter new];
}
#endif // RNS_BOTTOM_ACCESSORY_AVAILABLE
_bottomTabsHostView = nil;
}
#pragma mark - UIKit callbacks
#if RNS_BOTTOM_ACCESSORY_AVAILABLE
- (void)didMoveToWindow
{
if (self.window != nil) {
[_helper registerForAccessoryFrameChanges];
} else {
#if RCT_NEW_ARCH_ENABLED
[self invalidateController];
#else // RCT_NEW_ARCH_ENABLED
[self invalidate];
#endif // RCT_NEW_ARCH_ENABLED
}
}
#endif // RNS_BOTTOM_ACCESSORY_AVAILABLE
#if RCT_NEW_ARCH_ENABLED
#pragma mark - RCTViewComponentViewProtocol
#if RNS_BOTTOM_ACCESSORY_AVAILABLE
- (void)updateState:(const react::State::Shared &)state oldState:(const react::State::Shared &)oldState
{
[super updateState:state oldState:oldState];
_state = std::static_pointer_cast<const react::RNSBottomTabsAccessoryComponentDescriptor::ConcreteState>(state);
}
- (void)updateEventEmitter:(const facebook::react::EventEmitter::Shared &)eventEmitter
{
[super updateEventEmitter:eventEmitter];
const auto &castedEventEmitter =
std::static_pointer_cast<const react::RNSBottomTabsAccessoryEventEmitter>(eventEmitter);
[_reactEventEmitter updateEventEmitter:castedEventEmitter];
}
+ (react::ComponentDescriptorProvider)componentDescriptorProvider
{
return react::concreteComponentDescriptorProvider<react::RNSBottomTabsAccessoryComponentDescriptor>();
}
#endif // RNS_BOTTOM_ACCESSORY_AVAILABLE
+ (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;
}
- (facebook::react::RNSBottomTabsAccessoryShadowNode::ConcreteState::Shared)state
{
return _state;
}
#pragma mark - RNSViewControllerInvalidating
- (void)invalidateController
{
[self invalidateImpl];
}
- (BOOL)shouldInvalidateOnMutation:(const facebook::react::ShadowViewMutation &)mutation
{
// For bottom tabs, Host is responsible for invalidating children.
return NO;
}
#else // RCT_NEW_ARCH_ENABLED
#pragma mark - LEGACY architecture implementation
#if RNS_BOTTOM_ACCESSORY_AVAILABLE
- (void)setOnEnvironmentChange:(RCTDirectEventBlock)onEnvironmentChange
{
[self.reactEventEmitter setOnEnvironmentChange:onEnvironmentChange];
}
#endif // RNS_BOTTOM_ACCESSORY_AVAILABLE
#pragma mark - RCTInvalidating
- (void)invalidate
{
[self invalidateImpl];
}
#endif // RCT_NEW_ARCH_ENABLED
- (void)invalidateImpl
{
#if RNS_BOTTOM_ACCESSORY_AVAILABLE
[_helper invalidate];
_helper = nil;
[_shadowStateProxy invalidate];
_shadowStateProxy = nil;
#endif // RNS_BOTTOM_ACCESSORY_AVAILABLE
#if RCT_NEW_ARCH_ENABLED
_state.reset();
#endif // RCT_NEW_ARCH_ENABLED
}
#pragma mark - React events
#if RNS_BOTTOM_ACCESSORY_AVAILABLE
- (nonnull RNSBottomTabsAccessoryEventEmitter *)reactEventEmitter
{
RCTAssert(_reactEventEmitter != nil, @"[RNScreens] Attempt to access uninitialized _reactEventEmitter");
return _reactEventEmitter;
}
#endif // RNS_BOTTOM_ACCESSORY_AVAILABLE
@end
#if RCT_NEW_ARCH_ENABLED
#pragma mark - View class exposure
Class<RCTComponentViewProtocol> RNSBottomTabsAccessoryCls(void)
{
return RNSBottomTabsAccessoryComponentView.class;
}
#endif // RCT_NEW_ARCH_ENABLED

View File

@@ -0,0 +1,15 @@
#pragma once
#if !RCT_NEW_ARCH_ENABLED
#import <React/RCTViewManager.h>
NS_ASSUME_NONNULL_BEGIN
@interface RNSBottomTabsAccessoryComponentViewManager : RCTViewManager
@end
NS_ASSUME_NONNULL_END
#endif // !RCT_NEW_ARCH_ENABLED

View File

@@ -0,0 +1,25 @@
#import "RNSBottomTabsAccessoryComponentViewManager.h"
#if !RCT_NEW_ARCH_ENABLED
#import <React/RCTImageLoader.h>
#import "RNSBottomTabsAccessoryComponentView.h"
@implementation RNSBottomTabsAccessoryComponentViewManager
// TODO: This is legacy arch only - remove when no longer needed
RCT_EXPORT_MODULE(RNSBottomTabsAccessoryManager)
- (UIView *)view
{
// For Paper, we need to initialize TabsAccessory with bridge
return [[RNSBottomTabsAccessoryComponentView alloc] initWithFrame:CGRectZero bridge:self.bridge];
}
#pragma mark - LEGACY Events
RCT_EXPORT_VIEW_PROPERTY(onEnvironmentChange, RCTDirectEventBlock);
@end
#endif // !RCT_NEW_ARCH_ENABLED

View File

@@ -0,0 +1,24 @@
#pragma once
#import "RNSDefines.h"
#import "RNSEnums.h"
#import "RNSReactBaseView.h"
#if defined(__cplusplus)
#import <cxxreact/ReactNativeVersion.h>
#endif // defined(__cplusplus)
NS_ASSUME_NONNULL_BEGIN
@interface RNSBottomTabsAccessoryContentComponentView : RNSReactBaseView
#if RNS_BOTTOM_ACCESSORY_AVAILABLE && defined(__cplusplus) && REACT_NATIVE_VERSION_MINOR >= 82
@property (nonatomic) RNSBottomTabsAccessoryEnvironment environment;
#endif // RNS_BOTTOM_ACCESSORY_AVAILABLE && defined(__cplusplus) && REACT_NATIVE_VERSION_MINOR >= 82
@end
NS_ASSUME_NONNULL_END

View File

@@ -0,0 +1,122 @@
#import "RNSBottomTabsAccessoryContentComponentView.h"
#import "RNSBottomAccessoryHelper.h"
#import "RNSBottomTabsAccessoryComponentView.h"
#import "RNSConversions.h"
#if RCT_NEW_ARCH_ENABLED
#import <react/renderer/components/rnscreens/ComponentDescriptors.h>
#endif // RCT_NEW_ARCH_ENABLED
namespace react = facebook::react;
#pragma mark - View implementation
@implementation RNSBottomTabsAccessoryContentComponentView {
#if RNS_BOTTOM_ACCESSORY_AVAILABLE && REACT_NATIVE_VERSION_MINOR >= 82
RNSBottomTabsAccessoryComponentView *__weak _Nullable _accessoryView;
#endif // RNS_BOTTOM_ACCESSORY_AVAILABLE && REACT_NATIVE_VERSION_MINOR >= 82
}
- (instancetype)initWithFrame:(CGRect)frame
{
self = [super initWithFrame:frame];
return self;
}
#pragma mark - UIKit callbacks
#if RNS_BOTTOM_ACCESSORY_AVAILABLE && REACT_NATIVE_VERSION_MINOR >= 82
- (void)didMoveToWindow
{
if ([self.superview isKindOfClass:[RNSBottomTabsAccessoryComponentView class]]) {
RNSBottomTabsAccessoryComponentView *accessoryView =
static_cast<RNSBottomTabsAccessoryComponentView *>(self.superview);
_accessoryView = accessoryView;
[_accessoryView.helper setContentView:(self.window != nil ? self : nil) forEnvironment:_environment];
} else {
[_accessoryView.helper setContentView:nil forEnvironment:_environment];
_accessoryView = nil;
}
}
// `RCTViewComponentView` uses this deprecated callback to invalidate layer when trait collection
// `hasDifferentColorAppearanceComparedToTraitCollection`. This updates opacity which breaks our
// content view switching workaround. To mitigate this, we update content view visibility after
// RCTViewComponentView handles the change. We need to use the same deprecated callback as it's
// called after callbacks registered via the new API.
- (void)traitCollectionDidChange:(UITraitCollection *)previousTraitCollection
{
[super traitCollectionDidChange:previousTraitCollection];
if ([self.traitCollection hasDifferentColorAppearanceComparedToTraitCollection:previousTraitCollection]) {
[_accessoryView.helper handleContentViewVisibilityForEnvironmentIfNeeded];
}
}
#endif // RNS_BOTTOM_ACCESSORY_AVAILABLE && REACT_NATIVE_VERSION_MINOR >= 82
#if RCT_NEW_ARCH_ENABLED
#pragma mark - RCTViewComponentViewProtocol
#if RNS_BOTTOM_ACCESSORY_AVAILABLE
#if REACT_NATIVE_VERSION_MINOR >= 82
- (void)updateProps:(const facebook::react::Props::Shared &)props
oldProps:(const facebook::react::Props::Shared &)oldProps
{
const auto &oldComponentProps = *std::static_pointer_cast<const react::RNSBottomTabsAccessoryContentProps>(_props);
const auto &newComponentProps = *std::static_pointer_cast<const react::RNSBottomTabsAccessoryContentProps>(props);
if (newComponentProps.environment != oldComponentProps.environment) {
_environment =
rnscreens::conversion::RNSBottomTabsAccessoryEnvironmentFromCppEquivalent(newComponentProps.environment);
}
[super updateProps:props oldProps:oldProps];
}
- (void)finalizeUpdates:(RNComponentViewUpdateMask)updateMask
{
[super finalizeUpdates:updateMask];
// In finalize updates, `invalidateLayer` is called. It resets `view.layer.opacity`
// which we use to switch visible bottom accessory content view. In order to mitigate
// this, we update visibility after `[super finalizeUpdates:updateMask]`. Without this,
// both content views are visible on first render. It does not happen on subsequent
// renders because `updateState` is called before trait changes but there might be other
// cases when `finalizeUpdates` will run so to make sure that we maintain correct
// visibility, we call `handleContentViewVisibilityForEnvironmentIfNeeded` here.
[_accessoryView.helper handleContentViewVisibilityForEnvironmentIfNeeded];
}
#endif // REACT_NATIVE_VERSION_MINOR >= 82
+ (react::ComponentDescriptorProvider)componentDescriptorProvider
{
return react::concreteComponentDescriptorProvider<react::RNSBottomTabsAccessoryContentComponentDescriptor>();
}
#endif // RNS_BOTTOM_ACCESSORY_AVAILABLE
+ (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;
}
#endif // RCT_NEW_ARCH_ENABLED
@end
#if RCT_NEW_ARCH_ENABLED
#pragma mark - View class exposure
Class<RCTComponentViewProtocol> RNSBottomTabsAccessoryContentCls(void)
{
return RNSBottomTabsAccessoryContentComponentView.class;
}
#endif // RCT_NEW_ARCH_ENABLED

View File

@@ -0,0 +1,52 @@
#pragma once
#import "RNSDefines.h"
#if RNS_BOTTOM_ACCESSORY_AVAILABLE
#import <Foundation/Foundation.h>
#import <UIKit/UIKit.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 // defined(__cplusplus) && RCT_NEW_ARCH_ENABLED
#if !RCT_NEW_ARCH_ENABLED
#import <React/RCTComponent.h>
#endif // !RCT_NEW_ARCH_ENABLED
NS_ASSUME_NONNULL_BEGIN
@interface RNSBottomTabsAccessoryEventEmitter : NSObject
- (BOOL)emitOnEnvironmentChangeIfNeeded:(UITabAccessoryEnvironment)environment API_AVAILABLE(ios(26.0));
@end
#pragma mark - Hidden from Swift
#if defined(__cplusplus)
@interface RNSBottomTabsAccessoryEventEmitter ()
#if RCT_NEW_ARCH_ENABLED
- (void)updateEventEmitter:(const std::shared_ptr<const react::RNSBottomTabsAccessoryEventEmitter> &)emitter;
#else
#pragma mark - LEGACY Event emitter blocks
@property (nonatomic, copy) RCTDirectEventBlock onEnvironmentChange;
#endif // RCT_NEW_ARCH_ENABLED
@end
#endif // defined(__cplusplus)
NS_ASSUME_NONNULL_END
#endif // RNS_BOTTOM_ACCESSORY_AVAILABLE

View File

@@ -0,0 +1,80 @@
#import "RNSBottomTabsAccessoryEventEmitter.h"
#if RNS_BOTTOM_ACCESSORY_AVAILABLE
#import <React/RCTLog.h>
#import "RNSConversions.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 RNSBottomTabsAccessoryEventEmitter {
#if RCT_NEW_ARCH_ENABLED
std::shared_ptr<const react::RNSBottomTabsAccessoryEventEmitter> _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::RNSBottomTabsAccessoryEventEmitter> &)emitter
{
_reactEventEmitter = emitter;
}
#endif // RCT_NEW_ARCH_ENABLED
- (BOOL)emitOnEnvironmentChangeIfNeeded:(UITabAccessoryEnvironment)environment API_AVAILABLE(ios(26.0))
{
#if RCT_NEW_ARCH_ENABLED
if (_reactEventEmitter != nullptr) {
auto payloadEnvironment =
rnscreens::conversion::RNSBottomTabsAccessoryOnEnvironmentChangePayloadFromUITabAccessoryEnvironment(
environment);
// If environment is other than `regular` or `inline`, we don't emit the event.
if (!payloadEnvironment.has_value()) {
return NO;
}
_reactEventEmitter->onEnvironmentChange({.environment = payloadEnvironment.value()});
return YES;
} else {
RCTLogWarn(@"[RNScreens] Skipped OnEnvironmentChange event emission due to nullish emitter");
return NO;
}
#else
if (self.onEnvironmentChange) {
NSString *environmentString =
rnscreens::conversion::RNSBottomTabsAccessoryOnEnvironmentChangePayloadFromUITabAccessoryEnvironment(
environment);
// If environment is other than `regular` or `inline`, we don't emit the event.
if (environmentString == nil) {
return NO;
}
self.onEnvironmentChange(@{@"environment" : environmentString});
return YES;
} else {
RCTLogWarn(@"[RNScreens] Skipped OnEnvironmentChange event emission due to nullish emitter");
return NO;
}
#endif // RCT_NEW_ARCH_ENABLED
}
@end
#endif // RNS_BOTTOM_ACCESSORY_AVAILABLE

View File

@@ -0,0 +1,35 @@
#pragma once
#import "RNSBottomAccessoryHelper.h"
#if RNS_BOTTOM_ACCESSORY_AVAILABLE
NS_ASSUME_NONNULL_BEGIN
@class RNSBottomTabsAccessoryComponentView;
/**
* @class RNSBottomTabsAccessoryShadowStateProxy
* @brief Class responsible for communication with ShadowTree for
* RNSBottomTabsAccessoryComponentView.
*/
API_AVAILABLE(ios(26.0))
@interface RNSBottomTabsAccessoryShadowStateProxy : NSObject
- (instancetype)initWithBottomAccessoryView:(RNSBottomTabsAccessoryComponentView *)bottomAccessoryView;
/**
* Updates bottom accessory's frame in ShadowTree if new frame is different than previously sent frame.
*/
- (void)updateShadowStateWithFrame:(CGRect)frame;
/**
* Resets internal properties.
*/
- (void)invalidate;
@end
NS_ASSUME_NONNULL_END
#endif // RNS_BOTTOM_ACCESSORY_AVAILABLE

View File

@@ -0,0 +1,62 @@
#import "RNSBottomTabsAccessoryShadowStateProxy.h"
#if RNS_BOTTOM_ACCESSORY_AVAILABLE
#if RCT_NEW_ARCH_ENABLED
#import <React/RCTConversions.h>
#import <rnscreens/RNSBottomTabsAccessoryShadowNode.h>
#else // RCT_NEW_ARCH_ENABLED
#import <React/RCTUIManager.h>
#endif // RCT_NEW_ARCH_ENABLED
@implementation RNSBottomTabsAccessoryShadowStateProxy {
RNSBottomTabsAccessoryComponentView *__weak _bottomAccessoryView;
CGRect _previousFrame;
}
- (instancetype)initWithBottomAccessoryView:(RNSBottomTabsAccessoryComponentView *)bottomAccessoryView
{
if (self = [super init]) {
_bottomAccessoryView = bottomAccessoryView;
[self initState];
}
return self;
}
- (void)initState
{
_previousFrame = CGRectZero;
}
- (void)updateShadowStateWithFrame:(CGRect)frame
{
if (!CGRectEqualToRect(frame, _previousFrame)) {
#if RCT_NEW_ARCH_ENABLED
if (_bottomAccessoryView.state != nullptr) {
auto newState =
react::RNSBottomTabsAccessoryState{RCTSizeFromCGSize(frame.size), RCTPointFromCGPoint(frame.origin)};
_bottomAccessoryView.state->updateState(
std::move(newState)
#if REACT_NATIVE_VERSION_MINOR >= 82
,
facebook::react::EventQueue::UpdateMode::unstable_Immediate
#endif // REACT_NATIVE_VERSION_MINOR >= 82
);
_previousFrame = frame;
}
#else // RCT_NEW_ARCH_ENABLED
[_bottomAccessoryView.bridge.uiManager setSize:frame.size forView:_bottomAccessoryView];
_previousFrame = frame;
#endif // RCT_NEW_ARCH_ENABLED
}
}
- (void)invalidate
{
_previousFrame = CGRectZero;
}
@end
#endif // RNS_BOTTOM_ACCESSORY_AVAILABLE

View File

@@ -0,0 +1,24 @@
#pragma once
#import "RNSBottomTabsHostComponentView.h"
#if defined(__cplusplus) && RCT_NEW_ARCH_ENABLED
#import "RNSBottomTabsShadowNode.h"
#endif // defined(__cplusplus) && RCT_NEW_ARCH_ENABLED
NS_ASSUME_NONNULL_BEGIN
#if defined(__cplusplus) && RCT_NEW_ARCH_ENABLED
@class RCTImageLoader;
@interface RNSBottomTabsHostComponentView (RNSImageLoader)
- (nullable RCTImageLoader *)retrieveImageLoaderFromState:
(facebook::react::RNSBottomTabsShadowNode::ConcreteState::Shared)state;
@end
#endif // defined(__cplusplus) && RCT_NEW_ARCH_ENABLED
NS_ASSUME_NONNULL_END

View File

@@ -0,0 +1,23 @@
#import "RNSBottomTabsHostComponentView+RNSImageLoader.h"
#if RCT_NEW_ARCH_ENABLED
#import <React/RCTImageLoader.h>
#import <react/utils/ManagedObjectWrapper.h>
@implementation RNSBottomTabsHostComponentView (RNSImageLoader)
- (nullable RCTImageLoader *)retrieveImageLoaderFromState:
(facebook::react::RNSBottomTabsShadowNode::ConcreteState::Shared)receivedState
{
if (auto imgLoaderPtr = receivedState.get()->getData().getImageLoader().lock()) {
return react::unwrapManagedObject(imgLoaderPtr);
}
RCTLogWarn(@"[RNScreens] unable to retrieve RCTImageLoader");
return nil;
}
@end
#endif // RCT_NEW_ARCH_ENABLED

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

View File

@@ -0,0 +1,130 @@
#pragma once
#import <React/RCTImageSource.h>
#import "RNSBottomTabsScreenEventEmitter.h"
#import "RNSEnums.h"
#import "RNSReactBaseView.h"
#import "RNSSafeAreaProviding.h"
#import "RNSScrollEdgeEffectApplicator.h"
#import "RNSScrollViewBehaviorOverriding.h"
#ifdef RCT_NEW_ARCH_ENABLED
#import "RNSViewControllerInvalidating.h"
#else
#import <React/RCTInvalidating.h>
#endif
NS_ASSUME_NONNULL_BEGIN
@class RNSBottomTabsHostComponentView;
@class RNSTabsScreenViewController;
/**
* Component view with react managed lifecycle. This view serves as root view in hierarchy
* of a particular tab.
*/
@interface RNSBottomTabsScreenComponentView : RNSReactBaseView <
RNSSafeAreaProviding,
#ifdef RCT_NEW_ARCH_ENABLED
RNSViewControllerInvalidating
#else
RCTInvalidating
#endif
>
/**
* View controller responsible for managing tab represented by this component view.
*/
@property (nonatomic, strong, readonly, nullable) RNSTabsScreenViewController *controller;
/**
* If not null, the bottom tabs host view that this tab component view belongs to.
*/
@property (nonatomic, weak, nullable) RNSBottomTabsHostComponentView *reactSuperview;
/**
* Updates [scroll edge effects](https://developer.apple.com/documentation/uikit/uiscrolledgeeffect)
* on a content ScrollView inside the tab screen, if one exists. It uses ScrollViewFinder to find the ScrollView.
*/
- (void)updateContentScrollViewEdgeEffectsIfExists;
@end
#pragma mark - Props
/**
* Properties set on component in JavaScript.
*/
@interface RNSBottomTabsScreenComponentView () <RNSScrollViewBehaviorOverriding, RNSScrollEdgeEffectProviding>
// TODO: All of these properties should be `readonly`. Do this when support for legacy
// architecture is dropped.
@property (nonatomic) BOOL isSelectedScreen;
@property (nonatomic, nullable) NSString *tabKey;
@property (nonatomic, nullable) NSString *badgeValue;
@property (nonatomic, nullable) NSString *tabBarItemTestID;
@property (nonatomic, nullable) NSString *tabBarItemAccessibilityLabel;
@property (nonatomic, readonly) RNSBottomTabsIconType iconType;
@property (nonatomic, strong, readonly, nullable) RCTImageSource *iconImageSource;
@property (nonatomic, strong, readonly, nullable) NSString *iconResourceName;
@property (nonatomic, strong, readonly, nullable) RCTImageSource *selectedIconImageSource;
@property (nonatomic, strong, readonly, nullable) NSString *selectedIconResourceName;
@property (nonatomic, strong, readonly, nullable) UITabBarAppearance *standardAppearance;
@property (nonatomic, strong, readonly, nullable) UITabBarAppearance *scrollEdgeAppearance;
@property (nonatomic, nullable) NSString *title;
@property (nonatomic, readonly) BOOL isTitleUndefined;
@property (nonatomic, readonly) RNSOrientation orientation;
@property (nonatomic) BOOL shouldUseRepeatedTabSelectionPopToRootSpecialEffect;
@property (nonatomic) BOOL shouldUseRepeatedTabSelectionScrollToTopSpecialEffect;
@property (nonatomic, readonly) BOOL overrideScrollViewContentInsetAdjustmentBehavior;
@property (nonatomic, nullable) NSString *tabItemTestID;
@property (nonatomic, nullable) NSString *tabItemAccessibilityLabel;
@property (nonatomic) BOOL tabBarItemNeedsA11yUpdate;
@property (nonatomic) RNSScrollEdgeEffect bottomScrollEdgeEffect;
@property (nonatomic) RNSScrollEdgeEffect leftScrollEdgeEffect;
@property (nonatomic) RNSScrollEdgeEffect rightScrollEdgeEffect;
@property (nonatomic) RNSScrollEdgeEffect topScrollEdgeEffect;
@property (nonatomic) RNSBottomTabsScreenSystemItem systemItem;
@end
#pragma mark - Experimental
@interface RNSBottomTabsScreenComponentView ()
@property (nonatomic) UIUserInterfaceStyle userInterfaceStyle;
@end
#pragma mark - Events
@interface RNSBottomTabsScreenComponentView ()
/**
* Use returned object to emit appropriate React Events to Element Tree.
*/
- (nonnull RNSBottomTabsScreenEventEmitter *)reactEventEmitter;
#if !RCT_NEW_ARCH_ENABLED
#pragma mark - LEGACY Event emitting blocks
@property (nonatomic, copy, nullable) RCTDirectEventBlock onWillAppear;
@property (nonatomic, copy, nullable) RCTDirectEventBlock onDidAppear;
@property (nonatomic, copy, nullable) RCTDirectEventBlock onWillDisappear;
@property (nonatomic, copy, nullable) RCTDirectEventBlock onDidDisappear;
#endif // !RCT_NEW_ARCH_ENABLED
@end
NS_ASSUME_NONNULL_END

View File

@@ -0,0 +1,752 @@
#import "RNSBottomTabsScreenComponentView.h"
#import "NSString+RNSUtility.h"
#import "RNSConversions.h"
#import "RNSDefines.h"
#import "RNSLog.h"
#import "RNSSafeAreaViewNotifications.h"
#import "RNSScrollViewFinder.h"
#import "RNSScrollViewHelper.h"
#import "RNSTabBarAppearanceCoordinator.h"
#import "RNSTabBarController.h"
#if RCT_NEW_ARCH_ENABLED
#import <React/RCTConversions.h>
#import <React/RCTImageSource.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>
#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 _tabBarItemNeedsRecreation;
BOOL _tabBarItemNeedsUpdate;
BOOL _scrollEdgeEffectsNeedUpdate;
#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<const react::RNSBottomTabsScreenProps>();
_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;
_tabBarItemNeedsRecreation = NO;
_tabBarItemNeedsUpdate = NO;
_scrollEdgeEffectsNeedUpdate = NO;
#endif
[self resetProps];
}
- (void)resetProps
{
_isSelectedScreen = NO;
_badgeValue = nil;
_title = nil;
_isTitleUndefined = YES;
_orientation = RNSOrientationInherit;
_standardAppearance = [UITabBarAppearance new];
_scrollEdgeAppearance = nil;
_shouldUseRepeatedTabSelectionPopToRootSpecialEffect = YES;
_shouldUseRepeatedTabSelectionScrollToTopSpecialEffect = YES;
_overrideScrollViewContentInsetAdjustmentBehavior = YES;
_isOverrideScrollViewContentInsetAdjustmentBehaviorSet = NO;
_iconType = RNSBottomTabsIconTypeSfSymbol;
_iconImageSource = nil;
_iconResourceName = nil;
_selectedIconImageSource = nil;
_selectedIconResourceName = nil;
_systemItem = RNSBottomTabsScreenSystemItemNone;
_userInterfaceStyle = UIUserInterfaceStyleUnspecified;
}
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;
}
- (BOOL)shouldInvalidateOnMutation:(const facebook::react::ShadowViewMutation &)mutation
{
// For bottom tabs, Host is responsible for invalidating children.
return NO;
}
#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<RNSTabBarController *>(_controller.tabBarController);
}
#pragma mark - RNSScrollViewBehaviorOverriding
- (BOOL)shouldOverrideScrollViewContentInsetAdjustmentBehavior
{
return self.overrideScrollViewContentInsetAdjustmentBehavior;
}
- (void)overrideScrollViewBehaviorInFirstDescendantChainIfNeeded
{
if ([self shouldOverrideScrollViewContentInsetAdjustmentBehavior]) {
[RNSScrollViewHelper overrideScrollViewBehaviorInFirstDescendantChainFrom:self];
}
}
- (void)updateContentScrollViewEdgeEffectsIfExists
{
[RNSScrollEdgeEffectApplicator applyToScrollView:[RNSScrollViewFinder findScrollViewInFirstDescendantChainFrom:self]
withProvider:self];
}
#pragma mark - Prop update utils
- (void)createTabBarItem
{
UITabBarItem *tabBarItem = nil;
if (_systemItem != RNSBottomTabsScreenSystemItemNone) {
UITabBarSystemItem systemItem =
rnscreens::conversion::RNSBottomTabsScreenSystemItemToUITabBarSystemItem(_systemItem);
tabBarItem = [[UITabBarItem alloc] initWithTabBarSystemItem:systemItem tag:0];
} else {
tabBarItem = [[UITabBarItem alloc] init];
}
_controller.tabBarItem = tabBarItem;
}
- (void)updateTabBarItem
{
UITabBarItem *tabBarItem = _controller.tabBarItem;
NSString *evaluatedTitle = _title;
if (_title == nil && _systemItem != RNSBottomTabsScreenSystemItemNone) {
// Restore default system item title
UITabBarSystemItem systemItem =
rnscreens::conversion::RNSBottomTabsScreenSystemItemToUITabBarSystemItem(_systemItem);
evaluatedTitle = [[UITabBarItem alloc] initWithTabBarSystemItem:systemItem tag:0].title;
}
[self updateTabBarItemTitle:evaluatedTitle];
if (![tabBarItem.badgeValue isEqualToString:_badgeValue]) {
tabBarItem.badgeValue = _badgeValue;
}
}
- (void)updateTabBarItemTitle:(NSString *)newTitle
{
// Setting _controller.title updates also _controller.tabBarItem.title but only if there
// is a change to _controller.title. After creating new tabBarItem, _controller.title
// remains the same but _controller.tabBarItem.title is nil. For consistency, we always
// update both.
if (![_controller.tabBarItem.title isEqualToString:newTitle] || ![_controller.title isEqualToString:newTitle]) {
_controller.title = newTitle;
_controller.tabBarItem.title = newTitle;
}
}
#pragma mark - RNSSafeAreaProviding
- (UIEdgeInsets)providerSafeAreaInsets
{
return self.safeAreaInsets;
}
- (void)dispatchSafeAreaDidChangeNotification
{
[NSNotificationCenter.defaultCenter postNotificationName:RNSSafeAreaDidChange object:self userInfo:nil];
}
#pragma mark - RNSSafeAreaProviding related methods
// TODO: register for UIKeyboard notifications
- (void)safeAreaInsetsDidChange
{
[super safeAreaInsetsDidChange];
[self dispatchSafeAreaDidChangeNotification];
}
#if RCT_NEW_ARCH_ENABLED
#pragma mark - RCTComponentViewProtocol
- (void)updateProps:(const facebook::react::Props::Shared &)props
oldProps:(const facebook::react::Props::Shared &)oldProps
{
const auto &oldComponentProps = *std::static_pointer_cast<const react::RNSBottomTabsScreenProps>(_props);
const auto &newComponentProps = *std::static_pointer_cast<const react::RNSBottomTabsScreenProps>(props);
bool tabItemNeedsAppearanceUpdate{false};
bool tabScreenOrientationNeedsUpdate{false};
bool tabBarItemNeedsRecreation{false};
bool tabBarItemNeedsUpdate{false};
bool scrollEdgeEffectsNeedUpdate{false};
if (newComponentProps.title != oldComponentProps.title ||
newComponentProps.isTitleUndefined != oldComponentProps.isTitleUndefined) {
_isTitleUndefined = newComponentProps.isTitleUndefined;
if (_isTitleUndefined) {
_title = nil;
} else {
_title = RCTNSStringFromString(newComponentProps.title);
}
tabBarItemNeedsUpdate = YES;
}
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.tabBarItemTestID != oldComponentProps.tabBarItemTestID) {
_tabItemTestID = RCTNSStringFromStringNilIfEmpty(newComponentProps.tabBarItemTestID);
_tabBarItemNeedsA11yUpdate = YES;
}
if (newComponentProps.tabBarItemAccessibilityLabel != oldComponentProps.tabBarItemAccessibilityLabel) {
_tabItemAccessibilityLabel = RCTNSStringFromStringNilIfEmpty(newComponentProps.tabBarItemAccessibilityLabel);
_tabBarItemNeedsA11yUpdate = 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;
}
if (newComponentProps.iconImageSource != oldComponentProps.iconImageSource) {
_iconImageSource =
rnscreens::conversion::RCTImageSourceFromImageSourceAndIconType(&newComponentProps.iconImageSource, _iconType);
tabItemNeedsAppearanceUpdate = YES;
}
if (newComponentProps.iconResourceName != oldComponentProps.iconResourceName) {
_iconResourceName = RCTNSStringFromStringNilIfEmpty(newComponentProps.iconResourceName);
tabItemNeedsAppearanceUpdate = YES;
}
if (newComponentProps.selectedIconImageSource != oldComponentProps.selectedIconImageSource) {
_selectedIconImageSource = rnscreens::conversion::RCTImageSourceFromImageSourceAndIconType(
&newComponentProps.selectedIconImageSource, _iconType);
tabItemNeedsAppearanceUpdate = YES;
}
if (newComponentProps.selectedIconResourceName != oldComponentProps.selectedIconResourceName) {
_selectedIconResourceName = RCTNSStringFromStringNilIfEmpty(newComponentProps.selectedIconResourceName);
tabItemNeedsAppearanceUpdate = 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);
tabBarItemNeedsRecreation = YES;
}
if (newComponentProps.bottomScrollEdgeEffect != oldComponentProps.bottomScrollEdgeEffect) {
[self
setBottomScrollEdgeEffect:
rnscreens::conversion::RNSBottomTabsScrollEdgeEffectFromBottomTabsScreenBottomScrollEdgeEffectCppEquivalent(
newComponentProps.bottomScrollEdgeEffect)];
scrollEdgeEffectsNeedUpdate = YES;
}
if (newComponentProps.leftScrollEdgeEffect != oldComponentProps.leftScrollEdgeEffect) {
[self setLeftScrollEdgeEffect:
rnscreens::conversion::RNSBottomTabsScrollEdgeEffectFromBottomTabsScreenLeftScrollEdgeEffectCppEquivalent(
newComponentProps.leftScrollEdgeEffect)];
scrollEdgeEffectsNeedUpdate = YES;
}
if (newComponentProps.rightScrollEdgeEffect != oldComponentProps.rightScrollEdgeEffect) {
[self
setRightScrollEdgeEffect:
rnscreens::conversion::RNSBottomTabsScrollEdgeEffectFromBottomTabsScreenRightScrollEdgeEffectCppEquivalent(
newComponentProps.rightScrollEdgeEffect)];
scrollEdgeEffectsNeedUpdate = YES;
}
if (newComponentProps.topScrollEdgeEffect != oldComponentProps.topScrollEdgeEffect) {
[self setTopScrollEdgeEffect:rnscreens::conversion::
RNSBottomTabsScrollEdgeEffectFromBottomTabsScreenTopScrollEdgeEffectCppEquivalent(
newComponentProps.topScrollEdgeEffect)];
scrollEdgeEffectsNeedUpdate = YES;
}
if (newComponentProps.userInterfaceStyle != oldComponentProps.userInterfaceStyle) {
_userInterfaceStyle = rnscreens::conversion::UIUserInterfaceStyleFromBottomTabsScreenCppEquivalent(
newComponentProps.userInterfaceStyle);
}
if (tabBarItemNeedsRecreation) {
[self createTabBarItem];
tabBarItemNeedsUpdate = YES;
_tabBarItemNeedsA11yUpdate = 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];
}
if (scrollEdgeEffectsNeedUpdate) {
[self updateContentScrollViewEdgeEffectsIfExists];
scrollEdgeEffectsNeedUpdate = NO;
}
[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<const react::RNSBottomTabsScreenEventEmitter>(eventEmitter)];
}
- (void)mountChildComponentView:(UIView<RCTComponentViewProtocol> *)childComponentView index:(NSInteger)index
{
RNSLog(@"TabScreen [%ld] mount [%ld] at %ld", self.tag, childComponentView.tag, index);
[super mountChildComponentView:childComponentView index:index];
// overrideScrollViewBehavior and updateContentScrollViewEdgeEffects use first descendant chain
// from screen to find ScrollView, that's why we're only interested in child mounted at index 0.
if (index == 0) {
[self overrideScrollViewBehaviorInFirstDescendantChainIfNeeded];
[self updateContentScrollViewEdgeEffectsIfExists];
}
}
- (void)unmountChildComponentView:(UIView<RCTComponentViewProtocol> *)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<react::RNSBottomTabsScreenComponentDescriptor>();
}
+ (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<NSString *> *)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 (_tabBarItemNeedsRecreation) {
[self createTabBarItem];
_tabBarItemNeedsRecreation = NO;
_tabBarItemNeedsUpdate = YES;
_tabBarItemNeedsA11yUpdate = 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;
}
if (_scrollEdgeEffectsNeedUpdate) {
[self updateContentScrollViewEdgeEffectsIfExists];
_scrollEdgeEffectsNeedUpdate = 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;
_isTitleUndefined = title == nil;
_tabBarItemNeedsUpdate = YES;
}
- (void)setBadgeValue:(NSString *)badgeValue
{
_badgeValue = [NSString rnscreens_stringOrNilIfBlank:badgeValue];
_tabBarItemNeedsUpdate = YES;
}
- (void)setTabBarItemTestID:(NSString *)tabBarItemTestID
{
_tabItemTestID = tabBarItemTestID;
_tabBarItemNeedsA11yUpdate = YES;
}
- (void)setTabBarItemAccessibilityLabel:(NSString *)tabBarItemAccessibilityLabel
{
_tabItemAccessibilityLabel = tabBarItemAccessibilityLabel;
_tabBarItemNeedsA11yUpdate = YES;
}
- (void)setIconType:(RNSBottomTabsIconType)iconType
{
_iconType = iconType;
_tabItemNeedsAppearanceUpdate = YES;
}
- (void)setIconImageSource:(RCTImageSource *)iconImageSource
{
_iconImageSource = iconImageSource;
_tabItemNeedsAppearanceUpdate = YES;
}
- (void)setIconResourceName:(NSString *)iconResourceName
{
_iconResourceName = [NSString rnscreens_stringOrNilIfEmpty:iconResourceName];
_tabItemNeedsAppearanceUpdate = YES;
}
- (void)setSelectedIconImageSource:(RCTImageSource *)selectedIconImageSource
{
_selectedIconImageSource = selectedIconImageSource;
_tabItemNeedsAppearanceUpdate = YES;
}
- (void)setSelectedIconResourceName:(NSString *)selectedIconResourceName
{
_selectedIconResourceName = [NSString rnscreens_stringOrNilIfEmpty:selectedIconResourceName];
_tabItemNeedsAppearanceUpdate = YES;
}
- (void)setBottomScrollEdgeEffect:(RNSScrollEdgeEffect)bottomScrollEdgeEffect
{
_bottomScrollEdgeEffect = bottomScrollEdgeEffect;
_scrollEdgeEffectsNeedUpdate = YES;
}
- (void)setLeftScrollEdgeEffect:(RNSScrollEdgeEffect)leftScrollEdgeEffect
{
_leftScrollEdgeEffect = leftScrollEdgeEffect;
_scrollEdgeEffectsNeedUpdate = YES;
}
- (void)setRightScrollEdgeEffect:(RNSScrollEdgeEffect)rightScrollEdgeEffect
{
_rightScrollEdgeEffect = rightScrollEdgeEffect;
_scrollEdgeEffectsNeedUpdate = YES;
}
- (void)setTopScrollEdgeEffect:(RNSScrollEdgeEffect)topScrollEdgeEffect
{
_topScrollEdgeEffect = topScrollEdgeEffect;
_scrollEdgeEffectsNeedUpdate = 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;
_tabBarItemNeedsRecreation = YES;
}
- (void)setSpecialEffects:(NSDictionary *)specialEffects
{
if (specialEffects == nil || specialEffects[@"repeatedTabSelection"] == nil ||
![specialEffects[@"repeatedTabSelection"] isKindOfClass:[NSDictionary class]]) {
_shouldUseRepeatedTabSelectionPopToRootSpecialEffect = YES;
_shouldUseRepeatedTabSelectionScrollToTopSpecialEffect = YES;
return;
}
NSDictionary *repeatedTabSelection = specialEffects[@"repeatedTabSelection"];
if (repeatedTabSelection[@"popToRoot"] != nil) {
_shouldUseRepeatedTabSelectionPopToRootSpecialEffect = [RCTConvert BOOL:repeatedTabSelection[@"popToRoot"]];
} else {
_shouldUseRepeatedTabSelectionPopToRootSpecialEffect = YES;
}
if (repeatedTabSelection[@"scrollToTop"] != nil) {
_shouldUseRepeatedTabSelectionScrollToTopSpecialEffect = [RCTConvert BOOL:repeatedTabSelection[@"scrollToTop"]];
} else {
_shouldUseRepeatedTabSelectionScrollToTopSpecialEffect = 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<RCTComponentViewProtocol> RNSBottomTabsScreen(void)
{
return RNSBottomTabsScreenComponentView.class;
}
#endif // RCT_NEW_ARCH_ENABLED

View File

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

View File

@@ -0,0 +1,59 @@
#import "RNSBottomTabsScreenComponentViewManager.h"
#if !RCT_NEW_ARCH_ENABLED
#import "RNSBottomTabsScreenComponentView.h"
#endif // !RCT_NEW_ARCH_ENABLED
@implementation RNSBottomTabsScreenComponentViewManager
// TODO: This seems to be legacy arch only - remove when no longer needed
RCT_EXPORT_MODULE(RNSBottomTabsScreenManager)
#if !RCT_NEW_ARCH_ENABLED
- (UIView *)view
{
// This uses main initializer for Fabric implementation.
return [[RNSBottomTabsScreenComponentView alloc] initWithFrame:CGRectZero];
}
RCT_EXPORT_VIEW_PROPERTY(tabKey, NSString);
RCT_REMAP_VIEW_PROPERTY(isFocused, isSelectedScreen, BOOL);
RCT_EXPORT_VIEW_PROPERTY(title, NSString);
RCT_EXPORT_VIEW_PROPERTY(orientation, RNSOrientation);
RCT_EXPORT_VIEW_PROPERTY(badgeValue, NSString);
RCT_EXPORT_VIEW_PROPERTY(tabBarItemTestID, NSString);
RCT_EXPORT_VIEW_PROPERTY(tabBarItemAccessibilityLabel, NSString);
RCT_EXPORT_VIEW_PROPERTY(standardAppearance, NSDictionary);
RCT_EXPORT_VIEW_PROPERTY(scrollEdgeAppearance, NSDictionary);
RCT_EXPORT_VIEW_PROPERTY(iconType, RNSBottomTabsIconType);
RCT_EXPORT_VIEW_PROPERTY(iconImageSource, RCTImageSource);
RCT_EXPORT_VIEW_PROPERTY(iconResourceName, NSString);
RCT_EXPORT_VIEW_PROPERTY(selectedIconImageSource, RCTImageSource);
RCT_EXPORT_VIEW_PROPERTY(selectedIconResourceName, NSString);
RCT_EXPORT_VIEW_PROPERTY(overrideScrollViewContentInsetAdjustmentBehavior, BOOL);
RCT_EXPORT_VIEW_PROPERTY(bottomScrollEdgeEffect, RNSScrollEdgeEffect);
RCT_EXPORT_VIEW_PROPERTY(leftScrollEdgeEffect, RNSScrollEdgeEffect);
RCT_EXPORT_VIEW_PROPERTY(rightScrollEdgeEffect, RNSScrollEdgeEffect);
RCT_EXPORT_VIEW_PROPERTY(topScrollEdgeEffect, RNSScrollEdgeEffect);
RCT_EXPORT_VIEW_PROPERTY(userInterfaceStyle, UIUserInterfaceStyle);
RCT_EXPORT_VIEW_PROPERTY(systemItem, RNSBottomTabsScreenSystemItem);
RCT_EXPORT_VIEW_PROPERTY(specialEffects, NSDictionary);
RCT_EXPORT_VIEW_PROPERTY(onWillAppear, RCTDirectEventBlock);
RCT_EXPORT_VIEW_PROPERTY(onWillDisappear, RCTDirectEventBlock);
RCT_EXPORT_VIEW_PROPERTY(onDidAppear, RCTDirectEventBlock);
RCT_EXPORT_VIEW_PROPERTY(onDidDisappear, RCTDirectEventBlock);
#endif // !RCT_NEW_ARCH_ENABLED
@end

View File

@@ -0,0 +1,55 @@
#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
/**
* These methods can be called to send an appropriate event to ElementTree.
* Returned value denotes whether the event has been successfully dispatched to React event pipeline.
* The returned value of `true` does not mean, that the event has been successfully delivered.
*/
@interface RNSBottomTabsScreenEventEmitter : NSObject
- (BOOL)emitOnWillAppear;
- (BOOL)emitOnDidAppear;
- (BOOL)emitOnWillDisappear;
- (BOOL)emitOnDidDisappear;
@end
#pragma mark - Hidden from Swift
#if defined(__cplusplus)
@interface RNSBottomTabsScreenEventEmitter ()
#if RCT_NEW_ARCH_ENABLED
- (void)updateEventEmitter:(const std::shared_ptr<const react::RNSBottomTabsScreenEventEmitter> &)emitter;
#else
#pragma mark - LEGACY Event emitting blocks
@property (nonatomic, copy, nullable) RCTDirectEventBlock onWillAppear;
@property (nonatomic, copy, nullable) RCTDirectEventBlock onDidAppear;
@property (nonatomic, copy, nullable) RCTDirectEventBlock onWillDisappear;
@property (nonatomic, copy, nullable) RCTDirectEventBlock onDidDisappear;
#endif // RCT_NEW_ARCH_ENABLED
@end
#endif // __cplusplus
NS_ASSUME_NONNULL_END

View File

@@ -0,0 +1,106 @@
#import "RNSBottomTabsScreenEventEmitter.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
@implementation RNSBottomTabsScreenEventEmitter {
#if RCT_NEW_ARCH_ENABLED
std::shared_ptr<const react::RNSBottomTabsScreenEventEmitter> _reactEventEmitter;
#endif // RCT_NEW_ARCH_ENABLED
}
- (BOOL)emitOnWillAppear
{
#if RCT_NEW_ARCH_ENABLED
if (_reactEventEmitter != nullptr) {
_reactEventEmitter->onWillAppear({});
return YES;
} else {
RCTLogWarn(@"[RNScreens] Skipped OnWillAppear event emission due to nullish emitter");
return NO;
}
#else
if (self.onWillAppear) {
self.onWillAppear(nil);
return YES;
} else {
RCTLogWarn(@"[RNScreens] Skipped OnWillAppear event emission due to nullish emitter");
return NO;
}
#endif
}
- (BOOL)emitOnDidAppear
{
#if RCT_NEW_ARCH_ENABLED
if (_reactEventEmitter != nullptr) {
_reactEventEmitter->onDidAppear({});
return YES;
} else {
RCTLogWarn(@"[RNScreens] Skipped OnDidAppear event emission due to nullish emitter");
return NO;
}
#else
if (self.onDidAppear) {
self.onDidAppear(nil);
return YES;
} else {
RCTLogWarn(@"[RNScreens] Skipped OnDidAppear event emission due to nullish emitter");
return NO;
}
#endif
}
- (BOOL)emitOnWillDisappear
{
#if RCT_NEW_ARCH_ENABLED
if (_reactEventEmitter != nullptr) {
_reactEventEmitter->onWillDisappear({});
return YES;
} else {
RCTLogWarn(@"[RNScreens] Skipped OnWillDisappear event emission due to nullish emitter");
return NO;
}
#else
if (self.onWillDisappear) {
self.onWillDisappear(nil);
return YES;
} else {
RCTLogWarn(@"[RNScreens] Skipped OnWillDisappear event emission due to nullish emitter");
return NO;
}
#endif
}
- (BOOL)emitOnDidDisappear
{
#if RCT_NEW_ARCH_ENABLED
if (_reactEventEmitter != nullptr) {
_reactEventEmitter->onDidDisappear({});
return YES;
} else {
RCTLogWarn(@"[RNScreens] Skipped OnDidDisappear event emission due to nullish emitter");
return NO;
}
#else
if (self.onDidDisappear) {
self.onDidDisappear(nil);
return YES;
} else {
RCTLogWarn(@"[RNScreens] Skipped OnDidDisappear event emission due to nullish emitter");
return NO;
}
#endif
}
#if RCT_NEW_ARCH_ENABLED
- (void)updateEventEmitter:(const std::shared_ptr<const react::RNSBottomTabsScreenEventEmitter> &)emitter
{
_reactEventEmitter = emitter;
}
#endif
@end

View File

@@ -0,0 +1,56 @@
#pragma once
#import <UIKit/UIKit.h>
#import "RNSBottomTabsScreenComponentView.h"
#import "RNSBottomTabsSpecialEffectsSupporting.h"
#if !TARGET_OS_TV
#import "RNSOrientationProviding.h"
#endif // !TARGET_OS_TV
NS_ASSUME_NONNULL_BEGIN
@interface RNSTabsScreenViewController : UIViewController
#if !TARGET_OS_TV
<RNSOrientationProviding>
#endif // !TARGET_OS_TV
@property (nonatomic, strong, readonly, nullable) RNSBottomTabsScreenComponentView *tabScreenComponentView;
@property (nonatomic, weak, readonly, nullable) id<RNSBottomTabsSpecialEffectsSupporting> tabsSpecialEffectsDelegate;
/**
* Tell the controller that the tab screen it owns has got its react-props-focus changed.
*/
- (void)tabScreenFocusHasChanged;
/**
* Tell the controller that the tab screen it owns has got its react-props related to appearance changed.
*/
- (void)tabItemAppearanceHasChanged;
/**
* Tell the controller that the tab screen it owns has got its react-props-orientation changed.
*/
- (void)tabScreenOrientationHasChanged;
/**
* Tell the controller that the tab item related to this controller has been selected again after being presented.
* Returns boolean indicating whether the action has been handled.
*/
- (bool)tabScreenSelectedRepeatedly;
/**
* Set new special effects delegate.
*/
- (void)setTabsSpecialEffectsDelegate:(nonnull id<RNSBottomTabsSpecialEffectsSupporting>)delegate;
/**
* Inform the controller about resignation from being special effects delegate. If resignation comes from current
* tabsSpecialEffectsDelegate, this method sets tabsSpecialEffectsDelegate to nil. If tabsSpecialEffectsDelegate has
* already changed (to other delegate or nil), this method does nothing.
*/
- (void)clearTabsSpecialEffectsDelegateIfNeeded:(nonnull id<RNSBottomTabsSpecialEffectsSupporting>)delegate;
@end
NS_ASSUME_NONNULL_END

View File

@@ -0,0 +1,135 @@
#import "RNSTabsScreenViewController.h"
#import "RNSLog.h"
#import "RNSScrollViewFinder.h"
#import "RNSTabBarController.h"
#import "UIScrollView+RNScreens.h"
@implementation RNSTabsScreenViewController
- (nullable RNSTabBarController *)findTabBarController
{
return static_cast<RNSTabBarController *_Nullable>(self.tabBarController);
}
- (nullable RNSBottomTabsScreenComponentView *)tabScreenComponentView
{
return static_cast<RNSBottomTabsScreenComponentView *>(self.view);
}
- (void)tabScreenFocusHasChanged
{
RNSLog(
@"TabScreen [%ld] changed focus: %d",
self.tabScreenComponentView.tag,
self.tabScreenComponentView.isSelectedScreen);
// The focus of owned tab has been updated from react. We tell the parent controller that it should update the
// container.
[[self findTabBarController] setNeedsUpdateOfSelectedTab:true];
}
- (void)tabItemAppearanceHasChanged
{
[[self findTabBarController] setNeedsUpdateOfTabBarAppearance:true];
}
- (void)tabScreenOrientationHasChanged
{
[[self findTabBarController] setNeedsOrientationUpdate:true];
}
- (void)viewWillAppear:(BOOL)animated
{
[self.tabScreenComponentView.reactEventEmitter emitOnWillAppear];
}
- (void)viewDidAppear:(BOOL)animated
{
[self.tabScreenComponentView.reactEventEmitter emitOnDidAppear];
}
- (void)viewWillDisappear:(BOOL)animated
{
[self.tabScreenComponentView.reactEventEmitter emitOnWillDisappear];
}
- (void)viewDidDisappear:(BOOL)animated
{
[self.tabScreenComponentView.reactEventEmitter emitOnDidDisappear];
}
- (void)didMoveToParentViewController:(UIViewController *)parent
{
RNSLog(@"TabScreen [%ld] ctrl moved to parent: %@", self.tabScreenComponentView.tag, parent);
if (parent == nil) {
return;
}
// Can be UINavigationController in case of MoreNavigationController
RCTAssert(
[parent isKindOfClass:RNSTabBarController.class] || [parent isKindOfClass:UINavigationController.class],
@"[RNScreens] TabScreenViewController added to parent of unexpected type: %@",
parent.class);
if ([parent isKindOfClass:UINavigationController.class]) {
// Hide the navigation bar for the more controller
[(UINavigationController *)parent setNavigationBarHidden:YES animated:YES];
}
RNSTabBarController *tabBarCtrl = [self findTabBarController];
RCTAssert(
tabBarCtrl != nil, @"[RNScreens] nullish tabBarCtrl after TabScreenViewController has been added to parent");
[tabBarCtrl setNeedsUpdateOfTabBarAppearance:true];
[tabBarCtrl updateTabBarAppearanceIfNeeded];
}
- (void)setTabsSpecialEffectsDelegate:(id<RNSBottomTabsSpecialEffectsSupporting>)delegate
{
RCTAssert(
delegate != nil,
@"[RNScreens] can't set special effects delegate to nil. Use clearTabsSpecialEffectsDelegateIfNeeded instead.");
_tabsSpecialEffectsDelegate = delegate;
}
- (void)clearTabsSpecialEffectsDelegateIfNeeded:(id<RNSBottomTabsSpecialEffectsSupporting>)delegate
{
if (_tabsSpecialEffectsDelegate == delegate) {
_tabsSpecialEffectsDelegate = nil;
}
}
- (bool)tabScreenSelectedRepeatedly
{
if ([self tabsSpecialEffectsDelegate] != nil) {
return [[self tabsSpecialEffectsDelegate] onRepeatedTabSelectionOfTabScreenController:self];
} else if (self.tabScreenComponentView.shouldUseRepeatedTabSelectionScrollToTopSpecialEffect) {
UIScrollView *scrollView =
[RNSScrollViewFinder findScrollViewInFirstDescendantChainFrom:[self tabScreenComponentView]];
return [scrollView rnscreens_scrollToTop];
}
return false;
}
#if !TARGET_OS_TV
- (RNSOrientation)evaluateOrientation
{
if ([self.childViewControllers.lastObject respondsToSelector:@selector(evaluateOrientation)]) {
id<RNSOrientationProviding> child = static_cast<id<RNSOrientationProviding>>(self.childViewControllers.lastObject);
RNSOrientation childOrientation = [child evaluateOrientation];
if (childOrientation != RNSOrientationInherit) {
return childOrientation;
}
}
return self.tabScreenComponentView.orientation;
}
#endif // !TARGET_OS_TV
@end