chore: add mobile app

This commit is contained in:
2026-02-07 22:33:02 +01:00
parent db34640acc
commit e89384ad4e
29327 changed files with 3886944 additions and 12 deletions
@@ -0,0 +1,21 @@
#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
@@ -0,0 +1,71 @@
#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),
}),
RNSBottomTabsIconTypeSfSymbol,
integerValue)
RCT_ENUM_CONVERTER(
RNSTabBarMinimizeBehavior,
(@{
@"automatic" : @(RNSTabBarMinimizeBehaviorAutomatic),
@"never" : @(RNSTabBarMinimizeBehaviorNever),
@"onScrollDown" : @(RNSTabBarMinimizeBehaviorOnScrollDown),
@"onScrollUp" : @(RNSTabBarMinimizeBehaviorOnScrollUp),
}),
RNSTabBarMinimizeBehaviorAutomatic,
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
@@ -0,0 +1,87 @@
#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 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
@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;
#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
@@ -0,0 +1,458 @@
#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 "RNSBottomTabsScreenComponentView.h"
#import "RNSConversions.h"
#import "RNSConvert.h"
#import "RNSDefines.h"
#import "RNSLog.h"
#import "RNSTabBarController.h"
#import "RNSTabBarControllerDelegate.h"
namespace react = facebook::react;
#pragma mark - View implementation
@interface RNSBottomTabsHostComponentView ()
#if RCT_NEW_ARCH_ENABLED
<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<RNSBottomTabsScreenComponentView *> *_reactSubviews;
BOOL _hasModifiedReactSubviewsInCurrentTransaction;
BOOL _needsTabBarAppearanceUpdate;
}
- (instancetype)initWithFrame:(CGRect)frame
{
if (self = [super initWithFrame:frame]) {
[self initState];
}
return self;
}
#if !RCT_NEW_ARCH_ENABLED
- (instancetype)initWithFrame:(CGRect)frame reactImageLoader:(RCTImageLoader *)imageLoader
{
if (self = [self initWithFrame:frame]) {
_imageLoader = imageLoader;
}
return self;
}
#endif // !RCT_NEW_ARCH_ENABLED
- (nonnull RNSTabBarController *)controller
{
RCTAssert(_controller != nil, @"[RNScreens] Controller must not be nil");
return _controller;
}
- (void)initState
{
[self resetProps];
_controller = [[RNSTabBarController alloc] initWithTabsHostComponentView:self];
_controllerDelegate = [RNSTabBarControllerDelegate new];
_controller.delegate = _controllerDelegate;
_reactSubviews = [NSMutableArray new];
_reactEventEmitter = [RNSBottomTabsHostEventEmitter new];
#if RCT_NEW_ARCH_ENABLED
_invalidatedComponentsRegistry = [RNSInvalidatedComponentsRegistry new];
#endif // RCT_NEW_ARCH_ENABLED
_hasModifiedReactSubviewsInCurrentTransaction = NO;
_needsTabBarAppearanceUpdate = NO;
}
- (void)resetProps
{
#if RCT_NEW_ARCH_ENABLED
static const auto defaultProps = std::make_shared<const react::RNSBottomTabsProps>();
_props = defaultProps;
#endif
_tabBarTintColor = nil;
}
#pragma mark - UIView methods
- (void)willMoveToWindow:(UIWindow *)newWindow
{
#if RCT_NEW_ARCH_ENABLED
if (newWindow == nil) {
[_invalidatedComponentsRegistry flushInvalidViews];
}
#endif // RCT_NEW_ARCH_ENABLED
}
- (void)didMoveToWindow
{
if ([self window] != nil) {
[self reactAddControllerToClosestParent:_controller];
#if !RCT_NEW_ARCH_ENABLED
// This is required on legacy architecture to prevent a bug with doubled size of UIViewControllerWrapperView.
_controller.view.frame = self.bounds;
#endif // !RCT_NEW_ARCH_ENABLED
}
}
- (void)reactAddControllerToClosestParent:(UIViewController *)controller
{
if (!controller.parentViewController) {
UIView *parentView = (UIView *)self.reactSuperview;
while (parentView) {
if (parentView.reactViewController) {
[parentView.reactViewController addChildViewController:controller];
[self addSubview:controller.view];
[controller didMoveToParentViewController:parentView.reactViewController];
break;
}
parentView = (UIView *)parentView.reactSuperview;
}
return;
}
}
#pragma mark - RNSScreenContainerDelegate
- (void)updateContainer
{
NSMutableArray<RNSTabsScreenViewController *> *tabControllers =
[[NSMutableArray alloc] initWithCapacity:_reactSubviews.count];
for (RNSBottomTabsScreenComponentView *childView in _reactSubviews) {
[tabControllers addObject:childView.controller];
}
RNSLog(@"updateContainer: tabControllers: %@", tabControllers);
[_controller childViewControllersHaveChangedTo:tabControllers];
}
- (void)markChildUpdated
{
[self updateContainer];
}
#if RCT_NEW_ARCH_ENABLED
#pragma mark - RNSViewControllerInvalidating
- (void)invalidateController
{
_controller = nil;
}
- (BOOL)shouldInvalidateOnMutation:(const facebook::react::ShadowViewMutation &)mutation
{
return (mutation.oldChildShadowView.tag == self.tag && mutation.type == facebook::react::ShadowViewMutation::Delete);
}
#else
#pragma mark - RCTInvalidating
- (void)invalidate
{
// We assume that bottom tabs host is removed from view hierarchy **only** when
// whole component is destroyed & therefore we do the necessary cleanup here.
// If at some point that statement does not hold anymore, this cleanup
// should be moved to a different place.
for (RNSBottomTabsScreenComponentView *subview in _reactSubviews) {
[subview invalidate];
}
_controller = nil;
}
#endif
#pragma mark - React events
- (nonnull RNSBottomTabsHostEventEmitter *)reactEventEmitter
{
RCTAssert(_reactEventEmitter != nil, @"[RNScreens] Attempt to access uninitialized _reactEventEmitter");
return _reactEventEmitter;
}
- (BOOL)emitOnNativeFocusChangeRequestSelectedTabScreen:(RNSBottomTabsScreenComponentView *)tabScreen
{
return [_reactEventEmitter emitOnNativeFocusChange:OnNativeFocusChangePayload{.tabKey = tabScreen.tabKey}];
}
#pragma mark - RCTViewComponentViewProtocol
#if RCT_NEW_ARCH_ENABLED
- (void)mountChildComponentView:(UIView<RCTComponentViewProtocol> *)childComponentView index:(NSInteger)index
{
RCTAssert(
[childComponentView isKindOfClass:RNSBottomTabsScreenComponentView.class],
@"BottomTabsView only accepts children of type BottomTabScreen. Attempted to mount %@",
childComponentView);
auto *childScreen = static_cast<RNSBottomTabsScreenComponentView *>(childComponentView);
childScreen.reactSuperview = self;
[_reactSubviews insertObject:childScreen atIndex:index];
_hasModifiedReactSubviewsInCurrentTransaction = YES;
}
- (void)unmountChildComponentView:(UIView<RCTComponentViewProtocol> *)childComponentView index:(NSInteger)index
{
RCTAssert(
[childComponentView isKindOfClass:RNSBottomTabsScreenComponentView.class],
@"BottomTabsView only accepts children of type BottomTabScreen. Attempted to unmount %@",
childComponentView);
auto *childScreen = static_cast<RNSBottomTabsScreenComponentView *>(childComponentView);
childScreen.reactSuperview = nil;
[_reactSubviews removeObject:childScreen];
_hasModifiedReactSubviewsInCurrentTransaction = YES;
}
- (void)updateProps:(const facebook::react::Props::Shared &)props
oldProps:(const facebook::react::Props::Shared &)oldProps
{
const auto &oldComponentProps = *std::static_pointer_cast<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.tabBarMinimizeBehavior != oldComponentProps.tabBarMinimizeBehavior) {
#if RNS_IPHONE_OS_VERSION_AVAILABLE(26_0)
if (@available(iOS 26.0, *)) {
_tabBarMinimizeBehavior = rnscreens::conversion::UITabBarMinimizeBehaviorFromRNSBottomTabsTabBarMinimizeBehavior(
newComponentProps.tabBarMinimizeBehavior);
_controller.tabBarMinimizeBehavior = _tabBarMinimizeBehavior;
} else
#endif // Check for iOS >= 26
if (newComponentProps.tabBarMinimizeBehavior != react::RNSBottomTabsTabBarMinimizeBehavior::Automatic) {
RCTLogWarn(@"[RNScreens] tabBarMinimizeBehavior is supported for iOS >= 26");
}
}
// Super call updates _props pointer. We should NOT update it before calling super.
[super updateProps:props oldProps:oldProps];
}
- (void)updateState:(const facebook::react::State::Shared &)state
oldState:(const facebook::react::State::Shared &)oldState
{
react::RNSBottomTabsShadowNode::ConcreteState::Shared receivedState =
std::static_pointer_cast<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
{
_hasModifiedReactSubviewsInCurrentTransaction = NO;
[_controller reactMountingTransactionWillMount];
#if RCT_NEW_ARCH_ENABLED
for (const auto &mutation : transaction.getMutations()) {
if ([self shouldInvalidateOnMutation:mutation]) {
for (RNSBottomTabsScreenComponentView *childView in _reactSubviews) {
[RNSViewControllerInvalidator invalidateViewIfDetached:childView forRegistry:_invalidatedComponentsRegistry];
}
[RNSViewControllerInvalidator invalidateViewIfDetached:self forRegistry:_invalidatedComponentsRegistry];
}
}
#endif // RCT_NEW_ARCH_ENABLED
}
- (void)mountingTransactionDidMount:(const facebook::react::MountingTransaction &)transaction
withSurfaceTelemetry:(const facebook::react::SurfaceTelemetry &)surfaceTelemetry
{
if (_hasModifiedReactSubviewsInCurrentTransaction) {
[self updateContainer];
}
[_controller reactMountingTransactionDidMount];
}
#else
#pragma mark - LEGACY architecture implementation
#pragma mark - LEGACY RCTComponent protocol
RNS_IGNORE_SUPER_CALL_BEGIN
- (void)insertReactSubview:(UIView *)subview atIndex:(NSInteger)index
{
RCTAssert(
[subview isKindOfClass:RNSBottomTabsScreenComponentView.class],
@"BottomTabsView only accepts children of type BottomTabScreen. Attempted to mount %@",
subview);
auto *childScreen = static_cast<RNSBottomTabsScreenComponentView *>(subview);
childScreen.reactSuperview = self;
[_reactSubviews insertObject:childScreen atIndex:index];
}
- (void)removeReactSubview:(UIView *)subview
{
RCTAssert(
[subview isKindOfClass:RNSBottomTabsScreenComponentView.class],
@"BottomTabsView only accepts children of type BottomTabScreen. Attempted to unmount %@",
subview);
auto *childScreen = static_cast<RNSBottomTabsScreenComponentView *>(subview);
childScreen.reactSuperview = nil;
[_reactSubviews removeObject:childScreen];
}
RNS_IGNORE_SUPER_CALL_END
- (void)didUpdateReactSubviews
{
[super didUpdateReactSubviews];
_hasModifiedReactSubviewsInCurrentTransaction = YES;
[self invalidateFlagsOnControllerIfNeeded];
}
- (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 (_hasModifiedReactSubviewsInCurrentTransaction) {
_hasModifiedReactSubviewsInCurrentTransaction = NO;
[self updateContainer];
}
}
- (void)invalidateTabBarAppearance
{
_needsTabBarAppearanceUpdate = YES;
[self invalidateFlagsOnControllerIfNeeded];
}
#pragma mark - LEGACY prop setters
// Paper will call property setters
- (void)setTabBarTintColor:(UIColor *_Nullable)tabBarTintColor
{
_tabBarTintColor = tabBarTintColor;
[self invalidateTabBarAppearance];
}
// This is a Paper-only setter method that will be called by the mounting code.
// It allows us to store UITabBarMinimizeBehavior in the component while accepting a custom enum as input from JS.
- (void)setTabBarMinimizeBehaviorFromRNSTabBarMinimizeBehavior:(RNSTabBarMinimizeBehavior)tabBarMinimizeBehavior
{
#if RNS_IPHONE_OS_VERSION_AVAILABLE(26_0)
if (@available(iOS 26.0, *)) {
_tabBarMinimizeBehavior =
rnscreens::conversion::UITabBarMinimizeBehaviorFromRNSTabBarMinimizeBehavior(tabBarMinimizeBehavior);
_controller.tabBarMinimizeBehavior = _tabBarMinimizeBehavior;
} else
#endif // Check for iOS >= 26
if (tabBarMinimizeBehavior != RNSTabBarMinimizeBehaviorAutomatic) {
RCTLogWarn(@"[RNScreens] tabBarMinimizeBehavior is supported for iOS >= 26");
}
}
- (void)setOnNativeFocusChange:(RCTDirectEventBlock)onNativeFocusChange
{
[self.reactEventEmitter setOnNativeFocusChange:onNativeFocusChange];
}
#endif // RCT_NEW_ARCH_ENABLED
#pragma mark - React Image Loader
- (nullable RCTImageLoader *)reactImageLoader
{
return _imageLoader;
}
@end
#if RCT_NEW_ARCH_ENABLED
#pragma mark - View class exposure
Class<RCTComponentViewProtocol> RNSBottomTabsCls(void)
{
return RNSBottomTabsHostComponentView.class;
}
#endif // RCT_NEW_ARCH_ENABLED
@@ -0,0 +1,11 @@
#pragma once
#import <React/RCTViewManager.h>
NS_ASSUME_NONNULL_BEGIN
@interface RNSBottomTabsHostComponentViewManager : RCTViewManager
@end
NS_ASSUME_NONNULL_END
@@ -0,0 +1,42 @@
#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);
// 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);
// TODO: Missing prop
//@property (nonatomic, readonly) BOOL experimental_controlNavigationStateInJS;
#pragma mark - LEGACY Events
RCT_EXPORT_VIEW_PROPERTY(onNativeFocusChange, RCTDirectEventBlock);
#endif
@end
@@ -0,0 +1,53 @@
#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;
};
#else
typedef struct {
NSString *_Nonnull tabKey;
} 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
@@ -0,0 +1,57 @@
#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)});
return YES;
} else {
RCTLogWarn(@"[RNScreens] Skipped OnNativeFocusChange event emission due to nullish emitter");
return NO;
}
#else
if (self.onNativeFocusChange) {
self.onNativeFocusChange(@{@"tabKey" : payload.tabKey});
return YES;
} else {
RCTLogWarn(@"[RNScreens] Skipped OnNativeFocusChange event emission due to nullish emitter");
return NO;
}
#endif // RCT_NEW_ARCH_ENABLED
}
@end
@@ -0,0 +1,98 @@
#import <React/RCTImageSource.h>
#import "RNSBottomTabsScreenEventEmitter.h"
#import "RNSEnums.h"
#import "RNSReactBaseView.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 <
#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;
@end
#pragma mark - Props
/**
* Properties set on component in JavaScript.
*/
@interface RNSBottomTabsScreenComponentView () <RNSScrollViewBehaviorOverriding>
// 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, readonly) RNSBottomTabsIconType iconType;
@property (nonatomic, strong, readonly, nullable) RCTImageSource *iconImageSource;
@property (nonatomic, strong, readonly, nullable) NSString *iconSfSymbolName;
@property (nonatomic, strong, readonly, nullable) RCTImageSource *selectedIconImageSource;
@property (nonatomic, strong, readonly, nullable) NSString *selectedIconSfSymbolName;
@property (nonatomic, strong, readonly, nullable) UITabBarAppearance *standardAppearance;
@property (nonatomic, strong, readonly, nullable) UITabBarAppearance *scrollEdgeAppearance;
@property (nonatomic, nullable) NSString *title;
@property (nonatomic, readonly) RNSOrientation orientation;
@property (nonatomic) BOOL shouldUseRepeatedTabSelectionPopToRootSpecialEffect;
@property (nonatomic) BOOL shouldUseRepeatedTabSelectionScrollToTopSpecialEffect;
@property (nonatomic, readonly) BOOL overrideScrollViewContentInsetAdjustmentBehavior;
@property (nonatomic) RNSBottomTabsScreenSystemItem systemItem;
@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
@@ -0,0 +1,549 @@
#import "RNSBottomTabsScreenComponentView.h"
#import "NSString+RNSUtility.h"
#import "RNSConversions.h"
#import "RNSDefines.h"
#import "RNSLog.h"
#import "RNSScrollViewHelper.h"
#import "RNSTabBarAppearanceCoordinator.h"
#import "RNSTabBarController.h"
#if RCT_NEW_ARCH_ENABLED
#import <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 _tabBarItemNeedsUpdate;
#endif // !RCT_NEW_ARCH_ENABLED
}
- (instancetype)initWithFrame:(CGRect)frame
{
if (self = [super initWithFrame:frame]) {
[self initState];
}
return self;
}
- (void)initState
{
#if RCT_NEW_ARCH_ENABLED
static const auto defaultProps = std::make_shared<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;
_tabBarItemNeedsUpdate = NO;
#endif
// This is a temporary workaround to avoid UIScrollEdgeEffect glitch
// when changing tabs when ScrollView is present.
// TODO: don't hardcode color here
self.backgroundColor = [UIColor whiteColor];
[self resetProps];
}
- (void)resetProps
{
_isSelectedScreen = NO;
_badgeValue = nil;
_title = nil;
_orientation = RNSOrientationInherit;
_standardAppearance = [UITabBarAppearance new];
_scrollEdgeAppearance = nil;
_shouldUseRepeatedTabSelectionPopToRootSpecialEffect = YES;
_shouldUseRepeatedTabSelectionScrollToTopSpecialEffect = YES;
_overrideScrollViewContentInsetAdjustmentBehavior = YES;
_isOverrideScrollViewContentInsetAdjustmentBehaviorSet = NO;
_iconType = RNSBottomTabsIconTypeSfSymbol;
_iconImageSource = nil;
_iconSfSymbolName = nil;
_selectedIconImageSource = nil;
_selectedIconSfSymbolName = nil;
_systemItem = RNSBottomTabsScreenSystemItemNone;
}
RNS_IGNORE_SUPER_CALL_BEGIN
- (nullable RNSBottomTabsHostComponentView *)reactSuperview
{
return _reactSuperview;
}
RNS_IGNORE_SUPER_CALL_END
#ifdef RCT_NEW_ARCH_ENABLED
#pragma mark - RNSViewControllerInvalidating
- (void)invalidateController
{
_controller = nil;
}
#else
#pragma mark - RCTInvalidating
- (void)invalidate
{
_controller = nil;
}
#endif
#pragma mark - Events
- (nonnull RNSBottomTabsScreenEventEmitter *)reactEventEmitter
{
RCTAssert(_reactEventEmitter != nil, @"[RNScreens] Attempt to access uninitialized _reactEventEmitter");
return _reactEventEmitter;
}
- (nullable RNSTabBarController *)findTabBarController
{
return static_cast<RNSTabBarController *>(_controller.tabBarController);
}
#pragma mark - RNSScrollViewBehaviorOverriding
- (BOOL)shouldOverrideScrollViewContentInsetAdjustmentBehavior
{
return self.overrideScrollViewContentInsetAdjustmentBehavior;
}
- (void)overrideScrollViewBehaviorInFirstDescendantChainIfNeeded
{
if ([self shouldOverrideScrollViewContentInsetAdjustmentBehavior]) {
[RNSScrollViewHelper overrideScrollViewBehaviorInFirstDescendantChainFrom:self];
}
}
#pragma mark - Prop update utils
- (void)updateTabBarItem
{
UITabBarItem *tabBarItem = nil;
if (_systemItem != RNSBottomTabsScreenSystemItemNone) {
UITabBarSystemItem systemItem =
rnscreens::conversion::RNSBottomTabsScreenSystemItemToUITabBarSystemItem(_systemItem);
tabBarItem = [[UITabBarItem alloc] initWithTabBarSystemItem:systemItem tag:0];
} else {
tabBarItem = [[UITabBarItem alloc] init];
tabBarItem.title = _title;
}
tabBarItem.badgeValue = _badgeValue;
_controller.tabBarItem = tabBarItem;
}
#if RCT_NEW_ARCH_ENABLED
#pragma mark - RCTViewComponentViewProtocol
- (void)updateProps:(const facebook::react::Props::Shared &)props
oldProps:(const facebook::react::Props::Shared &)oldProps
{
const auto &oldComponentProps = *std::static_pointer_cast<const react::RNSBottomTabsScreenProps>(_props);
const auto &newComponentProps = *std::static_pointer_cast<const react::RNSBottomTabsScreenProps>(props);
bool tabItemNeedsAppearanceUpdate{false};
bool tabScreenOrientationNeedsUpdate{false};
bool tabBarItemNeedsUpdate{false};
if (newComponentProps.title != oldComponentProps.title) {
_title = RCTNSStringFromStringNilIfEmpty(newComponentProps.title);
_controller.title = _title;
}
if (newComponentProps.orientation != oldComponentProps.orientation) {
_orientation =
rnscreens::conversion::RNSOrientationFromRNSBottomTabsScreenOrientation(newComponentProps.orientation);
tabScreenOrientationNeedsUpdate = YES;
}
if (newComponentProps.tabKey != oldComponentProps.tabKey) {
RCTAssert(!newComponentProps.tabKey.empty(), @"[RNScreens] tabKey must not be empty!");
_tabKey = RCTNSStringFromString(newComponentProps.tabKey);
}
if (newComponentProps.isFocused != oldComponentProps.isFocused) {
_isSelectedScreen = newComponentProps.isFocused;
[_controller tabScreenFocusHasChanged];
}
if (newComponentProps.badgeValue != oldComponentProps.badgeValue) {
_badgeValue = RCTNSStringFromStringNilIfEmpty(newComponentProps.badgeValue);
tabBarItemNeedsUpdate = YES;
}
if (newComponentProps.standardAppearance != oldComponentProps.standardAppearance) {
_standardAppearance = [UITabBarAppearance new];
[RNSTabBarAppearanceCoordinator configureTabBarAppearance:_standardAppearance
fromAppearanceProps:rnscreens::conversion::RNSConvertFollyDynamicToId(
newComponentProps.standardAppearance)];
tabItemNeedsAppearanceUpdate = YES;
}
if (newComponentProps.scrollEdgeAppearance != oldComponentProps.scrollEdgeAppearance) {
if (newComponentProps.scrollEdgeAppearance.type() == folly::dynamic::OBJECT) {
_scrollEdgeAppearance = [UITabBarAppearance new];
[RNSTabBarAppearanceCoordinator configureTabBarAppearance:_scrollEdgeAppearance
fromAppearanceProps:rnscreens::conversion::RNSConvertFollyDynamicToId(
newComponentProps.scrollEdgeAppearance)];
} else {
_scrollEdgeAppearance = nil;
}
tabItemNeedsAppearanceUpdate = YES;
}
if (newComponentProps.iconType != oldComponentProps.iconType) {
_iconType = rnscreens::conversion::RNSBottomTabsIconTypeFromIcon(newComponentProps.iconType);
tabItemNeedsAppearanceUpdate = YES;
tabBarItemNeedsUpdate = YES;
}
if (newComponentProps.iconImageSource != oldComponentProps.iconImageSource) {
_iconImageSource =
rnscreens::conversion::RCTImageSourceFromImageSourceAndIconType(&newComponentProps.iconImageSource, _iconType);
tabItemNeedsAppearanceUpdate = YES;
tabBarItemNeedsUpdate = YES;
}
if (newComponentProps.iconSfSymbolName != oldComponentProps.iconSfSymbolName) {
_iconSfSymbolName = RCTNSStringFromStringNilIfEmpty(newComponentProps.iconSfSymbolName);
tabItemNeedsAppearanceUpdate = YES;
tabBarItemNeedsUpdate = YES;
}
if (newComponentProps.selectedIconImageSource != oldComponentProps.selectedIconImageSource) {
_selectedIconImageSource = rnscreens::conversion::RCTImageSourceFromImageSourceAndIconType(
&newComponentProps.selectedIconImageSource, _iconType);
tabItemNeedsAppearanceUpdate = YES;
tabBarItemNeedsUpdate = YES;
}
if (newComponentProps.selectedIconSfSymbolName != oldComponentProps.selectedIconSfSymbolName) {
_selectedIconSfSymbolName = RCTNSStringFromStringNilIfEmpty(newComponentProps.selectedIconSfSymbolName);
tabItemNeedsAppearanceUpdate = YES;
tabBarItemNeedsUpdate = YES;
}
if (newComponentProps.specialEffects.repeatedTabSelection.popToRoot !=
oldComponentProps.specialEffects.repeatedTabSelection.popToRoot) {
_shouldUseRepeatedTabSelectionPopToRootSpecialEffect =
newComponentProps.specialEffects.repeatedTabSelection.popToRoot;
}
if (newComponentProps.specialEffects.repeatedTabSelection.scrollToTop !=
oldComponentProps.specialEffects.repeatedTabSelection.scrollToTop) {
_shouldUseRepeatedTabSelectionScrollToTopSpecialEffect =
newComponentProps.specialEffects.repeatedTabSelection.scrollToTop;
}
if (newComponentProps.overrideScrollViewContentInsetAdjustmentBehavior !=
oldComponentProps.overrideScrollViewContentInsetAdjustmentBehavior) {
_overrideScrollViewContentInsetAdjustmentBehavior =
newComponentProps.overrideScrollViewContentInsetAdjustmentBehavior;
if (_isOverrideScrollViewContentInsetAdjustmentBehaviorSet) {
RCTLogWarn(
@"[RNScreens] changing overrideScrollViewContentInsetAdjustmentBehavior dynamically is currently unsupported");
}
}
// This flag is set to YES when overrideScrollViewContentInsetAdjustmentBehavior prop
// is assigned for the first time. This allows us to identify any subsequent changes to this prop,
// enabling us to warn users that dynamic changes are not supported.
_isOverrideScrollViewContentInsetAdjustmentBehaviorSet = YES;
if (newComponentProps.systemItem != oldComponentProps.systemItem) {
_systemItem = rnscreens::conversion::RNSBottomTabsScreenSystemItemFromReactRNSBottomTabsScreenSystemItem(
newComponentProps.systemItem);
tabBarItemNeedsUpdate = YES;
}
if (tabBarItemNeedsUpdate) {
[self updateTabBarItem];
// Force appearance update to make sure correct image for tab bar item is used
tabItemNeedsAppearanceUpdate = YES;
}
if (tabItemNeedsAppearanceUpdate) {
[_controller tabItemAppearanceHasChanged];
}
if (tabScreenOrientationNeedsUpdate) {
[_controller tabScreenOrientationHasChanged];
}
[super updateProps:props oldProps:oldProps];
}
- (void)updateLayoutMetrics:(const facebook::react::LayoutMetrics &)layoutMetrics
oldLayoutMetrics:(const facebook::react::LayoutMetrics &)oldLayoutMetrics
{
RNSLog(
@"TabScreen [%ld] updateLayoutMetrics: %@", self.tag, NSStringFromCGRect(RCTCGRectFromRect(layoutMetrics.frame)));
[super updateLayoutMetrics:layoutMetrics oldLayoutMetrics:oldLayoutMetrics];
}
- (void)updateEventEmitter:(const facebook::react::EventEmitter::Shared &)eventEmitter
{
[super updateEventEmitter:eventEmitter];
[_reactEventEmitter
updateEventEmitter:std::static_pointer_cast<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];
}
- (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 (_tabBarItemNeedsUpdate) {
[self updateTabBarItem];
_tabBarItemNeedsUpdate = NO;
// Force appearance update to make sure correct image for tab bar item is used
_tabItemNeedsAppearanceUpdate = YES;
}
if (_tabItemNeedsAppearanceUpdate) {
[_controller tabItemAppearanceHasChanged];
_tabItemNeedsAppearanceUpdate = NO;
}
if (_tabScreenOrientationNeedsUpdate) {
[_controller tabScreenOrientationHasChanged];
_tabScreenOrientationNeedsUpdate = NO;
}
}
#pragma mark - LEGACY prop setters
- (void)setIsSelectedScreen:(BOOL)isSelectedScreen
{
if (_isSelectedScreen != isSelectedScreen) {
_isSelectedScreen = isSelectedScreen;
[_controller tabScreenFocusHasChanged];
}
}
- (void)setTabKey:(NSString *)tabKey
{
RCTAssert([NSString rnscreens_isBlankOrNull:tabKey] == NO, @"[RNScreens] tabKey must not be empty");
_tabKey = tabKey;
}
- (void)setTitle:(NSString *)title
{
_title = title;
_controller.title = title;
}
- (void)setBadgeValue:(NSString *)badgeValue
{
_badgeValue = [NSString rnscreens_stringOrNilIfBlank:badgeValue];
_tabBarItemNeedsUpdate = YES;
}
- (void)setIconType:(RNSBottomTabsIconType)iconType
{
_iconType = iconType;
_tabItemNeedsAppearanceUpdate = YES;
_tabBarItemNeedsUpdate = YES;
}
- (void)setIconImageSource:(RCTImageSource *)iconImageSource
{
_iconImageSource = iconImageSource;
_tabItemNeedsAppearanceUpdate = YES;
_tabBarItemNeedsUpdate = YES;
}
- (void)setIconSfSymbolName:(NSString *)iconSfSymbolName
{
_iconSfSymbolName = [NSString rnscreens_stringOrNilIfEmpty:iconSfSymbolName];
_tabItemNeedsAppearanceUpdate = YES;
_tabBarItemNeedsUpdate = YES;
}
- (void)setSelectedIconImageSource:(RCTImageSource *)selectedIconImageSource
{
_selectedIconImageSource = selectedIconImageSource;
_tabItemNeedsAppearanceUpdate = YES;
_tabBarItemNeedsUpdate = YES;
}
- (void)setSelectedIconSfSymbolName:(NSString *)selectedIconSfSymbolName
{
_selectedIconSfSymbolName = [NSString rnscreens_stringOrNilIfEmpty:selectedIconSfSymbolName];
_tabItemNeedsAppearanceUpdate = YES;
_tabBarItemNeedsUpdate = YES;
}
- (void)setOverrideScrollViewContentInsetAdjustmentBehavior:(BOOL)overrideScrollViewContentInsetAdjustmentBehavior
{
_overrideScrollViewContentInsetAdjustmentBehavior = overrideScrollViewContentInsetAdjustmentBehavior;
if (_isOverrideScrollViewContentInsetAdjustmentBehaviorSet) {
RCTLogWarn(
@"[RNScreens] changing overrideScrollViewContentInsetAdjustmentBehavior dynamically is currently unsupported");
}
// _isOverrideScrollViewContentInsetAdjustmentBehaviorSet flag is set in didSetProps to handle a case
// when the prop is undefined in JS and default value is used instead of calling this setter.
}
- (void)setStandardAppearance:(NSDictionary *)standardAppearanceProps
{
_standardAppearance = [UITabBarAppearance new];
if (standardAppearanceProps != nil) {
[RNSTabBarAppearanceCoordinator configureTabBarAppearance:_standardAppearance
fromAppearanceProps:standardAppearanceProps];
}
_tabItemNeedsAppearanceUpdate = YES;
}
- (void)setScrollEdgeAppearance:(NSDictionary *)scrollEdgeAppearanceProps
{
if (scrollEdgeAppearanceProps != nil) {
_scrollEdgeAppearance = [UITabBarAppearance new];
[RNSTabBarAppearanceCoordinator configureTabBarAppearance:_scrollEdgeAppearance
fromAppearanceProps:scrollEdgeAppearanceProps];
} else {
_scrollEdgeAppearance = nil;
}
_tabItemNeedsAppearanceUpdate = YES;
}
// This is a Paper-only setter method that will be called by the mounting code.
// It allows us to store UITabBarMinimizeBehavior in the component while accepting a custom enum as input from JS.
- (void)setSystemItem:(RNSBottomTabsScreenSystemItem)systemItem
{
_systemItem = systemItem;
_tabBarItemNeedsUpdate = YES;
}
- (void)setOrientation:(RNSOrientation)orientation
{
_orientation = orientation;
_tabScreenOrientationNeedsUpdate = YES;
}
- (void)setOnWillAppear:(RCTDirectEventBlock)onWillAppear
{
[self.reactEventEmitter setOnWillAppear:onWillAppear];
}
- (void)setOnWillDisappear:(RCTDirectEventBlock)onWillDisappear
{
[self.reactEventEmitter setOnWillDisappear:onWillDisappear];
}
- (void)setOnDidAppear:(RCTDirectEventBlock)onDidAppear
{
[self.reactEventEmitter setOnDidAppear:onDidAppear];
}
- (void)setOnDidDisappear:(RCTDirectEventBlock)onDidDisappear
{
[self.reactEventEmitter setOnDidDisappear:onDidDisappear];
}
#define RNS_FAILING_EVENT_GETTER(eventName) \
-(RCTDirectEventBlock)eventName \
{ \
RCTAssert(NO, @"[RNScreens] Events should be emitted through reactEventEmitter"); \
return nil; \
}
RNS_FAILING_EVENT_GETTER(onWillAppear);
RNS_FAILING_EVENT_GETTER(onDidAppear);
RNS_FAILING_EVENT_GETTER(onWillDisappear);
RNS_FAILING_EVENT_GETTER(onDidDisappear);
#undef RNS_FAILING_EVENT_GETTER
#endif // RCT_NEW_ARCH_ENABLED
@end
#if RCT_NEW_ARCH_ENABLED
#pragma mark - View class exposure
Class<RCTComponentViewProtocol> RNSBottomTabsScreen(void)
{
return RNSBottomTabsScreenComponentView.class;
}
#endif // RCT_NEW_ARCH_ENABLED
@@ -0,0 +1,9 @@
#import <React/RCTViewManager.h>
NS_ASSUME_NONNULL_BEGIN
@interface RNSBottomTabsScreenComponentViewManager : RCTViewManager
@end
NS_ASSUME_NONNULL_END
@@ -0,0 +1,50 @@
#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(standardAppearance, NSDictionary);
RCT_EXPORT_VIEW_PROPERTY(scrollEdgeAppearance, NSDictionary);
RCT_EXPORT_VIEW_PROPERTY(iconType, RNSBottomTabsIconType);
RCT_EXPORT_VIEW_PROPERTY(iconImageSource, RCTImageSource);
RCT_EXPORT_VIEW_PROPERTY(iconSfSymbolName, NSString);
RCT_EXPORT_VIEW_PROPERTY(selectedIconImageSource, RCTImageSource);
RCT_EXPORT_VIEW_PROPERTY(selectedIconSfSymbolName, NSString);
RCT_EXPORT_VIEW_PROPERTY(shouldUseRepeatedTabSelectionPopToRootSpecialEffect, BOOL);
RCT_EXPORT_VIEW_PROPERTY(shouldUseRepeatedTabSelectionScrollToTopSpecialEffect, BOOL);
RCT_EXPORT_VIEW_PROPERTY(overrideScrollViewContentInsetAdjustmentBehavior, BOOL);
RCT_EXPORT_VIEW_PROPERTY(systemItem, RNSBottomTabsScreenSystemItem);
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
@@ -0,0 +1,53 @@
#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
@@ -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
@@ -0,0 +1,17 @@
#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
@@ -0,0 +1,49 @@
#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
@@ -0,0 +1,226 @@
#import "RNSTabBarAppearanceCoordinator.h"
#import <React/RCTFont.h>
#import <React/RCTImageLoader.h>
#import "RCTConvert+RNSBottomTabs.h"
#import "RNSConversions.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;
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) {
if (screenView.iconSfSymbolName != nil) {
tabBarItem.image = [UIImage systemImageNamed:screenView.iconSfSymbolName];
}
if (screenView.selectedIconSfSymbolName != nil) {
tabBarItem.selectedImage = [UIImage systemImageNamed:screenView.selectedIconSfSymbolName];
}
} else if (imageLoader != nil) {
bool isTemplate = screenView.iconType == RNSBottomTabsIconTypeTemplate;
// Normal icon
if (screenView.iconImageSource != nil) {
[self loadImageFrom:screenView.iconImageSource
withImageLoader:imageLoader
asTemplate:isTemplate
completionBlock:^(UIImage *image) {
tabBarItem.image = image;
}];
} else {
tabBarItem.image = nil;
}
// Selected icon
if (screenView.selectedIconImageSource != nil) {
[self loadImageFrom:screenView.selectedIconImageSource
withImageLoader:imageLoader
asTemplate:isTemplate
completionBlock:^(UIImage *image) {
tabBarItem.selectedImage = image;
}];
} else {
tabBarItem.selectedImage = nil;
}
} else {
RCTLogWarn(@"[RNScreens] unable to load tab bar item icons: imageLoader should not be nil");
}
}
- (void)loadImageFrom:(nonnull RCTImageSource *)imageSource
withImageLoader:(nonnull RCTImageLoader *)imageLoader
asTemplate:(bool)isTemplate
completionBlock:(void (^)(UIImage *image))imageLoadingCompletionBlock
{
RCTAssert(imageSource != nil, @"[RNScreens] imageSource must not be nil");
RCTAssert(imageLoader != nil, @"[RNScreens] imageLoader must not be nil");
[imageLoader loadImageWithURLRequest:imageSource.request
size:imageSource.size
scale:imageSource.scale
clipped:true
resizeMode:RCTResizeModeContain
progressBlock:^(int64_t progress, int64_t total) {
}
partialLoadBlock:^(UIImage *_Nonnull image) {
}
completionBlock:^(NSError *_Nullable error, UIImage *_Nullable image) {
dispatch_async(dispatch_get_main_queue(), ^{
if (isTemplate) {
imageLoadingCompletionBlock([image imageWithRenderingMode:UIImageRenderingModeAlwaysTemplate]);
} else {
imageLoadingCompletionBlock([image imageWithRenderingMode:UIImageRenderingModeAlwaysOriginal]);
}
});
}];
}
+ (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:nil
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
@@ -0,0 +1,164 @@
#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
@@ -0,0 +1,250 @@
#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 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);
[selectedViewController.tabScreenComponentView overrideScrollViewBehaviorInFirstDescendantChainIfNeeded];
[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]];
}
#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
@@ -0,0 +1,9 @@
#import <UIKit/UIKit.h>
NS_ASSUME_NONNULL_BEGIN
@interface RNSTabBarControllerDelegate : NSObject <UITabBarControllerDelegate>
@end
NS_ASSUME_NONNULL_END
@@ -0,0 +1,93 @@
#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
bool repeatedSelectionHandledNatively = false;
// Detect repeated selection and inform tabScreenController
if ([tabBarCtrl selectedViewController] == tabScreenCtrl) {
repeatedSelectionHandledNatively = [tabScreenCtrl tabScreenSelectedRepeatedly];
}
// TODO: send an event with information about event being handled natively
if (!repeatedSelectionHandledNatively) {
[tabBarCtrl.tabsHostComponentView
emitOnNativeFocusChangeRequestSelectedTabScreen:tabScreenCtrl.tabScreenComponentView];
// TODO: handle overrideScrollViewBehaviorInFirstDescendantChainIfNeeded for natively-driven tabs
return ![self shouldPreventNativeTabChangeWithinTabBarController:tabBarCtrl];
}
// TODO: handle enforcing orientation with natively-driven tabs
// As we're selecting the same controller, returning both true and false works here.
return true;
}
- (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
@@ -0,0 +1,54 @@
#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
@@ -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
@@ -0,0 +1,22 @@
#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
@@ -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