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,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