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,15 @@
#pragma once
#if !RCT_NEW_ARCH_ENABLED
#import <React/RCTConvert.h>
NS_ASSUME_NONNULL_BEGIN
@interface RCTConvert (RNScreens)
@end
NS_ASSUME_NONNULL_END
#endif // !RCT_NEW_ARCH_ENABLED

View File

@@ -0,0 +1,69 @@
#import "RCTConvert+RNScreens.h"
#if !RCT_NEW_ARCH_ENABLED
#import "RNSEnums.h"
@implementation RCTConvert (RNScreens)
+ (NSMutableDictionary *)blurEffectsForIOSVersion
{
NSMutableDictionary *blurEffects = [NSMutableDictionary new];
[blurEffects addEntriesFromDictionary:@{
@"none" : @(RNSBlurEffectStyleNone),
@"systemDefault" : @(RNSBlurEffectStyleSystemDefault),
@"extraLight" : @(RNSBlurEffectStyleExtraLight),
@"light" : @(RNSBlurEffectStyleLight),
@"dark" : @(RNSBlurEffectStyleDark),
@"regular" : @(RNSBlurEffectStyleRegular),
@"prominent" : @(RNSBlurEffectStyleProminent),
}];
#if !TARGET_OS_TV
[blurEffects addEntriesFromDictionary:@{
@"systemUltraThinMaterial" : @(RNSBlurEffectStyleSystemUltraThinMaterial),
@"systemThinMaterial" : @(RNSBlurEffectStyleSystemThinMaterial),
@"systemMaterial" : @(RNSBlurEffectStyleSystemMaterial),
@"systemThickMaterial" : @(RNSBlurEffectStyleSystemThickMaterial),
@"systemChromeMaterial" : @(RNSBlurEffectStyleSystemChromeMaterial),
@"systemUltraThinMaterialLight" : @(RNSBlurEffectStyleSystemUltraThinMaterialLight),
@"systemThinMaterialLight" : @(RNSBlurEffectStyleSystemThinMaterialLight),
@"systemMaterialLight" : @(RNSBlurEffectStyleSystemMaterialLight),
@"systemThickMaterialLight" : @(RNSBlurEffectStyleSystemThickMaterialLight),
@"systemChromeMaterialLight" : @(RNSBlurEffectStyleSystemChromeMaterialLight),
@"systemUltraThinMaterialDark" : @(RNSBlurEffectStyleSystemUltraThinMaterialDark),
@"systemThinMaterialDark" : @(RNSBlurEffectStyleSystemThinMaterialDark),
@"systemMaterialDark" : @(RNSBlurEffectStyleSystemMaterialDark),
@"systemThickMaterialDark" : @(RNSBlurEffectStyleSystemThickMaterialDark),
@"systemChromeMaterialDark" : @(RNSBlurEffectStyleSystemChromeMaterialDark),
}];
#endif
return blurEffects;
}
RCT_ENUM_CONVERTER(RNSBlurEffectStyle, ([self blurEffectsForIOSVersion]), RNSBlurEffectStyleSystemDefault, integerValue)
RCT_ENUM_CONVERTER(
RNSOptionalBoolean,
(@{
@"undefined" : @(RNSOptionalBooleanUndefined),
@"true" : @(RNSOptionalBooleanTrue),
@"false" : @(RNSOptionalBooleanFalse),
}),
RNSOptionalBooleanUndefined,
integerValue)
RCT_ENUM_CONVERTER(
RNSScrollEdgeEffect,
(@{
@"automatic" : @(RNSScrollEdgeEffectAutomatic),
@"hard" : @(RNSScrollEdgeEffectHard),
@"soft" : @(RNSScrollEdgeEffectSoft),
@"hidden" : @(RNSScrollEdgeEffectHidden),
}),
RNSScrollEdgeEffectAutomatic,
integerValue)
@end
#endif // !RCT_NEW_ARCH_ENABLED

View File

@@ -0,0 +1,13 @@
#pragma once
#ifdef RCT_NEW_ARCH_ENABLED
#include <React/RCTImageComponentView.h>
@interface RCTImageComponentView (RNSScreenStackHeaderConfig)
- (UIImage *)image;
@end
#endif // RCT_NEW_ARCH_ENABLED

View File

@@ -0,0 +1,14 @@
#ifdef RCT_NEW_ARCH_ENABLED
#include "RCTImageComponentView+RNSScreenStackHeaderConfig.h"
@implementation RCTImageComponentView (RNSScreenStackHeaderConfig)
- (UIImage *)image
{
return _imageView.image;
}
@end
#endif // RCT_NEW_ARCH_ENABLED

View File

@@ -0,0 +1,16 @@
#pragma once
#import <React/RCTImageLoader.h>
#import <UIKit/UIKit.h>
typedef void (^RNSBarButtonItemAction)(NSString *buttonId);
typedef void (^RNSBarButtonMenuItemAction)(NSString *menuId);
@interface RNSBarButtonItem : UIBarButtonItem
- (instancetype)initWithConfig:(NSDictionary<NSString *, id> *)dict
action:(RNSBarButtonItemAction)action
menuAction:(RNSBarButtonMenuItemAction)menuAction
imageLoader:(RCTImageLoader *)imageLoader;
@end

View File

@@ -0,0 +1,380 @@
#import "RNSBarButtonItem.h"
#import <React/RCTConvert.h>
#import <React/RCTFont.h>
#import "RNSDefines.h"
#import "RNSImageLoadingHelper.h"
static UIMenuOptions RNSMakeUIMenuOptionsFromConfig(NSDictionary *config);
@implementation RNSBarButtonItem {
NSString *_buttonId;
RNSBarButtonItemAction _itemAction;
}
- (instancetype)initWithConfig:(NSDictionary<NSString *, id> *)dict
action:(RNSBarButtonItemAction)action
menuAction:(RNSBarButtonMenuItemAction)menuAction
imageLoader:(RCTImageLoader *)imageLoader
{
self = [super init];
if (!self) {
return self;
}
NSString *title = dict[@"title"];
NSDictionary *imageSourceObj = dict[@"imageSource"];
NSDictionary *templateSourceObj = dict[@"templateSource"];
NSString *sfSymbolName = dict[@"sfSymbolName"];
NSString *xcassetName = dict[@"xcassetName"];
if (imageSourceObj != nil || templateSourceObj != nil) {
BOOL isTemplate = imageSourceObj != nil ? NO : YES;
NSDictionary *source = imageSourceObj != nil ? imageSourceObj : templateSourceObj;
[RNSImageLoadingHelper loadImageSyncIfPossibleFromJsonSource:source
withImageLoader:imageLoader
asTemplate:isTemplate
completionBlock:^(UIImage *image) {
self.image = image;
}];
} else if (sfSymbolName != nil) {
self.image = [UIImage systemImageNamed:sfSymbolName];
} else if (xcassetName != nil) {
self.image = [UIImage imageNamed:xcassetName];
}
if (title != nil) {
self.title = title;
NSDictionary *titleStyle = dict[@"titleStyle"];
if (titleStyle != nil) {
[self setTitleStyleFromConfig:titleStyle];
}
}
id tintColorObj = dict[@"tintColor"];
if (tintColorObj) {
self.tintColor = [RCTConvert UIColor:tintColorObj];
}
#if !TARGET_OS_TV
NSNumber *selectedNum = dict[@"selected"];
if (selectedNum != nil) {
self.selected = [selectedNum boolValue];
}
#endif
NSNumber *disabledNum = dict[@"disabled"];
if (disabledNum != nil) {
self.enabled = ![disabledNum boolValue];
}
NSNumber *width = dict[@"width"];
if (width) {
self.width = [width doubleValue];
}
#if !TARGET_OS_TV || __TV_OS_VERSION_MAX_ALLOWED >= 170000
if (@available(tvOS 17.0, *)) {
NSNumber *changesSelectionAsPrimaryActionNum = dict[@"changesSelectionAsPrimaryAction"];
if (changesSelectionAsPrimaryActionNum != nil) {
self.changesSelectionAsPrimaryAction = [changesSelectionAsPrimaryActionNum boolValue];
}
}
#endif
#if RNS_IPHONE_OS_VERSION_AVAILABLE(26_0)
if (@available(iOS 26.0, *)) {
NSNumber *hidesSharedBackgroundNum = dict[@"hidesSharedBackground"];
if (hidesSharedBackgroundNum != nil) {
self.hidesSharedBackground = [hidesSharedBackgroundNum boolValue];
}
NSNumber *sharesBackgroundNum = dict[@"sharesBackground"];
if (sharesBackgroundNum != nil) {
self.sharesBackground = [sharesBackgroundNum boolValue];
}
NSString *identifier = dict[@"identifier"];
if (identifier != nil) {
self.identifier = identifier;
}
NSDictionary *badgeConfig = dict[@"badge"];
if (badgeConfig != nil) {
[self setBadgeFromConfig:badgeConfig];
}
}
#endif
NSString *variant = dict[@"variant"];
if (variant) {
if ([variant isEqualToString:@"done"]) {
self.style = UIBarButtonItemStyleDone;
} else if ([variant isEqualToString:@"prominent"]) {
#if RNS_IPHONE_OS_VERSION_AVAILABLE(26_0)
if (@available(iOS 26.0, *)) {
self.style = UIBarButtonItemStyleProminent;
}
#endif
} else {
self.style = UIBarButtonItemStylePlain;
}
}
if (dict[@"accessibilityLabel"]) {
self.accessibilityLabel = dict[@"accessibilityLabel"];
}
if (dict[@"accessibilityHint"]) {
self.accessibilityHint = dict[@"accessibilityHint"];
}
#if !TARGET_OS_TV || __TV_OS_VERSION_MAX_ALLOWED >= 170000
if (@available(tvOS 17.0, *)) {
NSDictionary *menu = dict[@"menu"];
if (menu) {
self.menu = [[self class] initUIMenuWithDict:menu menuAction:menuAction];
}
}
#endif
NSString *buttonId = dict[@"buttonId"];
if (buttonId && action) {
self.target = self;
self.action = @selector(handleBarButtonItemPress:);
_itemAction = action;
_buttonId = buttonId;
}
return self;
}
+ (UIMenu *)initUIMenuWithDict:(NSDictionary<NSString *, id> *)dict menuAction:(RNSBarButtonMenuItemAction)menuAction
{
NSArray *items = dict[@"items"];
NSMutableArray<UIMenuElement *> *elements = [NSMutableArray new];
if (items.count > 0) {
for (NSDictionary *item in items) {
NSString *menuId = item[@"menuId"];
if (menuId) {
UIAction *actionItem = [self createActionItemFromConfig:item menuAction:menuAction];
[elements addObject:actionItem];
} else {
UIMenu *childMenu = [self initUIMenuWithDict:item menuAction:menuAction];
if (childMenu) {
[elements addObject:childMenu];
}
}
}
}
NSString *title = dict[@"title"];
NSString *sfSymbolName = dict[@"sfSymbolName"];
NSString *xcassetName = dict[@"xcassetName"];
UIImage* image = nil;
if (sfSymbolName != nil) {
image = [UIImage systemImageNamed:sfSymbolName];
} else if (xcassetName != nil) {
image = [UIImage imageNamed:xcassetName];
}
return [UIMenu menuWithTitle:title
image:image
identifier:nil
options:RNSMakeUIMenuOptionsFromConfig(dict)
children:elements];
}
+ (UIAction *)createActionItemFromConfig:(NSDictionary *)dict menuAction:(RNSBarButtonMenuItemAction)menuAction
{
NSString *menuId = dict[@"menuId"];
NSString *title = dict[@"title"];
NSString *sfSymbolName = dict[@"sfSymbolName"];
NSString *xcassetName = dict[@"xcassetName"];
UIImage *image = nil;
if (sfSymbolName != nil) {
image = [UIImage systemImageNamed:sfSymbolName];
} else if (xcassetName != nil) {
image = [UIImage imageNamed:xcassetName];
}
UIAction *actionElement = [UIAction actionWithTitle:title
image:image
identifier:nil
handler:^(__kindof UIAction *_Nonnull a) {
menuAction(menuId);
}];
NSString *discoverabilityLabel = dict[@"discoverabilityLabel"];
if (discoverabilityLabel != nil) {
actionElement.discoverabilityTitle = discoverabilityLabel;
}
NSString *state = dict[@"state"];
if ([state isEqualToString:@"on"]) {
actionElement.state = UIMenuElementStateOn;
} else if ([state isEqualToString:@"off"]) {
actionElement.state = UIMenuElementStateOff;
} else if ([state isEqualToString:@"mixed"]) {
actionElement.state = UIMenuElementStateMixed;
}
NSNumber *disabled = dict[@"disabled"];
NSNumber *hidden = dict[@"hidden"];
NSNumber *destructive = dict[@"destructive"];
NSNumber *keepsMenuPresented = dict[@"keepsMenuPresented"];
if (disabled != nil && [disabled boolValue]) {
actionElement.attributes |= UIMenuElementAttributesDisabled;
}
if (hidden != nil && [hidden boolValue]) {
actionElement.attributes |= UIMenuElementAttributesHidden;
}
if (destructive != nil && [destructive boolValue]) {
actionElement.attributes |= UIMenuElementAttributesDestructive;
}
if (keepsMenuPresented != nil && [keepsMenuPresented boolValue]) {
#if RNS_IPHONE_OS_VERSION_AVAILABLE(16_0)
if (@available(iOS 16.0, *)) {
actionElement.attributes |= UIMenuElementAttributesKeepsMenuPresented;
}
#endif
#if TARGET_OS_TV && __TV_OS_VERSION_MAX_ALLOWED >= 160000
if (@available(tvOS 16.0, *)) {
actionElement.attributes |= UIMenuElementAttributesKeepsMenuPresented;
}
#endif
}
NSString *subtitle = dict[@"subtitle"];
if (subtitle != nil) {
actionElement.subtitle = subtitle;
}
return actionElement;
}
- (void)handleBarButtonItemPress:(UIBarButtonItem *)item
{
if (_itemAction && _buttonId) {
_itemAction(_buttonId);
}
}
- (void)setTitleStyleFromConfig:(NSDictionary *)titleStyle
{
NSString *fontFamily = titleStyle[@"fontFamily"];
NSNumber *fontSize = titleStyle[@"fontSize"];
NSString *fontWeight = titleStyle[@"fontWeight"];
NSMutableDictionary *attrs = [NSMutableDictionary new];
if (fontFamily || fontWeight) {
NSNumber *resolvedFontSize = fontSize;
if (!resolvedFontSize) {
#if TARGET_OS_TV
resolvedFontSize = [NSNumber numberWithDouble:17.0];
#else
resolvedFontSize = [NSNumber numberWithFloat:[UIFont labelFontSize]];
#endif
}
attrs[NSFontAttributeName] = [RCTFont updateFont:nil
withFamily:fontFamily
size:resolvedFontSize
weight:fontWeight
style:nil
variant:nil
scaleMultiplier:1.0];
} else {
CGFloat resolvedFontSize = fontSize ? [fontSize floatValue] : 0;
if (resolvedFontSize == 0) {
#if TARGET_OS_TV
resolvedFontSize = 17.0;
#else
resolvedFontSize = [UIFont labelFontSize];
#endif
}
attrs[NSFontAttributeName] = [UIFont systemFontOfSize:resolvedFontSize];
}
id titleColor = titleStyle[@"color"];
if (titleColor) {
attrs[NSForegroundColorAttributeName] = [RCTConvert UIColor:titleColor];
}
[self setTitleTextAttributes:attrs forState:UIControlStateNormal];
[self setTitleTextAttributes:attrs forState:UIControlStateHighlighted];
[self setTitleTextAttributes:attrs forState:UIControlStateDisabled];
[self setTitleTextAttributes:attrs forState:UIControlStateSelected];
[self setTitleTextAttributes:attrs forState:UIControlStateFocused];
}
#if RNS_IPHONE_OS_VERSION_AVAILABLE(26_0)
- (void)setBadgeFromConfig:(NSDictionary *)badgeObj
{
if (@available(iOS 26.0, *)) {
UIBarButtonItemBadge *badge = [UIBarButtonItemBadge badgeWithString:badgeObj[@"value"]];
NSDictionary *style = badgeObj[@"style"];
if (style) {
id colorObj = style[@"color"];
if (colorObj) {
badge.foregroundColor = [RCTConvert UIColor:colorObj];
}
id backgroundColorObj = style[@"backgroundColor"];
if (backgroundColorObj) {
badge.backgroundColor = [RCTConvert UIColor:backgroundColorObj];
}
NSString *fontFamily = style[@"fontFamily"];
NSNumber *fontSize = style[@"fontSize"];
NSString *fontWeight = style[@"fontWeight"];
if (fontSize || fontWeight) {
badge.font = [RCTFont updateFont:nil
withFamily:fontFamily
size:fontSize
weight:fontWeight
style:nil
variant:nil
scaleMultiplier:1.0];
} else {
CGFloat resolvedFontSize = fontSize ? [fontSize floatValue] : 0;
if (resolvedFontSize == 0) {
#if TARGET_OS_TV
resolvedFontSize = 17.0;
#else
resolvedFontSize = [UIFont labelFontSize];
#endif
}
badge.font = [UIFont systemFontOfSize:resolvedFontSize];
}
}
self.badge = badge;
}
}
#endif
@end
UIMenuOptions RNSMakeUIMenuOptionsFromConfig(NSDictionary *config)
{
UIMenuOptions options = 0;
NSNumber *singleSelection = config[@"singleSelection"];
NSNumber *displayAsPalette = config[@"displayAsPalette"];
NSNumber *displayInline = config[@"displayInline"];
NSNumber *destructive = config[@"destructive"];
if (singleSelection != nil && [singleSelection boolValue]) {
options |= UIMenuOptionsSingleSelection;
}
#if RNS_IPHONE_OS_VERSION_AVAILABLE(17_0)
if (@available(iOS 17.0, *)) {
if (displayAsPalette != nil && [displayAsPalette boolValue]) {
options |= UIMenuOptionsDisplayAsPalette;
}
}
#endif // RNS_IPHONE_OS_VERSION_AVAILABLE(17_0)
if (displayInline != nil && [displayInline boolValue]) {
options |= UIMenuOptionsDisplayInline;
}
if (destructive != nil && [destructive boolValue]) {
options |= UIMenuOptionsDestructive;
}
return options;
}

80
node_modules/react-native-screens/ios/RNSConvert.h generated vendored Normal file
View File

@@ -0,0 +1,80 @@
#pragma once
#import <UIKit/UIKit.h>
#ifdef RCT_NEW_ARCH_ENABLED
#import <react/renderer/components/rnscreens/Props.h>
#endif // RCT_NEW_ARCH_ENABLED
#import "RNSEnums.h"
#ifdef RCT_NEW_ARCH_ENABLED
namespace react = facebook::react;
#endif // RCT_NEW_ARCH_ENABLED
@interface RNSConvert : NSObject
#ifdef RCT_NEW_ARCH_ENABLED
+ (UISemanticContentAttribute)UISemanticContentAttributeFromCppEquivalent:
(react::RNSScreenStackHeaderConfigDirection)direction;
+ (UINavigationItemBackButtonDisplayMode)UINavigationItemBackButtonDisplayModeFromCppEquivalent:
(react::RNSScreenStackHeaderConfigBackButtonDisplayMode)backButtonDisplayMode;
+ (RNSOptionalBoolean)RNSOptionalBooleanFromRNSFullScreenSwipeEnabledCppEquivalent:
(react::RNSScreenFullScreenSwipeEnabled)fullScreenSwipeEnabled;
+ (RNSScreenStackPresentation)RNSScreenStackPresentationFromCppEquivalent:
(react::RNSScreenStackPresentation)stackPresentation;
+ (RNSScreenStackAnimation)RNSScreenStackAnimationFromCppEquivalent:(react::RNSScreenStackAnimation)stackAnimation;
+ (RNSScreenStackHeaderSubviewType)RNSScreenStackHeaderSubviewTypeFromCppEquivalent:
(react::RNSScreenStackHeaderSubviewType)subviewType;
+ (RNSScreenReplaceAnimation)RNSScreenReplaceAnimationFromCppEquivalent:
(react::RNSScreenReplaceAnimation)replaceAnimation;
+ (RNSScreenSwipeDirection)RNSScreenSwipeDirectionFromCppEquivalent:(react::RNSScreenSwipeDirection)swipeDirection;
+ (NSArray<NSNumber *> *)detentFractionsArrayFromVector:(const std::vector<react::Float> &)detents;
+ (NSDictionary *)gestureResponseDistanceDictFromCppStruct:
(const react::RNSScreenGestureResponseDistanceStruct &)gestureResponseDistance;
#if !TARGET_OS_VISION
+ (UITextAutocapitalizationType)UITextAutocapitalizationTypeFromCppEquivalent:
(react::RNSSearchBarAutoCapitalize)autoCapitalize;
#endif
+ (RNSSearchBarPlacement)RNSScreenSearchBarPlacementFromCppEquivalent:(react::RNSSearchBarPlacement)placement;
+ (RNSOptionalBoolean)RNSOptionalBooleanFromRNSSearchBarObscureBackground:
(react::RNSSearchBarObscureBackground)obscureBackground;
+ (RNSOptionalBoolean)RNSOptionalBooleanFromRNSSearchBarHideNavigationBar:
(react::RNSSearchBarHideNavigationBar)hideNavigationBar;
+ (UIUserInterfaceStyle)UIUserInterfaceStyleFromCppEquivalent:
(react::RNSScreenStackHeaderConfigUserInterfaceStyle)userInterfaceStyle;
+ (NSMutableArray<NSNumber *> *)arrayFromVector:(const std::vector<CGFloat> &)vector;
+ (RNSBlurEffectStyle)RNSBlurEffectStyleFromCppEquivalent:(react::RNSScreenStackHeaderConfigBlurEffect)blurEffect;
+ (RNSScrollEdgeEffect)RNSScrollEdgeEffectFromScreenBottomScrollEdgeEffectCppEquivalent:
(react::RNSScreenBottomScrollEdgeEffect)edgeEffect;
+ (RNSScrollEdgeEffect)RNSScrollEdgeEffectFromScreenLeftScrollEdgeEffectCppEquivalent:
(react::RNSScreenLeftScrollEdgeEffect)edgeEffect;
+ (RNSScrollEdgeEffect)RNSScrollEdgeEffectFromScreenRightScrollEdgeEffectCppEquivalent:
(react::RNSScreenRightScrollEdgeEffect)edgeEffect;
+ (RNSScrollEdgeEffect)RNSScrollEdgeEffectFromScreenTopScrollEdgeEffectCppEquivalent:
(react::RNSScreenTopScrollEdgeEffect)edgeEffect;
+ (id)idFromFollyDynamic:(const folly::dynamic &)dyn;
#endif // RCT_NEW_ARCH_ENABLED
/// This method fails (by assertion) when `blurEffect == RNSBlurEffectStyleNone` or `blurEffect ==
/// RNSBlurEffectStyleSystemDefault` which have no counter parts in the UIKit types.
+ (UIBlurEffectStyle)tryConvertRNSBlurEffectStyleToUIBlurEffectStyle:(RNSBlurEffectStyle)blurEffect;
@end

406
node_modules/react-native-screens/ios/RNSConvert.mm generated vendored Normal file
View File

@@ -0,0 +1,406 @@
#import "RNSConvert.h"
#import <React/RCTLog.h>
#ifndef RCT_NEW_ARCH_ENABLED
#import <React/RCTAssert.h>
#endif // !RCT_NEW_ARCH_ENABLED
@implementation RNSConvert
#ifdef RCT_NEW_ARCH_ENABLED
+ (UISemanticContentAttribute)UISemanticContentAttributeFromCppEquivalent:
(react::RNSScreenStackHeaderConfigDirection)direction
{
switch (direction) {
using enum react::RNSScreenStackHeaderConfigDirection;
case Rtl:
return UISemanticContentAttributeForceRightToLeft;
case Ltr:
return UISemanticContentAttributeForceLeftToRight;
}
}
+ (UINavigationItemBackButtonDisplayMode)UINavigationItemBackButtonDisplayModeFromCppEquivalent:
(react::RNSScreenStackHeaderConfigBackButtonDisplayMode)backButtonDisplayMode
{
switch (backButtonDisplayMode) {
using enum react::RNSScreenStackHeaderConfigBackButtonDisplayMode;
case Default:
return UINavigationItemBackButtonDisplayModeDefault;
case Generic:
return UINavigationItemBackButtonDisplayModeGeneric;
case Minimal:
return UINavigationItemBackButtonDisplayModeMinimal;
}
}
+ (RNSOptionalBoolean)RNSOptionalBooleanFromRNSFullScreenSwipeEnabledCppEquivalent:
(react::RNSScreenFullScreenSwipeEnabled)fullScreenSwipeEnabled
{
switch (fullScreenSwipeEnabled) {
using enum react::RNSScreenFullScreenSwipeEnabled;
case Undefined:
return RNSOptionalBooleanUndefined;
case True:
return RNSOptionalBooleanTrue;
case False:
return RNSOptionalBooleanFalse;
}
}
+ (RNSScreenStackPresentation)RNSScreenStackPresentationFromCppEquivalent:
(react::RNSScreenStackPresentation)stackPresentation
{
switch (stackPresentation) {
using enum react::RNSScreenStackPresentation;
case Push:
return RNSScreenStackPresentationPush;
case Modal:
return RNSScreenStackPresentationModal;
case FullScreenModal:
return RNSScreenStackPresentationFullScreenModal;
case FormSheet:
return RNSScreenStackPresentationFormSheet;
case PageSheet:
return RNSScreenStackPresentationPageSheet;
case ContainedModal:
return RNSScreenStackPresentationContainedModal;
case TransparentModal:
return RNSScreenStackPresentationTransparentModal;
case ContainedTransparentModal:
return RNSScreenStackPresentationContainedTransparentModal;
}
}
+ (RNSScreenStackAnimation)RNSScreenStackAnimationFromCppEquivalent:(react::RNSScreenStackAnimation)stackAnimation
{
switch (stackAnimation) {
using enum react::RNSScreenStackAnimation;
// these three are intentionally grouped
case Slide_from_right:
case Ios_from_right:
case Default:
return RNSScreenStackAnimationDefault;
// these two are intentionally grouped
case Slide_from_left:
case Ios_from_left:
return RNSScreenStackAnimationSlideFromLeft;
case Flip:
return RNSScreenStackAnimationFlip;
case Simple_push:
return RNSScreenStackAnimationSimplePush;
case None:
return RNSScreenStackAnimationNone;
case Fade:
return RNSScreenStackAnimationFade;
case Slide_from_bottom:
return RNSScreenStackAnimationSlideFromBottom;
case Fade_from_bottom:
return RNSScreenStackAnimationFadeFromBottom;
}
}
+ (RNSScreenStackHeaderSubviewType)RNSScreenStackHeaderSubviewTypeFromCppEquivalent:
(react::RNSScreenStackHeaderSubviewType)subviewType
{
switch (subviewType) {
using enum react::RNSScreenStackHeaderSubviewType;
case Left:
return RNSScreenStackHeaderSubviewTypeLeft;
case Right:
return RNSScreenStackHeaderSubviewTypeRight;
case Title:
return RNSScreenStackHeaderSubviewTypeTitle;
case Center:
return RNSScreenStackHeaderSubviewTypeCenter;
case SearchBar:
return RNSScreenStackHeaderSubviewTypeSearchBar;
case Back:
return RNSScreenStackHeaderSubviewTypeBackButton;
}
}
+ (RNSScreenReplaceAnimation)RNSScreenReplaceAnimationFromCppEquivalent:
(react::RNSScreenReplaceAnimation)replaceAnimation
{
switch (replaceAnimation) {
using enum react::RNSScreenReplaceAnimation;
case Pop:
return RNSScreenReplaceAnimationPop;
case Push:
return RNSScreenReplaceAnimationPush;
}
}
+ (RNSScreenSwipeDirection)RNSScreenSwipeDirectionFromCppEquivalent:(react::RNSScreenSwipeDirection)swipeDirection
{
switch (swipeDirection) {
using enum react::RNSScreenSwipeDirection;
case Horizontal:
return RNSScreenSwipeDirectionHorizontal;
case Vertical:
return RNSScreenSwipeDirectionVertical;
}
}
#define SWITCH_EDGE_EFFECT(X) \
switch (edgeEffect) { \
using enum react::X; \
case Automatic: \
return RNSScrollEdgeEffectAutomatic; \
case Hard: \
return RNSScrollEdgeEffectHard; \
case Soft: \
return RNSScrollEdgeEffectSoft; \
case Hidden: \
return RNSScrollEdgeEffectHidden; \
default: \
RCTLogError(@"[RNScreens] unsupported edge effect"); \
return RNSScrollEdgeEffectAutomatic; \
}
+ (RNSScrollEdgeEffect)RNSScrollEdgeEffectFromScreenBottomScrollEdgeEffectCppEquivalent:
(react::RNSScreenBottomScrollEdgeEffect)edgeEffect
{
SWITCH_EDGE_EFFECT(RNSScreenBottomScrollEdgeEffect);
}
+ (RNSScrollEdgeEffect)RNSScrollEdgeEffectFromScreenLeftScrollEdgeEffectCppEquivalent:
(react::RNSScreenLeftScrollEdgeEffect)edgeEffect
{
SWITCH_EDGE_EFFECT(RNSScreenLeftScrollEdgeEffect);
}
+ (RNSScrollEdgeEffect)RNSScrollEdgeEffectFromScreenRightScrollEdgeEffectCppEquivalent:
(react::RNSScreenRightScrollEdgeEffect)edgeEffect
{
SWITCH_EDGE_EFFECT(RNSScreenRightScrollEdgeEffect);
}
+ (RNSScrollEdgeEffect)RNSScrollEdgeEffectFromScreenTopScrollEdgeEffectCppEquivalent:
(react::RNSScreenTopScrollEdgeEffect)edgeEffect
{
SWITCH_EDGE_EFFECT(RNSScreenTopScrollEdgeEffect);
}
#undef SWITCH_EDGE_EFFECT
+ (NSArray<NSNumber *> *)detentFractionsArrayFromVector:(const std::vector<react::Float> &)detents
{
auto array = [NSMutableArray<NSNumber *> arrayWithCapacity:detents.size()];
for (const react::Float value : detents) {
[array addObject:[NSNumber numberWithFloat:value]];
}
return array;
}
+ (NSDictionary *)gestureResponseDistanceDictFromCppStruct:
(const react::RNSScreenGestureResponseDistanceStruct &)gestureResponseDistance
{
return @{
@"start" : @(gestureResponseDistance.start),
@"end" : @(gestureResponseDistance.end),
@"top" : @(gestureResponseDistance.top),
@"bottom" : @(gestureResponseDistance.bottom),
};
}
#if !TARGET_OS_VISION
+ (UITextAutocapitalizationType)UITextAutocapitalizationTypeFromCppEquivalent:
(react::RNSSearchBarAutoCapitalize)autoCapitalize
{
switch (autoCapitalize) {
using enum react::RNSSearchBarAutoCapitalize;
case Words:
return UITextAutocapitalizationTypeWords;
case SystemDefault:
case Sentences:
return UITextAutocapitalizationTypeSentences;
case Characters:
return UITextAutocapitalizationTypeAllCharacters;
case None:
return UITextAutocapitalizationTypeNone;
}
}
#endif
+ (RNSSearchBarPlacement)RNSScreenSearchBarPlacementFromCppEquivalent:(react::RNSSearchBarPlacement)placement
{
switch (placement) {
using enum react::RNSSearchBarPlacement;
case Stacked:
return RNSSearchBarPlacementStacked;
case Automatic:
return RNSSearchBarPlacementAutomatic;
case Inline:
return RNSSearchBarPlacementInline;
case Integrated:
return RNSSearchBarPlacementIntegrated;
case IntegratedButton:
return RNSSearchBarPlacementIntegratedButton;
case IntegratedCentered:
return RNSSearchBarPlacementIntegratedCentered;
}
}
+ (RNSOptionalBoolean)RNSOptionalBooleanFromRNSSearchBarObscureBackground:
(react::RNSSearchBarObscureBackground)obscureBackground
{
switch (obscureBackground) {
using enum react::RNSSearchBarObscureBackground;
case Undefined:
return RNSOptionalBooleanUndefined;
case True:
return RNSOptionalBooleanTrue;
case False:
return RNSOptionalBooleanFalse;
}
}
+ (RNSOptionalBoolean)RNSOptionalBooleanFromRNSSearchBarHideNavigationBar:
(react::RNSSearchBarHideNavigationBar)hideNavigationBar
{
switch (hideNavigationBar) {
using enum react::RNSSearchBarHideNavigationBar;
case Undefined:
return RNSOptionalBooleanUndefined;
case True:
return RNSOptionalBooleanTrue;
case False:
return RNSOptionalBooleanFalse;
}
}
+ (UIUserInterfaceStyle)UIUserInterfaceStyleFromCppEquivalent:
(react::RNSScreenStackHeaderConfigUserInterfaceStyle)userInterfaceStyle
{
switch (userInterfaceStyle) {
using enum react::RNSScreenStackHeaderConfigUserInterfaceStyle;
case Unspecified:
return UIUserInterfaceStyleUnspecified;
case Light:
return UIUserInterfaceStyleLight;
case Dark:
return UIUserInterfaceStyleDark;
default:
RCTLogError(@"[RNScreens] unsupported user interface style");
}
}
+ (NSMutableArray<NSNumber *> *)arrayFromVector:(const std::vector<CGFloat> &)vector
{
NSMutableArray *array = [NSMutableArray arrayWithCapacity:vector.size()];
for (CGFloat val : vector) {
[array addObject:[NSNumber numberWithFloat:val]];
}
return array;
}
+ (RNSBlurEffectStyle)RNSBlurEffectStyleFromCppEquivalent:(react::RNSScreenStackHeaderConfigBlurEffect)blurEffect
{
using enum react::RNSScreenStackHeaderConfigBlurEffect;
switch (blurEffect) {
case None:
return RNSBlurEffectStyleNone;
case ExtraLight:
return RNSBlurEffectStyleExtraLight;
case Light:
return RNSBlurEffectStyleLight;
case Dark:
return RNSBlurEffectStyleDark;
case Regular:
return RNSBlurEffectStyleRegular;
case Prominent:
return RNSBlurEffectStyleProminent;
#if !TARGET_OS_TV
case SystemUltraThinMaterial:
return RNSBlurEffectStyleSystemUltraThinMaterial;
case SystemThinMaterial:
return RNSBlurEffectStyleSystemThinMaterial;
case SystemMaterial:
return RNSBlurEffectStyleSystemMaterial;
case SystemThickMaterial:
return RNSBlurEffectStyleSystemThickMaterial;
case SystemChromeMaterial:
return RNSBlurEffectStyleSystemChromeMaterial;
case SystemUltraThinMaterialLight:
return RNSBlurEffectStyleSystemUltraThinMaterialLight;
case SystemThinMaterialLight:
return RNSBlurEffectStyleSystemThinMaterialLight;
case SystemMaterialLight:
return RNSBlurEffectStyleSystemMaterialLight;
case SystemThickMaterialLight:
return RNSBlurEffectStyleSystemThickMaterialLight;
case SystemChromeMaterialLight:
return RNSBlurEffectStyleSystemChromeMaterialLight;
case SystemUltraThinMaterialDark:
return RNSBlurEffectStyleSystemUltraThinMaterialDark;
case SystemThinMaterialDark:
return RNSBlurEffectStyleSystemThinMaterialDark;
case SystemMaterialDark:
return RNSBlurEffectStyleSystemMaterialDark;
case SystemThickMaterialDark:
return RNSBlurEffectStyleSystemThickMaterialDark;
case SystemChromeMaterialDark:
return RNSBlurEffectStyleSystemChromeMaterialDark;
default:
RCTLogError(@"[RNScreens] unsupported blur effect style");
return RNSBlurEffectStyleNone;
#else // !TARGET_OS_TV
default:
return RNSBlurEffectStyleNone;
#endif // !TARGET_OS_TV
}
}
+ (id)idFromFollyDynamic:(const folly::dynamic &)dyn
{
if (dyn.isNull()) {
return nil;
} else if (dyn.isBool()) {
return [NSNumber numberWithBool:dyn.getBool()];
} else if (dyn.isInt()) {
return [NSNumber numberWithLongLong:dyn.getInt()];
} else if (dyn.isDouble()) {
return [NSNumber numberWithDouble:dyn.getDouble()];
} else if (dyn.isString()) {
return [NSString stringWithUTF8String:dyn.getString().c_str()];
} else if (dyn.isArray()) {
NSMutableArray *array = [NSMutableArray arrayWithCapacity:dyn.size()];
for (const auto &item : dyn) {
[array addObject:[self idFromFollyDynamic:item]];
}
return array;
} else if (dyn.isObject()) {
NSMutableDictionary *dict = [NSMutableDictionary dictionaryWithCapacity:dyn.size()];
for (const auto &pair : dyn.items()) {
dict[@(pair.first.c_str())] = [self idFromFollyDynamic:pair.second];
}
return dict;
}
return nil;
}
#endif // RCT_NEW_ARCH_ENABLED
+ (UIBlurEffectStyle)tryConvertRNSBlurEffectStyleToUIBlurEffectStyle:(RNSBlurEffectStyle)blurEffect
{
#ifdef RCT_NEW_ARCH_ENABLED
react_native_assert(blurEffect != RNSBlurEffectStyleNone && blurEffect != RNSBlurEffectStyleSystemDefault);
#else
RCTAssert(
blurEffect != RNSBlurEffectStyleNone && blurEffect != RNSBlurEffectStyleSystemDefault,
@"RNSBlurEffectStyleNone and RNSBlurEffectStyleSystemDefault variants are not convertible to UIBlurEffectStyle");
#endif // RCT_NEW_ARCH_ENABLED
// Cast safety: RNSBlurEffectStyle is defined in such way that its values map 1:1 with
// UIBlurEffectStyle, except RNSBlurEffectStyleNone and RNSBlurEffectStyleSystemDefault which are excluded above.
return (UIBlurEffectStyle)blurEffect;
}
@end

191
node_modules/react-native-screens/ios/RNSEnums.h generated vendored Normal file
View File

@@ -0,0 +1,191 @@
#pragma once
typedef NS_ENUM(NSInteger, RNSScreenStackPresentation) {
RNSScreenStackPresentationPush,
RNSScreenStackPresentationModal,
RNSScreenStackPresentationTransparentModal,
RNSScreenStackPresentationContainedModal,
RNSScreenStackPresentationContainedTransparentModal,
RNSScreenStackPresentationFullScreenModal,
RNSScreenStackPresentationFormSheet,
RNSScreenStackPresentationPageSheet,
};
typedef NS_ENUM(NSInteger, RNSScreenStackAnimation) {
RNSScreenStackAnimationDefault,
RNSScreenStackAnimationNone,
RNSScreenStackAnimationFade,
RNSScreenStackAnimationFadeFromBottom,
RNSScreenStackAnimationFlip,
RNSScreenStackAnimationSlideFromBottom,
RNSScreenStackAnimationSimplePush,
RNSScreenStackAnimationSlideFromLeft,
};
typedef NS_ENUM(NSInteger, RNSScreenReplaceAnimation) {
RNSScreenReplaceAnimationPop,
RNSScreenReplaceAnimationPush,
};
typedef NS_ENUM(NSInteger, RNSScreenSwipeDirection) {
RNSScreenSwipeDirectionHorizontal,
RNSScreenSwipeDirectionVertical,
};
typedef NS_ENUM(NSInteger, RNSActivityState) {
RNSActivityStateUndefined = -1,
RNSActivityStateInactive = 0,
RNSActivityStateTransitioningOrBelowTop = 1,
RNSActivityStateOnTop = 2
};
typedef NS_ENUM(NSInteger, RNSScrollEdgeEffect) {
RNSScrollEdgeEffectAutomatic,
RNSScrollEdgeEffectHard,
RNSScrollEdgeEffectSoft,
RNSScrollEdgeEffectHidden,
};
typedef NS_ENUM(NSInteger, RNSStatusBarStyle) {
RNSStatusBarStyleAuto,
RNSStatusBarStyleInverted,
RNSStatusBarStyleLight,
RNSStatusBarStyleDark,
};
typedef NS_ENUM(NSInteger, RNSWindowTrait) {
RNSWindowTraitStyle,
RNSWindowTraitAnimation,
RNSWindowTraitHidden,
RNSWindowTraitOrientation,
RNSWindowTraitHomeIndicatorHidden,
};
typedef NS_ENUM(NSInteger, RNSScreenStackHeaderSubviewType) {
RNSScreenStackHeaderSubviewTypeBackButton,
RNSScreenStackHeaderSubviewTypeLeft,
RNSScreenStackHeaderSubviewTypeRight,
RNSScreenStackHeaderSubviewTypeTitle,
RNSScreenStackHeaderSubviewTypeCenter,
RNSScreenStackHeaderSubviewTypeSearchBar,
};
typedef NS_ENUM(NSInteger, RNSScreenDetentType) {
RNSScreenDetentTypeMedium,
RNSScreenDetentTypeLarge,
RNSScreenDetentTypeAll,
};
typedef NS_ENUM(NSInteger, RNSSearchBarPlacement) {
RNSSearchBarPlacementAutomatic,
RNSSearchBarPlacementInline,
RNSSearchBarPlacementStacked,
RNSSearchBarPlacementIntegrated,
RNSSearchBarPlacementIntegratedButton,
RNSSearchBarPlacementIntegratedCentered,
};
typedef NS_ENUM(NSInteger, RNSSplitViewScreenColumnType) {
RNSSplitViewScreenColumnTypeColumn,
RNSSplitViewScreenColumnTypeInspector,
};
// Redefinition of UIBlurEffectStyle. We need to represent additional cases of `None` and `SystemDefault`.
typedef NS_ENUM(NSInteger, RNSBlurEffectStyle) {
/// Default blur effect should be used
RNSBlurEffectStyleSystemDefault = -2,
/// No blur effect should be visible
RNSBlurEffectStyleNone = -1,
RNSBlurEffectStyleExtraLight = UIBlurEffectStyleExtraLight,
RNSBlurEffectStyleLight = UIBlurEffectStyleLight,
RNSBlurEffectStyleDark = UIBlurEffectStyleDark,
// TODO: Add support for this variant on tvOS
// RNSBlurEffectStyleExtraDark = UIBlurEffectStyleExtraDark API_UNAVAILABLE(ios) API_UNAVAILABLE(watchos),
RNSBlurEffectStyleRegular API_UNAVAILABLE(watchos) = UIBlurEffectStyleRegular,
RNSBlurEffectStyleProminent API_UNAVAILABLE(watchos) = UIBlurEffectStyleProminent,
RNSBlurEffectStyleSystemUltraThinMaterial API_UNAVAILABLE(watchos, tvos) = UIBlurEffectStyleSystemUltraThinMaterial,
RNSBlurEffectStyleSystemThinMaterial API_UNAVAILABLE(watchos, tvos) = UIBlurEffectStyleSystemThinMaterial,
RNSBlurEffectStyleSystemMaterial API_UNAVAILABLE(watchos, tvos) = UIBlurEffectStyleSystemMaterial,
RNSBlurEffectStyleSystemThickMaterial API_UNAVAILABLE(watchos, tvos) = UIBlurEffectStyleSystemThickMaterial,
RNSBlurEffectStyleSystemChromeMaterial API_UNAVAILABLE(watchos, tvos) = UIBlurEffectStyleSystemChromeMaterial,
RNSBlurEffectStyleSystemUltraThinMaterialLight API_UNAVAILABLE(watchos, tvos) =
UIBlurEffectStyleSystemUltraThinMaterialLight,
RNSBlurEffectStyleSystemThinMaterialLight API_UNAVAILABLE(watchos, tvos) = UIBlurEffectStyleSystemThinMaterialLight,
RNSBlurEffectStyleSystemMaterialLight API_UNAVAILABLE(watchos, tvos) = UIBlurEffectStyleSystemMaterialLight,
RNSBlurEffectStyleSystemThickMaterialLight API_UNAVAILABLE(watchos, tvos) = UIBlurEffectStyleSystemThickMaterialLight,
RNSBlurEffectStyleSystemChromeMaterialLight API_UNAVAILABLE(watchos, tvos) =
UIBlurEffectStyleSystemChromeMaterialLight,
RNSBlurEffectStyleSystemUltraThinMaterialDark API_UNAVAILABLE(watchos, tvos) =
UIBlurEffectStyleSystemUltraThinMaterialDark,
RNSBlurEffectStyleSystemThinMaterialDark API_UNAVAILABLE(watchos, tvos) = UIBlurEffectStyleSystemThinMaterialDark,
RNSBlurEffectStyleSystemMaterialDark API_UNAVAILABLE(watchos, tvos) = UIBlurEffectStyleSystemMaterialDark,
RNSBlurEffectStyleSystemThickMaterialDark API_UNAVAILABLE(watchos, tvos) = UIBlurEffectStyleSystemThickMaterialDark,
RNSBlurEffectStyleSystemChromeMaterialDark API_UNAVAILABLE(watchos, tvos) = UIBlurEffectStyleSystemChromeMaterialDark
} API_UNAVAILABLE(watchos);
typedef NS_ENUM(NSInteger, RNSBottomTabsIconType) {
RNSBottomTabsIconTypeImage,
RNSBottomTabsIconTypeTemplate,
RNSBottomTabsIconTypeSfSymbol,
RNSBottomTabsIconTypeXcasset,
};
#if !RCT_NEW_ARCH_ENABLED
typedef NS_ENUM(NSInteger, RNSTabBarMinimizeBehavior) {
RNSTabBarMinimizeBehaviorAutomatic,
RNSTabBarMinimizeBehaviorNever,
RNSTabBarMinimizeBehaviorOnScrollDown,
RNSTabBarMinimizeBehaviorOnScrollUp,
};
#endif
#if !RCT_NEW_ARCH_ENABLED
typedef NS_ENUM(NSInteger, RNSTabBarControllerMode) {
RNSTabBarControllerModeAutomatic,
RNSTabBarControllerModeTabBar,
RNSTabBarControllerModeTabSidebar,
};
#endif
// TODO: investigate objc - swift interop and deduplicate this code
// This enum needs to be compatible with the RNSOrientationSwift enum.
typedef NS_ENUM(NSInteger, RNSOrientation) {
RNSOrientationInherit,
RNSOrientationAll,
RNSOrientationAllButUpsideDown,
RNSOrientationPortrait,
RNSOrientationPortraitUp,
RNSOrientationPortraitDown,
RNSOrientationLandscape,
RNSOrientationLandscapeLeft,
RNSOrientationLandscapeRight,
};
typedef NS_ENUM(NSInteger, RNSBottomTabsScreenSystemItem) {
RNSBottomTabsScreenSystemItemNone,
RNSBottomTabsScreenSystemItemBookmarks,
RNSBottomTabsScreenSystemItemContacts,
RNSBottomTabsScreenSystemItemDownloads,
RNSBottomTabsScreenSystemItemFavorites,
RNSBottomTabsScreenSystemItemFeatured,
RNSBottomTabsScreenSystemItemHistory,
RNSBottomTabsScreenSystemItemMore,
RNSBottomTabsScreenSystemItemMostRecent,
RNSBottomTabsScreenSystemItemMostViewed,
RNSBottomTabsScreenSystemItemRecents,
RNSBottomTabsScreenSystemItemSearch,
RNSBottomTabsScreenSystemItemTopRated
};
typedef NS_ENUM(NSInteger, RNSOptionalBoolean) {
RNSOptionalBooleanUndefined,
RNSOptionalBooleanTrue,
RNSOptionalBooleanFalse
};
typedef NS_ENUM(NSInteger, RNSBottomTabsAccessoryEnvironment) {
RNSBottomTabsAccessoryEnvironmentRegular,
RNSBottomTabsAccessoryEnvironmentInline
};

View File

@@ -0,0 +1,38 @@
#pragma once
#import <React/RCTViewManager.h>
#ifdef RCT_NEW_ARCH_ENABLED
#import <React/RCTViewComponentView.h>
#else
#import <React/RCTInvalidating.h>
#import <React/RCTView.h>
#endif
#ifdef RCT_NEW_ARCH_ENABLED
namespace react = facebook::react;
#endif // RCT_NEW_ARCH_ENABLED
@interface RNSFullWindowOverlayManager : RCTViewManager
@end
@interface RNSFullWindowOverlayContainer : UIView
@end
@interface RNSFullWindowOverlay :
#ifdef RCT_NEW_ARCH_ENABLED
RCTViewComponentView
#else
RCTView <RCTInvalidating>
#endif // RCT_NEW_ARCH_ENABLED
@property (nonatomic) BOOL accessibilityContainerViewIsModal;
#ifdef RCT_NEW_ARCH_ENABLED
@property (nonatomic) react::LayoutMetrics oldLayoutMetrics;
@property (nonatomic) react::LayoutMetrics newLayoutMetrics;
#endif // RCT_NEW_ARCH_ENABLED
@end

View File

@@ -0,0 +1,296 @@
#import <UIKit/UIKit.h>
#import "RNSDefines.h"
#import "RNSFullWindowOverlay.h"
#ifdef RCT_NEW_ARCH_ENABLED
#import <React/RCTConversions.h>
#import <React/RCTFabricComponentsPlugins.h>
#import <React/RCTSurfaceTouchHandler.h>
#import <react/renderer/components/rnscreens/ComponentDescriptors.h>
#import <react/renderer/components/rnscreens/Props.h>
#import <react/renderer/components/rnscreens/RCTComponentViewHelpers.h>
#import <rnscreens/RNSFullWindowOverlayComponentDescriptor.h>
#else
#import <React/RCTTouchHandler.h>
#endif // RCT_NEW_ARCH_ENABLED
@implementation RNSFullWindowOverlayContainer
- (instancetype)initWithFrame:(CGRect)frame accessibilityViewIsModal:(BOOL)accessibilityViewIsModal
{
if (self = [super initWithFrame:frame]) {
self.accessibilityViewIsModal = accessibilityViewIsModal;
}
return self;
}
- (BOOL)pointInside:(CGPoint)point withEvent:(UIEvent *)event
{
for (UIView *view in [self subviews]) {
if (view.userInteractionEnabled && [view pointInside:[self convertPoint:point toView:view] withEvent:event]) {
return YES;
}
}
return NO;
}
- (UIView *)hitTest:(CGPoint)point withEvent:(UIEvent *)event
{
BOOL canReceiveTouchEvents = ([self isUserInteractionEnabled] && ![self isHidden]);
if (!canReceiveTouchEvents) {
return nil;
}
// `hitSubview` is the topmost subview which was hit. The hit point can
// be outside the bounds of `view` (e.g., if -clipsToBounds is NO).
UIView *hitSubview = nil;
BOOL isPointInside = [self pointInside:point withEvent:event];
if (![self clipsToBounds] || isPointInside) {
// Take z-index into account when calculating the touch target.
NSArray<UIView *> *sortedSubviews = [self reactZIndexSortedSubviews];
// The default behaviour of UIKit is that if a view does not contain a point,
// then no subviews will be returned from hit testing, even if they contain
// the hit point. By doing hit testing directly on the subviews, we bypass
// the strict containment policy (i.e., UIKit guarantees that every ancestor
// of the hit view will return YES from -pointInside:withEvent:). See:
// - https://developer.apple.com/library/ios/qa/qa2013/qa1812.html
for (UIView *subview in [sortedSubviews reverseObjectEnumerator]) {
CGPoint convertedPoint = [subview convertPoint:point fromView:self];
hitSubview = [subview hitTest:convertedPoint withEvent:event];
if (hitSubview != nil) {
break;
}
}
}
return hitSubview;
}
@end
@implementation RNSFullWindowOverlay {
__weak RCTBridge *_bridge;
RNSFullWindowOverlayContainer *_container;
CGRect _reactFrame;
#ifdef RCT_NEW_ARCH_ENABLED
RCTSurfaceTouchHandler *_touchHandler;
#else
RCTTouchHandler *_touchHandler;
#endif // RCT_NEW_ARCH_ENABLED
}
// Needed because of this: https://github.com/facebook/react-native/pull/37274
+ (void)load
{
[super load];
}
#ifdef RCT_NEW_ARCH_ENABLED
- (instancetype)init
{
if (self = [super init]) {
static const auto defaultProps = std::make_shared<const react::RNSFullWindowOverlayProps>();
_props = defaultProps;
[self initCommonProps];
}
return self;
}
#else
- (instancetype)initWithBridge:(RCTBridge *)bridge
{
if (self = [super init]) {
_bridge = bridge;
[self initCommonProps];
}
return self;
}
#endif // RCT_NEW_ARCH_ENABLED
- (void)initCommonProps
{
// Default value used by container.
_accessibilityContainerViewIsModal = YES;
_reactFrame = CGRectNull;
_container = self.container;
}
- (void)dealloc
{
[[NSNotificationCenter defaultCenter] removeObserver:self];
}
- (void)setAccessibilityContainerViewIsModal:(BOOL)accessibilityContainerViewIsModal
{
_accessibilityContainerViewIsModal = accessibilityContainerViewIsModal;
self.container.accessibilityViewIsModal = accessibilityContainerViewIsModal;
}
- (void)addSubview:(UIView *)view
{
[_container addSubview:view];
}
- (RNSFullWindowOverlayContainer *)container
{
if (_container == nil) {
_container = [[RNSFullWindowOverlayContainer alloc] initWithFrame:_reactFrame
accessibilityViewIsModal:_accessibilityContainerViewIsModal];
}
return _container;
}
- (void)didMoveToSuperview
{
if (self.superview == nil) {
if (_container != nil) {
[_container removeFromSuperview];
[_touchHandler detachFromView:_container];
}
} else {
if (_container != nil) {
UIAccessibilityPostNotification(UIAccessibilityLayoutChangedNotification, _container);
}
if (_touchHandler == nil) {
#ifdef RCT_NEW_ARCH_ENABLED
_touchHandler = [RCTSurfaceTouchHandler new];
#else
_touchHandler = [[RCTTouchHandler alloc] initWithBridge:_bridge];
#endif
}
[_touchHandler attachToView:_container];
// Called here in case the attempt to show the container failed
// in `didMoveToWindow`, as it happens e.g. in case where a `fullScreenModal`
// is in presntation. This assumes that this method is called after
// `didMoveToWindow`.
[self maybeShow];
}
}
- (void)didMoveToWindow
{
// Detaching FullWindowOverlay is handled by `didMoveToSuperview`
if (self.window != nil) {
[self maybeShow];
}
}
- (void)maybeShow
{
UIWindow *window = [self window];
if (window == nil) {
// This fallback might return wrong window in case of multi-window
// apps e.g. on iPad.
window = RCTKeyWindow();
}
if (![[window subviews] containsObject:_container]) {
[window addSubview:_container];
}
}
#ifdef RCT_NEW_ARCH_ENABLED
#pragma mark - Fabric Specific
+ (react::ComponentDescriptorProvider)componentDescriptorProvider
{
return react::concreteComponentDescriptorProvider<react::RNSFullWindowOverlayComponentDescriptor>();
}
- (void)prepareForRecycle
{
[_container removeFromSuperview];
// Due to view recycling we don't really want to set _container = nil
// as it won't be instantiated when the component appears for the second time.
// We could consider nulling in here & using container (lazy getter) everywhere else.
// _container = nil;
[super prepareForRecycle];
}
- (void)mountChildComponentView:(UIView<RCTComponentViewProtocol> *)childComponentView index:(NSInteger)index
{
[self addSubview:childComponentView];
}
- (void)unmountChildComponentView:(UIView<RCTComponentViewProtocol> *)childComponentView index:(NSInteger)index
{
[childComponentView removeFromSuperview];
}
// We do not set frame for ouselves, but rather for the container.
RNS_IGNORE_SUPER_CALL_BEGIN
- (void)updateLayoutMetrics:(react::LayoutMetrics const &)layoutMetrics
oldLayoutMetrics:(react::LayoutMetrics const &)oldLayoutMetrics
{
CGRect frame = RCTCGRectFromRect(layoutMetrics.frame);
// Due to view flattening on new architecture there are situations
// when we receive frames with origin different from (0, 0).
// We account for this frame manipulation in shadow node by setting
// RootNodeKind trait for the shadow node making state consistent
// between Host & Shadow Tree
frame.origin = CGPointZero;
_reactFrame = frame;
[_container setFrame:frame];
}
RNS_IGNORE_SUPER_CALL_END
- (void)updateProps:(const facebook::react::Props::Shared &)props
oldProps:(const facebook::react::Props::Shared &)oldProps
{
const auto &oldComponentProps = *std::static_pointer_cast<const react::RNSFullWindowOverlayProps>(_props);
const auto &newComponentProps = *std::static_pointer_cast<const react::RNSFullWindowOverlayProps>(props);
if (newComponentProps.accessibilityContainerViewIsModal != oldComponentProps.accessibilityContainerViewIsModal) {
[self setAccessibilityContainerViewIsModal:newComponentProps.accessibilityContainerViewIsModal];
}
[super updateProps:props oldProps:oldProps];
}
#else
#pragma mark - Paper specific
- (void)reactSetFrame:(CGRect)frame
{
_reactFrame = frame;
[_container setFrame:frame];
}
- (void)invalidate
{
[_container removeFromSuperview];
_container = nil;
}
#endif // RCT_NEW_ARCH_ENABLED
@end
#ifdef RCT_NEW_ARCH_ENABLED
Class<RCTComponentViewProtocol> RNSFullWindowOverlayCls(void)
{
return RNSFullWindowOverlay.class;
}
#endif // RCT_NEW_ARCH_ENABLED
@implementation RNSFullWindowOverlayManager
RCT_EXPORT_MODULE()
RCT_EXPORT_VIEW_PROPERTY(accessibilityContainerViewIsModal, BOOL)
#ifdef RCT_NEW_ARCH_ENABLED
#else
- (UIView *)view
{
return [[RNSFullWindowOverlay alloc] initWithBridge:self.bridge];
}
#endif // RCT_NEW_ARCH_ENABLED
@end

View File

@@ -0,0 +1,15 @@
#pragma once
#ifdef RCT_NEW_ARCH_ENABLED
#import <UIKit/UIKit.h>
#import "RNSViewControllerInvalidating.h"
@interface RNSInvalidatedComponentsRegistry : NSObject
- (void)pushForInvalidation:(UIView<RNSViewControllerInvalidating> *)view;
- (void)flushInvalidViews;
@end
#endif // RCT_NEW_ARCH_ENABLED

View File

@@ -0,0 +1,34 @@
#ifdef RCT_NEW_ARCH_ENABLED
#import "RNSInvalidatedComponentsRegistry.h"
@interface RNSInvalidatedComponentsRegistry ()
@property (nonatomic, strong) NSMutableSet<UIView<RNSViewControllerInvalidating> *> *invalidViews;
@end
@implementation RNSInvalidatedComponentsRegistry
- (instancetype)init
{
if (self = [super init]) {
_invalidViews = [NSMutableSet set];
}
return self;
}
- (void)pushForInvalidation:(UIView<RNSViewControllerInvalidating> *)view
{
[_invalidViews addObject:view];
}
- (void)flushInvalidViews
{
for (id<RNSViewControllerInvalidating> view in _invalidViews) {
[view invalidateController];
}
[_invalidViews removeAllObjects];
}
@end
#endif // RCT_NEW_ARCH_ENABLED

10
node_modules/react-native-screens/ios/RNSModalScreen.h generated vendored Normal file
View File

@@ -0,0 +1,10 @@
#pragma once
#import "RNSScreen.h"
@interface RNSModalScreen : RNSScreenView
@end
@interface RNSModalScreenManager : RNSScreenManager
@end

View File

@@ -0,0 +1,65 @@
#import "RNSModalScreen.h"
#ifdef RCT_NEW_ARCH_ENABLED
#import <React/RCTFabricComponentsPlugins.h>
#import <rnscreens/RNSModalScreenComponentDescriptor.h>
#endif
@implementation RNSModalScreen
// When using UIModalPresentationStyleFullScreen the whole view hierarchy mounted under primary `UITransitionView` is
// removed, including React's root view, which observes for trait collection changes & sends it to `Appearance` module
// via system notification centre. To workaround this detached-root-view-situation we emit the event to React's
// `Appearance` module ourselves. For the RCTRootView observer, visit
// https://github.com/facebook/react-native/blob/d3e0430deac573fd44792e6005d5de20e9ad2797/packages/react-native/React/Base/RCTRootView.m#L362
// For more information, see https://github.com/software-mansion/react-native-screens/pull/2211.
- (void)traitCollectionDidChange:(UITraitCollection *)previousTraitCollection
{
[super traitCollectionDidChange:previousTraitCollection];
if (RCTSharedApplication().applicationState == UIApplicationStateBackground ||
self.stackPresentation != RNSScreenStackPresentationFullScreenModal) {
return;
}
[[NSNotificationCenter defaultCenter]
postNotificationName:RCTUserInterfaceStyleDidChangeNotification
object:self
userInfo:@{
RCTUserInterfaceStyleDidChangeNotificationTraitCollectionKey : self.traitCollection,
}];
}
#ifdef RCT_NEW_ARCH_ENABLED
// Needed because of this: https://github.com/facebook/react-native/pull/37274
+ (void)load
{
[super load];
}
+ (react::ComponentDescriptorProvider)componentDescriptorProvider
{
return react::concreteComponentDescriptorProvider<react::RNSModalScreenComponentDescriptor>();
}
#endif
@end
#ifdef RCT_NEW_ARCH_ENABLED
Class<RCTComponentViewProtocol> RNSModalScreenCls(void)
{
return RNSModalScreen.class;
}
#endif
@implementation RNSModalScreenManager
RCT_EXPORT_MODULE()
#ifdef RCT_NEW_ARCH_ENABLED
#else
- (UIView *)view
{
return [[RNSModalScreen alloc] initWithBridge:self.bridge];
}
#endif // RCT_NEW_ARCH_ENABLED
@end

20
node_modules/react-native-screens/ios/RNSModule.h generated vendored Normal file
View File

@@ -0,0 +1,20 @@
#pragma once
#ifdef RCT_NEW_ARCH_ENABLED
#import <rnscreens/rnscreens.h>
#else
#import <React/RCTBridge.h>
#endif
NS_ASSUME_NONNULL_BEGIN
@interface RNSModule : NSObject
#ifdef RCT_NEW_ARCH_ENABLED
<NativeScreensModuleSpec>
#else
<RCTBridgeModule>
#endif
@end
NS_ASSUME_NONNULL_END

173
node_modules/react-native-screens/ios/RNSModule.mm generated vendored Normal file
View File

@@ -0,0 +1,173 @@
#import "RNSModule.h"
#import <React/RCTBridge+Private.h>
#import <React/RCTBridge.h>
#import <React/RCTUIManager.h>
#import <React/RCTUIManagerUtils.h>
#import <React/RCTUtils.h>
#include <jsi/jsi.h>
#import "RNSScreenStack.h"
#import "RNScreensTurboModule.h"
NS_ASSUME_NONNULL_BEGIN
@implementation RNSModule {
std::atomic<bool> isActiveTransition;
}
RCT_EXPORT_MODULE()
#ifdef RCT_NEW_ARCH_ENABLED
@synthesize viewRegistry_DEPRECATED = _viewRegistry_DEPRECATED;
#endif // RCT_NEW_ARCH_ENABLED
@synthesize bridge = _bridge;
- (dispatch_queue_t)methodQueue
{
// It seems that due to how UIBlocks work with uiManager, we need to call the methods there
// for the blocks to be dispatched before the batch is completed
return dispatch_get_main_queue();
}
- (NSDictionary *)constantsToExport
{
[self installHostObject];
return @{};
}
+ (BOOL)requiresMainQueueSetup
{
return NO;
}
- (RNSScreenStackView *)getScreenStackView:(NSNumber *)reactTag
{
RCTAssertMainQueue();
RNSScreenStackView *view;
#ifdef RCT_NEW_ARCH_ENABLED
view = (RNSScreenStackView *)[self.viewRegistry_DEPRECATED viewForReactTag:reactTag];
#else
view = (RNSScreenStackView *)[self.bridge.uiManager viewForReactTag:reactTag];
#endif // RCT_NEW_ARCH_ENABLED
return view;
}
- (NSArray<NSNumber *> *)startTransition:(NSNumber *)stackTag
{
RCTAssertMainQueue();
RNSScreenStackView *stackView = [self getStackView:stackTag];
if (stackView == nil || isActiveTransition) {
return @[ @(-1), @(-1) ];
}
NSArray<NSNumber *> *screenTags = @[ @(-1), @(-1) ];
auto screens = stackView.reactViewController.childViewControllers;
unsigned long screenCount = [screens count];
if (screenCount > 1) {
UIView *topScreen = screens[screenCount - 1].view;
UIView *belowTopScreen = screens[screenCount - 2].view;
belowTopScreen.transform = CGAffineTransformMake(1, 0, 0, 1, 0, 0);
isActiveTransition = true;
#ifdef RCT_NEW_ARCH_ENABLED
screenTags = @[ @(topScreen.tag), @(belowTopScreen.tag) ];
#else
screenTags = @[ topScreen.reactTag, belowTopScreen.reactTag ];
#endif // RCT_NEW_ARCH_ENABLED
[stackView startScreenTransition];
}
return screenTags;
}
- (bool)updateTransition:(NSNumber *)stackTag progress:(double)progress
{
RCTAssertMainQueue();
RNSScreenStackView *stackView = [self getStackView:stackTag];
if (stackView == nil || !isActiveTransition) {
return false;
}
[stackView updateScreenTransition:progress];
return true;
}
- (bool)finishTransition:(NSNumber *)stackTag canceled:(bool)canceled
{
RCTAssertMainQueue();
RNSScreenStackView *stackView = [self getStackView:stackTag];
if (stackView == nil || !isActiveTransition) {
return false;
}
[stackView finishScreenTransition:canceled];
if (!canceled) {
stackView.disableSwipeBack = NO;
}
isActiveTransition = false;
return true;
}
- (void)disableSwipeBackForTopScreen:(NSNumber *)stackTag
{
RCTAssertMainQueue();
RNSScreenStackView *stackView = [self getStackView:stackTag];
if (stackView == nil) {
return;
}
stackView.disableSwipeBack = YES;
}
- (RNSScreenStackView *)getStackView:(NSNumber *)stackTag
{
RNSScreenStackView *view = [self getScreenStackView:stackTag];
if (view != nil && ![view isKindOfClass:[RNSScreenStackView class]]) {
RCTLogError(@"[RNScreens] Invalid view type, expecting RNSScreenStackView, got: %@", view);
return nil;
}
return view;
}
#ifdef RCT_NEW_ARCH_ENABLED
- (std::shared_ptr<facebook::react::TurboModule>)getTurboModule:
(const facebook::react::ObjCTurboModule::InitParams &)params
{
[self installHostObject];
return std::make_shared<facebook::react::NativeScreensModuleSpecJSI>(params);
}
#endif
- (void)installHostObject
{
/*
installHostObject method is called from constantsToExport and getTurboModule,
because depending on the selected architecture, only one method will be called.
For `Paper`, it will be constantsToExport, and for `Fabric`, it will be getTurboModule.
*/
RCTCxxBridge *cxxBridge = (RCTCxxBridge *)self.bridge;
if (cxxBridge != nil) {
auto jsiRuntime = (jsi::Runtime *)cxxBridge.runtime;
if (jsiRuntime != nil) {
auto &runtime = *jsiRuntime;
__weak auto weakSelf = self;
const auto &startTransition = [weakSelf](int stackTag) -> std::array<int, 2> {
auto screensTags = [weakSelf startTransition:@(stackTag)];
return {[screensTags[0] intValue], [screensTags[1] intValue]};
};
const auto &updateTransition = [weakSelf](int stackTag, double progress) {
[weakSelf updateTransition:@(stackTag) progress:progress];
};
const auto &finishTransition = [weakSelf](int stackTag, bool canceled) {
[weakSelf finishTransition:@(stackTag) canceled:canceled];
};
const auto &disableSwipeBackForTopScreen = [weakSelf](int stackTag) {
[weakSelf disableSwipeBackForTopScreen:@(stackTag)];
};
auto rnScreensModule = std::make_shared<RNScreens::RNScreensTurboModule>(
startTransition, updateTransition, finishTransition, disableSwipeBackForTopScreen);
auto rnScreensModuleHostObject = jsi::Object::createFromHostObject(runtime, rnScreensModule);
runtime.global().setProperty(
runtime, RNScreens::RNScreensTurboModule::MODULE_NAME, std::move(rnScreensModuleHostObject));
}
}
}
@end
NS_ASSUME_NONNULL_END

View File

@@ -0,0 +1,15 @@
#pragma once
#import "RNSEnums.h"
NS_ASSUME_NONNULL_BEGIN
// TODO: investigate objc - swift interop and deduplicate this code
// This class needs to be compatible with the RNSOrientationProvidingSwift.
@protocol RNSOrientationProviding
- (RNSOrientation)evaluateOrientation;
@end
NS_ASSUME_NONNULL_END

View File

@@ -0,0 +1,14 @@
#pragma once
#import <UIKit/UIKit.h>
#import "RNSScreenStackAnimator.h"
NS_ASSUME_NONNULL_BEGIN
@interface RNSPercentDrivenInteractiveTransition : UIPercentDrivenInteractiveTransition
@property (nonatomic, nullable) RNSScreenStackAnimator *animationController;
@end
NS_ASSUME_NONNULL_END

View File

@@ -0,0 +1,69 @@
#import "RNSPercentDrivenInteractiveTransition.h"
@implementation RNSPercentDrivenInteractiveTransition {
RNSScreenStackAnimator *_animationController;
}
#pragma mark - UIViewControllerInteractiveTransitioning
- (void)startInteractiveTransition:(id<UIViewControllerContextTransitioning>)transitionContext
{
[super startInteractiveTransition:transitionContext];
}
#pragma mark - UIPercentDrivenInteractiveTransition
// `updateInteractiveTransition`, `finishInteractiveTransition`,
// `cancelInteractiveTransition` are forwared by superclass to
// corresponding methods in transition context. In case
// of "classical CA driven animations", such as UIView animation blocks
// or direct utilization of CoreAnimation API, context drives the animation,
// however in case of UIViewPropertyAnimator it does not. We need
// to drive animation manually and this is exactly what happens below.
- (void)updateInteractiveTransition:(CGFloat)percentComplete
{
if (_animationController != nil) {
[_animationController.inFlightAnimator setFractionComplete:percentComplete];
}
[super updateInteractiveTransition:percentComplete];
}
- (void)finishInteractiveTransition
{
[self finalizeInteractiveTransitionWithAnimationWasCancelled:NO];
[super finishInteractiveTransition];
}
- (void)cancelInteractiveTransition
{
[self finalizeInteractiveTransitionWithAnimationWasCancelled:YES];
[super cancelInteractiveTransition];
}
#pragma mark - Helpers
- (void)finalizeInteractiveTransitionWithAnimationWasCancelled:(BOOL)cancelled
{
if (_animationController == nil) {
return;
}
UIViewPropertyAnimator *_Nullable animator = _animationController.inFlightAnimator;
if (animator == nil) {
return;
}
BOOL shouldReverseAnimation = cancelled;
id<UITimingCurveProvider> timingParams = [_animationController timingParamsForAnimationCompletion];
[animator pauseAnimation];
[animator setReversed:shouldReverseAnimation];
[animator continueAnimationWithTimingParameters:timingParams durationFactor:(1 - animator.fractionComplete)];
// System retains it & we don't need it anymore.
_animationController = nil;
}
@end

227
node_modules/react-native-screens/ios/RNSScreen.h generated vendored Normal file
View File

@@ -0,0 +1,227 @@
#pragma once
#import <React/RCTComponent.h>
#import <React/RCTViewManager.h>
#import "RNSEnums.h"
#import "RNSSafeAreaProviding.h"
#import "RNSScreenContainer.h"
#import "RNSScreenContentWrapper.h"
#import "RNSScrollEdgeEffectApplicator.h"
#import "RNSScrollViewBehaviorOverriding.h"
#import "RNSViewInteractionManager.h"
#if !TARGET_OS_TV
#import "RNSOrientationProviding.h"
#endif // !TARGET_OS_TV
#if RCT_NEW_ARCH_ENABLED
#import <React/RCTViewComponentView.h>
#else
#import <React/RCTView.h>
#endif // RCT_NEW_ARCH_ENABLED
NS_ASSUME_NONNULL_BEGIN
#ifdef RCT_NEW_ARCH_ENABLED
namespace react = facebook::react;
#endif // RCT_NEW_ARCH_ENABLED
@interface RCTConvert (RNSScreen)
+ (RNSScreenStackPresentation)RNSScreenStackPresentation:(id)json;
+ (RNSScreenStackAnimation)RNSScreenStackAnimation:(id)json;
#if !TARGET_OS_TV
+ (RNSStatusBarStyle)RNSStatusBarStyle:(id)json;
+ (UIStatusBarAnimation)UIStatusBarAnimation:(id)json;
+ (UIInterfaceOrientationMask)UIInterfaceOrientationMask:(id)json;
#endif
@end
@class RNSScreenView;
@interface RNSScreen : UIViewController <
RNSViewControllerDelegate
#if !TARGET_OS_TV
,
RNSOrientationProviding
#endif // !TARGET_OS_TV
>
- (instancetype)initWithView:(UIView *)view;
- (UIViewController *)findChildVCForConfigAndTrait:(RNSWindowTrait)trait includingModals:(BOOL)includingModals;
- (BOOL)hasNestedStack;
- (void)calculateAndNotifyHeaderHeightChangeIsModal:(BOOL)isModal;
- (void)notifyFinishTransitioning;
- (RNSScreenView *)screenView;
#ifdef RCT_NEW_ARCH_ENABLED
- (void)setViewToSnapshot;
- (CGFloat)calculateHeaderHeightIsModal:(BOOL)isModal;
#endif
- (BOOL)isRemovedFromParent;
@end
@class RNSScreenStackHeaderConfig;
@interface RNSScreenView :
#ifdef RCT_NEW_ARCH_ENABLED
RCTViewComponentView
#else
RCTView
#endif
<RNSScreenContentWrapperDelegate,
RNSScrollViewBehaviorOverriding,
RNSSafeAreaProviding,
RNSScrollEdgeEffectProviding>
/**
* This is value of the prop as passed by the user. To get effective value see derived property
* `isFullScreenSwipeEffectivelyEnabled`
*/
@property (nonatomic) RNSOptionalBoolean fullScreenSwipeEnabled;
@property (nonatomic, readonly, getter=isFullScreenSwipeEffectivelyEnabled) BOOL fullScreenSwipeEffectivelyEnabled;
@property (nonatomic) BOOL fullScreenSwipeShadowEnabled;
@property (nonatomic) BOOL gestureEnabled;
@property (nonatomic) BOOL hasStatusBarHiddenSet;
@property (nonatomic) BOOL hasStatusBarStyleSet;
@property (nonatomic) BOOL hasStatusBarAnimationSet;
@property (nonatomic) BOOL hasHomeIndicatorHiddenSet;
@property (nonatomic) BOOL hasOrientationSet;
@property (nonatomic) RNSScreenStackAnimation stackAnimation;
@property (nonatomic) RNSScreenStackPresentation stackPresentation;
@property (nonatomic) RNSScreenSwipeDirection swipeDirection;
@property (nonatomic) RNSScreenReplaceAnimation replaceAnimation;
@property (nonatomic) RNSScrollEdgeEffect bottomScrollEdgeEffect;
@property (nonatomic) RNSScrollEdgeEffect leftScrollEdgeEffect;
@property (nonatomic) RNSScrollEdgeEffect rightScrollEdgeEffect;
@property (nonatomic) RNSScrollEdgeEffect topScrollEdgeEffect;
@property (nonatomic, readwrite) BOOL synchronousShadowStateUpdatesEnabled;
@property (nonatomic, retain) NSNumber *transitionDuration;
@property (nonatomic, readonly) BOOL dismissed;
@property (nonatomic) BOOL hideKeyboardOnSwipe;
@property (nonatomic) BOOL customAnimationOnSwipe;
@property (nonatomic) BOOL preventNativeDismiss;
@property (nonatomic, retain) RNSScreen *controller;
@property (nonatomic, copy) NSDictionary *gestureResponseDistance;
@property (nonatomic) int activityState;
@property (nonatomic, nullable) NSString *screenId;
@property (weak, nonatomic) UIView<RNSScreenContainerDelegate> *reactSuperview;
#if !TARGET_OS_TV
@property (nonatomic) RNSStatusBarStyle statusBarStyle;
@property (nonatomic) UIStatusBarAnimation statusBarAnimation;
@property (nonatomic) UIInterfaceOrientationMask screenOrientation;
@property (nonatomic) BOOL statusBarHidden;
@property (nonatomic) BOOL homeIndicatorHidden;
// Props controlling UISheetPresentationController
@property (nonatomic) NSArray<NSNumber *> *sheetAllowedDetents;
@property (nonatomic) NSNumber *sheetLargestUndimmedDetent;
@property (nonatomic) BOOL sheetGrabberVisible;
@property (nonatomic) CGFloat sheetCornerRadius;
@property (nonatomic) NSInteger sheetInitialDetent;
@property (nonatomic) BOOL sheetExpandsWhenScrolledToEdge;
#endif // !TARGET_OS_TV
#ifdef RCT_NEW_ARCH_ENABLED
// we recreate the behavior of `reactSetFrame` on new architecture
@property (nonatomic) react::LayoutMetrics oldLayoutMetrics;
@property (nonatomic) react::LayoutMetrics newLayoutMetrics;
@property (weak, nonatomic) RNSScreenStackHeaderConfig *config;
@property (nonatomic, readonly) BOOL hasHeaderConfig;
@property (nonatomic, readonly, getter=isMarkedForUnmountInCurrentTransaction)
BOOL markedForUnmountInCurrentTransaction;
/**
* Whether the snapshot for the transition made for JS-popped views should be taken after view updates or not.
* *This property was introduced for the sake of integration with reanimated.*
*/
@property (nonatomic) BOOL snapshotAfterUpdates;
#else
@property (nonatomic, copy) RCTDirectEventBlock onAppear;
@property (nonatomic, copy) RCTDirectEventBlock onDisappear;
@property (nonatomic, copy) RCTDirectEventBlock onDismissed;
@property (nonatomic, copy) RCTDirectEventBlock onHeaderHeightChange;
@property (nonatomic, copy) RCTDirectEventBlock onWillAppear;
@property (nonatomic, copy) RCTDirectEventBlock onWillDisappear;
@property (nonatomic, copy) RCTDirectEventBlock onNativeDismissCancelled;
@property (nonatomic, copy) RCTDirectEventBlock onTransitionProgress;
@property (nonatomic, copy) RCTDirectEventBlock onGestureCancel;
@property (nonatomic, copy) RCTDirectEventBlock onSheetDetentChanged;
#endif // RCT_NEW_ARCH_ENABLED
- (void)notifyFinishTransitioning;
- (void)notifyHeaderHeightChange:(double)height;
#ifdef RCT_NEW_ARCH_ENABLED
- (void)notifyWillAppear;
- (void)notifyWillDisappear;
- (void)notifyAppear;
- (void)notifyDisappear;
- (void)updateBounds;
- (void)notifyDismissedWithCount:(int)dismissCount;
- (instancetype)initWithFrame:(CGRect)frame;
/**
* Tell `Screen` that it will be unmounted in next transaction.
* The component needs this so that we can later decide whether to
* replace it with snapshot or not.
*/
- (void)willBeUnmountedInUpcomingTransaction;
#else
- (instancetype)initWithBridge:(RCTBridge *)bridge;
#endif // RCT_NEW_ARCH_ENABLED
- (void)notifyTransitionProgress:(double)progress closing:(BOOL)closing goingForward:(BOOL)goingForward;
- (void)notifyDismissCancelledWithDismissCount:(int)dismissCount;
- (BOOL)isModal;
- (BOOL)isPresentedAsNativeModal;
/**
* Holds a shared instance to a service that finds the view that needs to have interactions disabled for stack to not
* have multiple screen transitions at once.
*/
+ (RNSViewInteractionManager *)viewInteractionManagerInstance;
/**
* Tell `Screen` component that it has been removed from react state and can safely cleanup
* any retained resources.
*/
- (void)invalidateImpl;
#ifndef RCT_NEW_ARCH_ENABLED
/**
* Tell `Screen` component that it has been removed from react state and can safely cleanup
* any retained resources.
*
* On old architecture this method might be called by RN via `RCTInvalidating` protocol.
*/
- (void)invalidate;
#endif // !RCT_NEW_ARCH_ENABLED
/**
* Looks for header configuration in instance's `reactSubviews` and returns it. If not present returns `nil`.
*/
- (RNSScreenStackHeaderConfig *_Nullable)findHeaderConfig;
/**
* Returns `YES` if the wrapper has been registered and it should not attempt to register on screen views higher in the
* tree.
*/
- (BOOL)registerContentWrapper:(nonnull RNSScreenContentWrapper *)contentWrapper contentHeightErrata:(float)errata;
@end
@interface UIView (RNSScreen)
- (UIViewController *)parentViewController;
@end
@interface RNSScreenManager : RCTViewManager
@end
NS_ASSUME_NONNULL_END

2438
node_modules/react-native-screens/ios/RNSScreen.mm generated vendored Normal file

File diff suppressed because it is too large Load Diff

View File

@@ -0,0 +1,47 @@
#pragma once
#ifdef RCT_NEW_ARCH_ENABLED
#import <React/RCTViewComponentView.h>
#else
#endif
#import <React/RCTViewManager.h>
NS_ASSUME_NONNULL_BEGIN
@protocol RNSScreenContainerDelegate
- (void)markChildUpdated;
- (void)updateContainer;
@end
@protocol RNSViewControllerDelegate
@end
@interface RNSViewController : UIViewController <RNSViewControllerDelegate>
- (UIViewController *)findActiveChildVC;
@end
@interface RNSScreenContainerManager : RCTViewManager
@end
@interface RNSScreenContainerView :
#ifdef RCT_NEW_ARCH_ENABLED
RCTViewComponentView <RNSScreenContainerDelegate>
#else
UIView <RNSScreenContainerDelegate, RCTInvalidating>
#endif
@property (nonatomic, retain) UIViewController *controller;
@property (nonatomic, retain) NSMutableArray *reactSubviews;
- (void)maybeDismissVC;
@end
NS_ASSUME_NONNULL_END

View File

@@ -0,0 +1,360 @@
#import "RNSScreenContainer.h"
#import "RNSDefines.h"
#import "RNSScreen.h"
#ifdef RCT_NEW_ARCH_ENABLED
#import <React/RCTConversions.h>
#import <React/RCTFabricComponentsPlugins.h>
#import <react/renderer/components/rnscreens/ComponentDescriptors.h>
#import <react/renderer/components/rnscreens/Props.h>
namespace react = facebook::react;
#endif // RCT_NEW_ARCH_ENABLED
#pragma mark - RNSViewController
@implementation RNSViewController
#if !TARGET_OS_TV
- (UIViewController *)childViewControllerForStatusBarStyle
{
return [self findActiveChildVC];
}
- (UIStatusBarAnimation)preferredStatusBarUpdateAnimation
{
return [self findActiveChildVC].preferredStatusBarUpdateAnimation;
}
- (UIViewController *)childViewControllerForStatusBarHidden
{
return [self findActiveChildVC];
}
- (UIInterfaceOrientationMask)supportedInterfaceOrientations
{
return [self findActiveChildVC].supportedInterfaceOrientations;
}
- (UIViewController *)childViewControllerForHomeIndicatorAutoHidden
{
return [self findActiveChildVC];
}
#endif
- (UIViewController *)findActiveChildVC
{
for (UIViewController *childVC in self.childViewControllers) {
if ([childVC isKindOfClass:[RNSScreen class]] &&
((RNSScreen *)childVC).screenView.activityState == RNSActivityStateOnTop) {
return childVC;
}
}
return [[self childViewControllers] lastObject];
}
@end
#pragma mark - RNSScreenContainerView
@implementation RNSScreenContainerView {
BOOL _invalidated;
NSMutableSet *_activeScreens;
}
- (instancetype)init
{
if (self = [super initWithFrame:CGRectZero]) {
#ifdef RCT_NEW_ARCH_ENABLED
static const auto defaultProps = std::make_shared<const react::RNSScreenContainerProps>();
_props = defaultProps;
#endif
_activeScreens = [NSMutableSet new];
_reactSubviews = [NSMutableArray new];
[self setupController];
_invalidated = NO;
}
return self;
}
- (void)setupController
{
_controller = [[RNSViewController alloc] init];
[self addSubview:_controller.view];
}
- (void)markChildUpdated
{
// We want the attaching/detaching of children to be always made on main queue, which
// is currently true for `react-navigation` since this method is triggered
// by the changes of `Animated` value in stack's transition or adding/removing screens
// in all navigators
RCTAssertMainQueue();
[self updateContainer];
}
// We do not call super as we do not want to update UIKit model. It will
// be updated after we receive all mutations.
RNS_IGNORE_SUPER_CALL_BEGIN
- (void)insertReactSubview:(RNSScreenView *)subview atIndex:(NSInteger)atIndex
{
subview.reactSuperview = self;
[_reactSubviews insertObject:subview atIndex:atIndex];
[subview reactSetFrame:CGRectMake(0, 0, self.bounds.size.width, self.bounds.size.height)];
}
- (void)removeReactSubview:(RNSScreenView *)subview
{
subview.reactSuperview = nil;
[_reactSubviews removeObject:subview];
}
- (NSArray<UIView *> *)reactSubviews
{
return _reactSubviews;
}
RNS_IGNORE_SUPER_CALL_END
- (UIViewController *)reactViewController
{
return _controller;
}
- (UIViewController *)findChildControllerForScreen:(RNSScreenView *)screen
{
for (UIViewController *vc in _controller.childViewControllers) {
if (vc.view == screen) {
return vc;
}
}
return nil;
}
- (void)prepareDetach:(RNSScreenView *)screen
{
[[self findChildControllerForScreen:screen] willMoveToParentViewController:nil];
}
- (void)detachScreen:(RNSScreenView *)screen
{
// We use findChildControllerForScreen method instead of directly accesing
// screen.controller because screen.controller may be reset to nil when the
// original screen view gets detached from the view hierarchy (we reset controller
// reference to avoid reference loops)
UIViewController *detachController = [self findChildControllerForScreen:screen];
[detachController willMoveToParentViewController:nil];
[screen removeFromSuperview];
[detachController removeFromParentViewController];
[_activeScreens removeObject:screen];
}
- (void)attachScreen:(RNSScreenView *)screen atIndex:(NSInteger)index
{
[_controller addChildViewController:screen.controller];
// the frame is already set at this moment because we adjust it in insertReactSubview. We don't
// want to update it here as react-driven animations may already run and hence changing the frame
// would result in visual glitches
[_controller.view insertSubview:screen.controller.view atIndex:index];
[screen.controller didMoveToParentViewController:_controller];
[_activeScreens addObject:screen];
}
- (void)updateContainer
{
BOOL screenRemoved = NO;
// remove screens that are no longer active
NSMutableSet *orphaned = [NSMutableSet setWithSet:_activeScreens];
for (RNSScreenView *screen in _reactSubviews) {
if (screen.activityState == RNSActivityStateInactive && [_activeScreens containsObject:screen]) {
screenRemoved = YES;
[self detachScreen:screen];
}
[orphaned removeObject:screen];
}
for (RNSScreenView *screen in orphaned) {
screenRemoved = YES;
[self detachScreen:screen];
}
// detect if new screen is going to be activated
BOOL screenAdded = NO;
for (RNSScreenView *screen in _reactSubviews) {
if (screen.activityState != RNSActivityStateInactive && ![_activeScreens containsObject:screen]) {
screenAdded = YES;
}
}
if (screenAdded) {
// add new screens in order they are placed in subviews array
NSInteger index = 0;
for (RNSScreenView *screen in _reactSubviews) {
if (screen.activityState != RNSActivityStateInactive) {
if ([_activeScreens containsObject:screen] && screen.activityState == RNSActivityStateTransitioningOrBelowTop) {
// for screens that were already active we want to mimick the effect UINavigationController
// has when willMoveToWindow:nil is triggered before the animation starts
[self prepareDetach:screen];
} else if (![_activeScreens containsObject:screen]) {
[self attachScreen:screen atIndex:index];
}
index += 1;
}
}
}
for (RNSScreenView *screen in _reactSubviews) {
if (screen.activityState == RNSActivityStateOnTop) {
[screen notifyFinishTransitioning];
}
}
if (screenRemoved || screenAdded) {
[self maybeDismissVC];
}
}
- (void)maybeDismissVC
{
if (_controller.presentedViewController == nil && _controller.presentingViewController == nil) {
// if user has reachability enabled (one hand use) and the window is slided down the below
// method will force it to slide back up as it is expected to happen with UINavController when
// we push or pop views.
// We only do that if `presentedViewController` is nil, as otherwise it'd mean that modal has
// been presented on top of recently changed controller in which case the below method would
// dismiss such a modal (e.g., permission modal or alert)
[_controller dismissViewControllerAnimated:NO completion:nil];
}
}
- (void)didUpdateReactSubviews
{
[self markChildUpdated];
}
- (void)didMoveToWindow
{
if (self.window && !_invalidated) {
// We check whether the view has been invalidated before running side-effects in didMoveToWindow
// This is needed because when LayoutAnimations are used it is possible for view to be re-attached
// to a window despite the fact it has been removed from the React Native view hierarchy.
[self reactAddControllerToClosestParent:_controller];
}
}
- (void)layoutSubviews
{
[super layoutSubviews];
_controller.view.frame = self.bounds;
for (RNSScreenView *subview in _reactSubviews) {
#ifdef RCT_NEW_ARCH_ENABLED
react::LayoutMetrics screenLayoutMetrics = subview.newLayoutMetrics;
screenLayoutMetrics.frame = RCTRectFromCGRect(CGRectMake(0, 0, self.bounds.size.width, self.bounds.size.height));
[subview updateLayoutMetrics:screenLayoutMetrics oldLayoutMetrics:subview.oldLayoutMetrics];
#else
[subview reactSetFrame:CGRectMake(0, 0, self.bounds.size.width, self.bounds.size.height)];
#endif
[subview setNeedsLayout];
}
}
#pragma mark-- Fabric specific
#ifdef RCT_NEW_ARCH_ENABLED
// Needed because of this: https://github.com/facebook/react-native/pull/37274
+ (void)load
{
[super load];
}
#pragma mark - RCTComponentViewProtocol
- (void)mountChildComponentView:(UIView<RCTComponentViewProtocol> *)childComponentView index:(NSInteger)index
{
if (![childComponentView isKindOfClass:[RNSScreenView class]]) {
RCTLogError(@"ScreenContainer only accepts children of type Screen");
return;
}
RNSScreenView *screenView = (RNSScreenView *)childComponentView;
RCTAssert(
childComponentView.reactSuperview == nil,
@"Attempt to mount already mounted component view. (parent: %@, child: %@, index: %@, existing parent: %@)",
self,
childComponentView,
@(index),
@([childComponentView.superview tag]));
[_reactSubviews insertObject:screenView atIndex:index];
screenView.reactSuperview = self;
react::LayoutMetrics screenLayoutMetrics = screenView.newLayoutMetrics;
screenLayoutMetrics.frame = RCTRectFromCGRect(CGRectMake(0, 0, self.bounds.size.width, self.bounds.size.height));
[screenView updateLayoutMetrics:screenLayoutMetrics oldLayoutMetrics:screenView.oldLayoutMetrics];
[self markChildUpdated];
}
- (void)unmountChildComponentView:(UIView<RCTComponentViewProtocol> *)childComponentView index:(NSInteger)index
{
RCTAssert(
childComponentView.reactSuperview == self,
@"Attempt to unmount a view which is mounted inside different view. (parent: %@, child: %@, index: %@)",
self,
childComponentView,
@(index));
RCTAssert(
(_reactSubviews.count > index) && [_reactSubviews objectAtIndex:index] == childComponentView,
@"Attempt to unmount a view which has a different index. (parent: %@, child: %@, index: %@, actual index: %@, tag at index: %@)",
self,
childComponentView,
@(index),
@([_reactSubviews indexOfObject:childComponentView]),
@([[_reactSubviews objectAtIndex:index] tag]));
((RNSScreenView *)childComponentView).reactSuperview = nil;
[_reactSubviews removeObject:childComponentView];
[childComponentView removeFromSuperview];
[self markChildUpdated];
}
+ (react::ComponentDescriptorProvider)componentDescriptorProvider
{
return react::concreteComponentDescriptorProvider<react::RNSScreenContainerComponentDescriptor>();
}
- (void)prepareForRecycle
{
[super prepareForRecycle];
[_controller willMoveToParentViewController:nil];
[_controller removeFromParentViewController];
}
#pragma mark-- Paper specific
#else
- (void)invalidate
{
_invalidated = YES;
[_controller willMoveToParentViewController:nil];
[_controller removeFromParentViewController];
}
#endif
@end
#ifdef RCT_NEW_ARCH_ENABLED
Class<RCTComponentViewProtocol> RNSScreenContainerCls(void)
{
return RNSScreenContainerView.class;
}
#endif
@implementation RNSScreenContainerManager
RCT_EXPORT_MODULE()
- (UIView *)view
{
return [[RNSScreenContainerView alloc] init];
}
@end

View File

@@ -0,0 +1,57 @@
#pragma once
#import <React/RCTViewManager.h>
#import <UIKit/UIKit.h>
#import "RNSDefines.h"
#if RCT_NEW_ARCH_ENABLED
#import <React/RCTFabricComponentsPlugins.h>
#import <React/RCTViewComponentView.h>
#else
#import <React/RCTView.h>
#endif // RCT_NEW_ARCH_ENABLED
NS_ASSUME_NONNULL_BEGIN
@class RNSScreenContentWrapper;
@class RNS_REACT_SCROLL_VIEW_COMPONENT;
@protocol RNSScreenContentWrapperDelegate <NSObject>
/**
* Called by the content wrapper on a delegate when React Native updates the layout.
*/
- (void)contentWrapper:(RNSScreenContentWrapper *)contentWrapper receivedReactFrame:(CGRect)reactFrame;
@end
typedef struct {
RNS_REACT_SCROLL_VIEW_COMPONENT *scrollViewComponent;
UIView *contentContainerView;
} RNSScrollViewSearchResult;
@interface RNSScreenContentWrapper :
#ifdef RCT_NEW_ARCH_ENABLED
RCTViewComponentView
#else
RCTView
#endif
@property (nonatomic, nullable, weak) id<RNSScreenContentWrapperDelegate> delegate;
/**
* Call this method to notify delegate with most recent frame set by React.
*/
- (void)triggerDelegateUpdate;
- (RNSScrollViewSearchResult)childRCTScrollViewComponentAndContentContainer;
- (BOOL)coerceChildScrollViewComponentSizeToSize:(CGSize)size;
@end
@interface RNSScreenContentWrapperManager : RCTViewManager
@end
NS_ASSUME_NONNULL_END

View File

@@ -0,0 +1,229 @@
#import "RNSScreenContentWrapper.h"
#import "RNSDefines.h"
#import "RNSSafeAreaViewComponentView.h"
#import "RNSScreen.h"
#import "RNSScreenStack.h"
#ifdef RCT_NEW_ARCH_ENABLED
#import <React/RCTConversions.h>
#import <React/RCTLog.h>
#import <React/RCTScrollViewComponentView.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>
namespace react = facebook::react;
#else
#import <React/RCTScrollView.h>
#endif // RCT_NEW_ARCH_ENABLED
@implementation RNSScreenContentWrapper
#ifndef RCT_NEW_ARCH_ENABLED
- (void)reactSetFrame:(CGRect)frame
{
[super reactSetFrame:frame];
if (self.delegate != nil) {
[self.delegate contentWrapper:self receivedReactFrame:frame];
}
}
#endif // !RCT_NEW_ARCH_ENABLED
- (void)notifyDelegateWithFrame:(CGRect)frame
{
[self.delegate contentWrapper:self receivedReactFrame:frame];
}
- (void)triggerDelegateUpdate
{
[self notifyDelegateWithFrame:self.frame];
}
- (void)willMoveToWindow:(UIWindow *)newWindow
{
if (newWindow == nil) {
return;
}
[self attachToAncestorScreenView];
}
/**
* Searches for first `RNSScreen` instance that uses `formSheet` presentation and returns it together with accumulated
* heights of navigation bars discovered along tree path up.
*
* TODO: Such travelsal method could be defined as its own algorithm in separate helper methods set.
*/
- (void)attachToAncestorScreenViewStartingFrom:(nonnull RNSScreen *)screenCtrl
{
UIViewController *controller = screenCtrl;
float headerHeightErrata = 0.f;
do {
if ([controller isKindOfClass:RNSScreen.class]) {
RNSScreen *currentScreen = static_cast<RNSScreen *>(controller);
if ([currentScreen.screenView registerContentWrapper:self contentHeightErrata:headerHeightErrata]) {
break;
}
} else if ([controller isKindOfClass:RNSNavigationController.class]) {
UINavigationBar *navigationBar = static_cast<RNSNavigationController *>(controller).navigationBar;
headerHeightErrata += navigationBar.frame.size.height * !navigationBar.isHidden;
}
controller = controller.parentViewController;
} while (controller != nil);
}
- (void)attachToAncestorScreenView
{
RNSScreen *_Nullable screen =
static_cast<RNSScreen *_Nullable>([[self findFirstScreenViewAncestor] reactViewController]);
if (screen == nil) {
// On old architecture, especially when executing `replace` action it can happen that **replaced** (old one) screen
// receives willMoveToWindow: with not nil argument. On new architecture it seems to work as expected.
#ifdef RCT_NEW_ARCH_ENABLED
RCTLogWarn(@"Failed to find parent screen controller from %@.", self);
#endif
return;
}
[self attachToAncestorScreenViewStartingFrom:screen];
}
- (nullable RNSScreenView *)findFirstScreenViewAncestor
{
UIView *currentView = self;
// In standard scenario this should do only a single iteration.
// Haven't got repro, but we got reports that there are scenarios
// when there are intermediate views between screen view & the content wrapper.
// https://github.com/software-mansion/react-native-screens/pull/2683
do {
currentView = currentView.reactSuperview;
} while (currentView != nil && ![currentView isKindOfClass:RNSScreenView.class]);
return static_cast<RNSScreenView *_Nullable>(currentView);
}
- (RNSScrollViewSearchResult)childRCTScrollViewComponentAndContentContainer
{
// Directly search subviews
for (UIView *subview in self.subviews) {
if ([subview isKindOfClass:RNS_REACT_SCROLL_VIEW_COMPONENT.class]) {
return (RNSScrollViewSearchResult){.scrollViewComponent = static_cast<RNS_REACT_SCROLL_VIEW_COMPONENT *>(subview),
.contentContainerView = self};
}
}
// Fallback 1: Search through RNSSafeAreaViewComponentView subviews (iOS 26+ workaround with modified hierarchy)
#if RNS_IPHONE_OS_VERSION_AVAILABLE(26_0)
if (@available(iOS 26.0, *)) {
UIView *maybeSafeAreaView = self.subviews.firstObject;
if ([maybeSafeAreaView isKindOfClass:RNSSafeAreaViewComponentView.class]) {
for (UIView *subview in maybeSafeAreaView.subviews) {
if ([subview isKindOfClass:RNS_REACT_SCROLL_VIEW_COMPONENT.class]) {
return (RNSScrollViewSearchResult){
.scrollViewComponent = static_cast<RNS_REACT_SCROLL_VIEW_COMPONENT *>(subview),
.contentContainerView = maybeSafeAreaView};
}
}
}
}
#endif // RNS_IPHONE_OS_VERSION_AVAILABLE(26_0)
return (RNSScrollViewSearchResult){.scrollViewComponent = nullptr, .contentContainerView = nullptr};
}
- (BOOL)coerceChildScrollViewComponentSizeToSize:(CGSize)size
{
auto scrollViewComponentAndContentContainerPair = [self childRCTScrollViewComponentAndContentContainer];
RNS_REACT_SCROLL_VIEW_COMPONENT *_Nullable scrollViewComponent =
scrollViewComponentAndContentContainerPair.scrollViewComponent;
UIView *_Nullable containerView = scrollViewComponentAndContentContainerPair.contentContainerView;
if (scrollViewComponent == nil) {
return NO;
}
if (containerView.subviews.count > 2) {
RCTLogWarn(
@"[RNScreens] FormSheet with ScrollView expects at most 2 subviews. Got %ld for container: %@. This might result in incorrect layout. \
If you want to display header alongside the scrollView, make sure to apply `collapsable: false` on your header component view.",
containerView.subviews.count,
NSStringFromClass(containerView.class));
}
NSUInteger scrollViewComponentIndex = [containerView.subviews indexOfObject:scrollViewComponent];
// Case 1: ScrollView first child - takes whole size.
if (scrollViewComponentIndex == 0) {
CGRect newFrame = scrollViewComponent.frame;
newFrame.size = size;
scrollViewComponent.frame = newFrame;
return YES;
}
// Case 2: There is a header - we adjust scrollview size by the header height.
if (scrollViewComponentIndex == 1) {
UIView *headerView = containerView.subviews[0];
CGRect newFrame = scrollViewComponent.frame;
newFrame.size = size;
newFrame.size.height -= headerView.frame.size.height;
#ifdef RCT_NEW_ARCH_ENABLED
// For some unknown yet reason on new architecture the scroll view
// is placed at (0, 0), which makes no sense given there
// is another child at lower index in flex layout.
// TODO: Research why this happens and solve this properly.
if (newFrame.origin.y == 0) {
newFrame.origin.y = headerView.frame.size.height;
}
#endif // RCT_NEW_ARCH_ENABLED
scrollViewComponent.frame = newFrame;
return YES;
}
return NO;
}
#ifdef RCT_NEW_ARCH_ENABLED
#pragma mark - RCTComponentViewProtocol
- (void)updateLayoutMetrics:(const facebook::react::LayoutMetrics &)layoutMetrics
oldLayoutMetrics:(const facebook::react::LayoutMetrics &)oldLayoutMetrics
{
[super updateLayoutMetrics:layoutMetrics oldLayoutMetrics:oldLayoutMetrics];
[self notifyDelegateWithFrame:RCTCGRectFromRect(layoutMetrics.frame)];
}
+ (react::ComponentDescriptorProvider)componentDescriptorProvider
{
return react::concreteComponentDescriptorProvider<react::RNSScreenContentWrapperComponentDescriptor>();
}
Class<RCTComponentViewProtocol> RNSScreenContentWrapperCls(void)
{
return RNSScreenContentWrapper.class;
}
// Needed because of this: https://github.com/facebook/react-native/pull/37274
+ (void)load
{
[super load];
}
#endif // RCT_NEW_ARCH_ENABLED
@end
@implementation RNSScreenContentWrapperManager
RCT_EXPORT_MODULE()
- (UIView *)view
{
return [RNSScreenContentWrapper new];
}
@end

View File

@@ -0,0 +1,32 @@
#pragma once
#import <React/RCTViewManager.h>
#import <UIKit/UIKit.h>
#if RCT_NEW_ARCH_ENABLED
#import <React/RCTFabricComponentsPlugins.h>
#import <React/RCTViewComponentView.h>
#else
#import <React/RCTView.h>
#endif // RCT_NEW_ARCH_ENABLED
NS_ASSUME_NONNULL_BEGIN
typedef void (^OnLayoutCallback)(CGRect frame);
@interface RNSScreenFooter :
#ifdef RCT_NEW_ARCH_ENABLED
RCTViewComponentView
#else
RCTView
#endif
@property (nonatomic, copy, nullable) OnLayoutCallback onLayout;
@end
@interface RNSScreenFooterManager : RCTViewManager
@end
NS_ASSUME_NONNULL_END

View File

@@ -0,0 +1,142 @@
#import "RNSScreenFooter.h"
#import "RNSScreen.h"
#ifdef RCT_NEW_ARCH_ENABLED
#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
@implementation RNSScreenFooter {
RNSScreenView *_parent;
}
- (instancetype)init
{
if (self = [super init]) {
self.translatesAutoresizingMaskIntoConstraints = false;
_parent = nil;
}
return self;
}
- (void)willMoveToSuperview:(UIView *)newSuperview
{
[super willMoveToSuperview:newSuperview];
// if ([newSuperview isKindOfClass:RNSScreenView.class]) {
// RNSScreenView *screen = (RNSScreenView *)newSuperview;
// _parent = (RNSScreenView *)newSuperview;
// [NSLayoutConstraint activateConstraints:@[
// [NSLayoutConstraint constraintWithItem:self attribute:NSLayoutAttributeBottom
// relatedBy:NSLayoutRelationEqual toItem:screen attribute:NSLayoutAttributeBottom multiplier:1.0
// constant:0.0], [NSLayoutConstraint constraintWithItem:self attribute:NSLayoutAttributeLeft
// relatedBy:NSLayoutRelationEqual toItem:screen attribute:NSLayoutAttributeLeft multiplier:1.0 constant:0.0],
// [NSLayoutConstraint constraintWithItem:self attribute:NSLayoutAttributeRight relatedBy:NSLayoutRelationEqual
// toItem:screen attribute:NSLayoutAttributeRight multiplier:1.0 constant:0.0], [NSLayoutConstraint
// constraintWithItem:self attribute:NSLayoutAttributeTop relatedBy:NSLayoutRelationEqual toItem:screen
// attribute:NSLayoutAttributeTop multiplier:1.0 constant:0.0], [NSLayoutConstraint constraintWithItem:screen
// attribute:NSLayoutAttributeBottom relatedBy:NSLayoutRelationEqual toItem:self
// attribute:NSLayoutAttributeBottom multiplier:1.0 constant:0.0], [NSLayoutConstraint
// constraintWithItem:screen attribute:NSLayoutAttributeLeft relatedBy:NSLayoutRelationEqual toItem:self
// attribute:NSLayoutAttributeLeft multiplier:1.0 constant:0.0], [NSLayoutConstraint constraintWithItem:screen
// attribute:NSLayoutAttributeRight relatedBy:NSLayoutRelationEqual toItem:self
// attribute:NSLayoutAttributeRight multiplier:1.0 constant:0.0], [NSLayoutConstraint constraintWithItem:screen
// attribute:NSLayoutAttributeTop relatedBy:NSLayoutRelationEqual toItem:self attribute:NSLayoutAttributeTop
// multiplier:1.0 constant:0.0],
// ]];
// [self setNeedsLayout];
// }
}
- (void)didMoveToSuperview
{
if (_parent != nil) {
// [NSLayoutConstraint activateConstraints:@[
// [NSLayoutConstraint constraintWithItem:self
// attribute:NSLayoutAttributeBottom
// relatedBy:NSLayoutRelationEqual
// toItem:_parent
// attribute:NSLayoutAttributeBottom
// multiplier:1.0
// constant:0.0],
// [NSLayoutConstraint constraintWithItem:self attribute:NSLayoutAttributeLeft
// relatedBy:NSLayoutRelationEqual toItem:_parent attribute:NSLayoutAttributeLeft multiplier:1.0
// constant:0.0], [NSLayoutConstraint constraintWithItem:self attribute:NSLayoutAttributeRight
// relatedBy:NSLayoutRelationEqual toItem:_parent attribute:NSLayoutAttributeRight multiplier:1.0
// constant:0.0], [NSLayoutConstraint constraintWithItem:self attribute:NSLayoutAttributeTop
// relatedBy:NSLayoutRelationEqual toItem:_parent attribute:NSLayoutAttributeTop multiplier:1.0
// constant:0.0], [NSLayoutConstraint constraintWithItem:_parent attribute:NSLayoutAttributeBottom
// relatedBy:NSLayoutRelationEqual toItem:self attribute:NSLayoutAttributeBottom multiplier:1.0
// constant:0.0], [NSLayoutConstraint constraintWithItem:_parent attribute:NSLayoutAttributeLeft
// relatedBy:NSLayoutRelationEqual toItem:self attribute:NSLayoutAttributeLeft multiplier:1.0 constant:0.0],
// [NSLayoutConstraint constraintWithItem:_parent attribute:NSLayoutAttributeRight
// relatedBy:NSLayoutRelationEqual toItem:self attribute:NSLayoutAttributeRight multiplier:1.0 constant:0.0],
// [NSLayoutConstraint constraintWithItem:_parent attribute:NSLayoutAttributeTop
// relatedBy:NSLayoutRelationEqual toItem:self attribute:NSLayoutAttributeTop multiplier:1.0 constant:0.0],
// ]];
// [self setNeedsLayout];
}
}
- (void)layoutSubviews
{
[super layoutSubviews];
if (self.onLayout != nil) {
self.onLayout(self.frame);
}
//
// if (self.subviews.count > 0) {
// CGSize childsSize = self.subviews[0].frame.size;
//
// }
}
#ifdef RCT_NEW_ARCH_ENABLED
#pragma Fabric specific
// Needed because of this: https://github.com/facebook/react-native/pull/37274
+ (void)load
{
[super load];
}
+ (react::ComponentDescriptorProvider)componentDescriptorProvider
{
return react::concreteComponentDescriptorProvider<react::RNSScreenFooterComponentDescriptor>();
}
Class<RCTComponentViewProtocol> RNSScreenFooterCls(void)
{
return RNSScreenFooter.class;
}
#else
#pragma Paper specific
- (void)reactSetFrame:(CGRect)frame
{
// ignore frame from react
// this view should be layouted by it's parent screen
// [super reactSetFrame:frame];
}
#endif // RCT_NEW_ARCH_ENABLED
@end
@implementation RNSScreenFooterManager
RCT_EXPORT_MODULE()
- (UIView *)view
{
return [RNSScreenFooter new];
}
@end

View File

@@ -0,0 +1,18 @@
#pragma once
#import <React/RCTViewManager.h>
#import "RNSScreenContainer.h"
#import "RNSScreenStack.h"
@interface RNSContainerNavigationController : RNSNavigationController
@end
@interface RNSScreenNavigationContainerView : RNSScreenContainerView
@end
@interface RNSScreenNavigationContainerManager : RNSScreenContainerManager
@end

View File

@@ -0,0 +1,73 @@
#import "RNSScreenNavigationContainer.h"
#import "RNSScreen.h"
#import "RNSScreenContainer.h"
#ifdef RCT_NEW_ARCH_ENABLED
#import <React/RCTFabricComponentsPlugins.h>
#import <react/renderer/components/rnscreens/ComponentDescriptors.h>
#import <react/renderer/components/rnscreens/Props.h>
namespace react = facebook::react;
#endif // RCT_NEW_ARCH_ENABLED
@implementation RNSContainerNavigationController
@end
@implementation RNSScreenNavigationContainerView
- (void)setupController
{
self.controller = [[RNSContainerNavigationController alloc] init];
[(RNSContainerNavigationController *)self.controller setNavigationBarHidden:YES animated:NO];
[self addSubview:self.controller.view];
}
- (void)updateContainer
{
for (RNSScreenView *screen in self.reactSubviews) {
if (screen.activityState == RNSActivityStateOnTop) {
// there should never be more than one screen with `RNSActivityStateOnTop`
// since this component should be used for `tabs` and `drawer` navigators
[(RNSContainerNavigationController *)self.controller setViewControllers:@[ screen.controller ] animated:NO];
[screen notifyFinishTransitioning];
}
}
[self maybeDismissVC];
}
#pragma mark-- Fabric specific
#ifdef RCT_NEW_ARCH_ENABLED
+ (react::ComponentDescriptorProvider)componentDescriptorProvider
{
return react::concreteComponentDescriptorProvider<react::RNSScreenNavigationContainerComponentDescriptor>();
}
// Needed because of this: https://github.com/facebook/react-native/pull/37274
+ (void)load
{
[super load];
}
#endif
@end
#ifdef RCT_NEW_ARCH_ENABLED
Class<RCTComponentViewProtocol> RNSScreenNavigationContainerCls(void)
{
return RNSScreenNavigationContainerView.class;
}
#endif
@implementation RNSScreenNavigationContainerManager
RCT_EXPORT_MODULE()
- (UIView *)view
{
return [[RNSScreenNavigationContainerView alloc] init];
}
@end

71
node_modules/react-native-screens/ios/RNSScreenStack.h generated vendored Normal file
View File

@@ -0,0 +1,71 @@
#pragma once
#ifdef RCT_NEW_ARCH_ENABLED
#import <React/RCTViewComponentView.h>
#else
#import <React/RCTUIManagerObserverCoordinator.h>
#import <React/RCTViewManager.h>
#endif
#import "RNSBottomTabsSpecialEffectsSupporting.h"
#import "RNSScreenContainer.h"
#if !TARGET_OS_TV
#import "RNSOrientationProviding.h"
#endif // !TARGET_OS_TV
NS_ASSUME_NONNULL_BEGIN
@interface RNSNavigationController : UINavigationController <
RNSViewControllerDelegate,
RNSBottomTabsSpecialEffectsSupporting
#if !TARGET_OS_TV
,
RNSOrientationProviding
#endif // !TARGET_OS_TV
>
@end
@interface RNSScreenStackView :
#ifdef RCT_NEW_ARCH_ENABLED
RCTViewComponentView <RNSScreenContainerDelegate>
#else
UIView <RNSScreenContainerDelegate, RCTInvalidating>
#endif
- (void)markChildUpdated;
- (void)didUpdateChildren;
- (void)startScreenTransition;
- (void)updateScreenTransition:(double)progress;
- (void)finishScreenTransition:(BOOL)canceled;
@property (nonatomic) BOOL customAnimation;
@property (nonatomic) BOOL disableSwipeBack;
@property (nonatomic, readwrite) BOOL iosPreventReattachmentOfDismissedScreens;
#ifdef RCT_NEW_ARCH_ENABLED
#else
@property (nonatomic, copy) RCTDirectEventBlock onFinishTransitioning;
#endif // RCT_NEW_ARCH_ENABLED
@end
#pragma mark-- Integration
@interface RNSScreenStackView ()
/**
* \return Arrray with ids of screens owned by this stack. Ids are returned in no particular order. The list might be
* empty. The strings inside the list are nullable if the screen has not been assigned an ID.
*/
@property (nonatomic, readonly, nonnull) NSArray<NSString *> *screenIds;
@end
@interface RNSScreenStackManager : RCTViewManager <RCTInvalidating>
@end
NS_ASSUME_NONNULL_END

1585
node_modules/react-native-screens/ios/RNSScreenStack.mm generated vendored Normal file

File diff suppressed because it is too large Load Diff

View File

@@ -0,0 +1,22 @@
#pragma once
#import "RNSScreen.h"
@interface RNSScreenStackAnimator : NSObject <UIViewControllerAnimatedTransitioning>
/// This property is filled whenever there is an ongoing animation and cleared on animation end.
@property (nonatomic, strong, nullable, readonly) UIViewPropertyAnimator *inFlightAnimator;
- (nonnull instancetype)initWithOperation:(UINavigationControllerOperation)operation;
/// In case of interactive / interruptible transition (e.g. swipe back gesture) this method should return
/// timing parameters expected by animator to be used for animation completion (e.g. when user's
/// gesture had ended).
///
/// @return timing curve provider expected to be used for animation completion or nil,
/// when there is no interactive transition running.
- (nullable id<UITimingCurveProvider>)timingParamsForAnimationCompletion;
+ (BOOL)isCustomAnimation:(RNSScreenStackAnimation)animation;
@end

View File

@@ -0,0 +1,585 @@
#import "RNSScreenStackAnimator.h"
#import "RNSScreenStack.h"
#import "RNSScreen.h"
#pragma mark - Constants
// Default duration for transitions in seconds. Note, that this enforces the default
// only on Paper. On Fabric the transition duration coming from JS layer
// is never null, thus it defaults to the value set in component codegen spec.
static constexpr NSTimeInterval RNSDefaultTransitionDuration = 0.5;
// Proportions for diffrent phases of more complex animations.
// The reference duration differs from default transition duration,
// because we've changed the default duration & we want to keep proportions
// in tact. Unit = seconds.
static constexpr NSTimeInterval RNSTransitionDurationForProportion = 0.35;
static constexpr float RNSSlideOpenTransitionDurationProportion = 1;
static constexpr float RNSFadeOpenTransitionDurationProportion = 0.2 / RNSTransitionDurationForProportion;
static constexpr float RNSSlideCloseTransitionDurationProportion = 0.25 / RNSTransitionDurationForProportion;
static constexpr float RNSFadeCloseTransitionDurationProportion = 0.15 / RNSTransitionDurationForProportion;
static constexpr float RNSFadeCloseDelayTransitionDurationProportion = 0.1 / RNSTransitionDurationForProportion;
// Value used for dimming view attached for tranistion time.
// Same value is used in other projects using similar approach for transistions
// and it looks the most similar to the value used by Apple
static constexpr float RNSShadowViewMaxAlpha = 0.1;
@implementation RNSScreenStackAnimator {
UINavigationControllerOperation _operation;
NSTimeInterval _transitionDuration;
UIViewPropertyAnimator *_Nullable _inFlightAnimator;
}
- (instancetype)initWithOperation:(UINavigationControllerOperation)operation
{
if (self = [super init]) {
_operation = operation;
_transitionDuration = RNSDefaultTransitionDuration; // default duration in seconds
_inFlightAnimator = nil;
}
return self;
}
#pragma mark - UIViewControllerAnimatedTransitioning
- (NSTimeInterval)transitionDuration:(id<UIViewControllerContextTransitioning>)transitionContext
{
RNSScreenView *screen;
if (_operation == UINavigationControllerOperationPush) {
UIViewController *toViewController =
[transitionContext viewControllerForKey:UITransitionContextToViewControllerKey];
screen = ((RNSScreen *)toViewController).screenView;
} else if (_operation == UINavigationControllerOperationPop) {
UIViewController *fromViewController =
[transitionContext viewControllerForKey:UITransitionContextFromViewControllerKey];
screen = ((RNSScreen *)fromViewController).screenView;
}
if (screen != nil && screen.stackAnimation == RNSScreenStackAnimationNone) {
return 0.0;
}
if (screen != nil && screen.transitionDuration != nil && [screen.transitionDuration floatValue] >= 0) {
float durationInSeconds = [screen.transitionDuration floatValue] / 1000.0;
return durationInSeconds;
}
return _transitionDuration;
}
- (void)animateTransition:(id<UIViewControllerContextTransitioning>)transitionContext
{
UIViewController *toViewController = [transitionContext viewControllerForKey:UITransitionContextToViewControllerKey];
UIViewController *fromViewController =
[transitionContext viewControllerForKey:UITransitionContextFromViewControllerKey];
toViewController.view.frame = [transitionContext finalFrameForViewController:toViewController];
RNSScreenView *screen;
if (_operation == UINavigationControllerOperationPush) {
screen = ((RNSScreen *)toViewController).screenView;
} else if (_operation == UINavigationControllerOperationPop) {
screen = ((RNSScreen *)fromViewController).screenView;
}
if (screen != nil) {
if ([screen.reactSuperview isKindOfClass:[RNSScreenStackView class]] &&
((RNSScreenStackView *)(screen.reactSuperview)).customAnimation) {
[self animateWithNoAnimation:transitionContext toVC:toViewController fromVC:fromViewController];
} else if (screen.isFullScreenSwipeEffectivelyEnabled && transitionContext.isInteractive) {
// we are swiping with full width gesture
if (screen.customAnimationOnSwipe) {
[self animateTransitionWithStackAnimation:screen.stackAnimation
shadowEnabled:screen.fullScreenSwipeShadowEnabled
transitionContext:transitionContext
toVC:toViewController
fromVC:fromViewController];
} else {
// we have to provide an animation when swiping, otherwise the screen will be popped immediately,
// so in case of no custom animation on swipe set, we provide the one closest to the default
[self animateSimplePushWithShadowEnabled:screen.fullScreenSwipeShadowEnabled
transitionContext:transitionContext
toVC:toViewController
fromVC:fromViewController];
}
} else {
// we are going forward or provided custom animation on swipe or clicked native header back button
[self animateTransitionWithStackAnimation:screen.stackAnimation
shadowEnabled:screen.fullScreenSwipeShadowEnabled
transitionContext:transitionContext
toVC:toViewController
fromVC:fromViewController];
}
}
}
- (void)animationEnded:(BOOL)transitionCompleted
{
_inFlightAnimator = nil;
}
#pragma mark - Animation implementations
- (void)animateSimplePushWithShadowEnabled:(BOOL)shadowEnabled
transitionContext:(id<UIViewControllerContextTransitioning>)transitionContext
toVC:(UIViewController *)toViewController
fromVC:(UIViewController *)fromViewController
{
float containerWidth = transitionContext.containerView.bounds.size.width;
float belowViewWidth = containerWidth * 0.3;
CGAffineTransform rightTransform = CGAffineTransformMakeTranslation(containerWidth, 0);
CGAffineTransform leftTransform = CGAffineTransformMakeTranslation(-belowViewWidth, 0);
if (toViewController.navigationController.view.semanticContentAttribute ==
UISemanticContentAttributeForceRightToLeft) {
rightTransform = CGAffineTransformMakeTranslation(-containerWidth, 0);
leftTransform = CGAffineTransformMakeTranslation(belowViewWidth, 0);
}
UIView *shadowView;
if (shadowEnabled) {
shadowView = [[UIView alloc] initWithFrame:fromViewController.view.frame];
shadowView.backgroundColor = [UIColor blackColor];
}
if (_operation == UINavigationControllerOperationPush) {
toViewController.view.transform = rightTransform;
[[transitionContext containerView] addSubview:toViewController.view];
if (shadowView) {
[[transitionContext containerView] insertSubview:shadowView belowSubview:toViewController.view];
shadowView.alpha = 0.0;
}
UIViewPropertyAnimator *animator =
[[UIViewPropertyAnimator alloc] initWithDuration:[self transitionDuration:transitionContext]
timingParameters:[RNSScreenStackAnimator defaultSpringTimingParametersApprox]];
[animator addAnimations:^{
fromViewController.view.transform = leftTransform;
toViewController.view.transform = CGAffineTransformIdentity;
if (shadowView) {
shadowView.alpha = RNSShadowViewMaxAlpha;
}
}];
[animator addCompletion:^(UIViewAnimatingPosition finalPosition) {
if (shadowView) {
[shadowView removeFromSuperview];
}
fromViewController.view.transform = CGAffineTransformIdentity;
toViewController.view.transform = CGAffineTransformIdentity;
[transitionContext completeTransition:![transitionContext transitionWasCancelled]];
}];
_inFlightAnimator = animator;
[animator startAnimation];
} else if (_operation == UINavigationControllerOperationPop) {
toViewController.view.transform = leftTransform;
[[transitionContext containerView] insertSubview:toViewController.view belowSubview:fromViewController.view];
if (shadowView) {
[[transitionContext containerView] insertSubview:shadowView belowSubview:fromViewController.view];
shadowView.alpha = RNSShadowViewMaxAlpha;
}
void (^animationBlock)(void) = ^{
toViewController.view.transform = CGAffineTransformIdentity;
fromViewController.view.transform = rightTransform;
if (shadowView) {
shadowView.alpha = 0.0;
}
};
void (^completionBlock)(UIViewAnimatingPosition) = ^(UIViewAnimatingPosition finalPosition) {
if (shadowView) {
[shadowView removeFromSuperview];
}
fromViewController.view.transform = CGAffineTransformIdentity;
toViewController.view.transform = CGAffineTransformIdentity;
[transitionContext completeTransition:![transitionContext transitionWasCancelled]];
};
if (!transitionContext.isInteractive) {
UIViewPropertyAnimator *animator = [[UIViewPropertyAnimator alloc]
initWithDuration:[self transitionDuration:transitionContext]
timingParameters:[RNSScreenStackAnimator defaultSpringTimingParametersApprox]];
[animator addAnimations:animationBlock];
[animator addCompletion:completionBlock];
_inFlightAnimator = animator;
[animator startAnimation];
} else {
// we don't want the EaseInOut option when swiping to dismiss the view, it is the same in default animation option
UIViewPropertyAnimator *animator =
[[UIViewPropertyAnimator alloc] initWithDuration:[self transitionDuration:transitionContext]
curve:UIViewAnimationCurveLinear
animations:animationBlock];
[animator addCompletion:completionBlock];
[animator setUserInteractionEnabled:YES];
_inFlightAnimator = animator;
}
}
}
- (void)animateSlideFromLeftWithTransitionContext:(id<UIViewControllerContextTransitioning>)transitionContext
toVC:(UIViewController *)toViewController
fromVC:(UIViewController *)fromViewController
{
float containerWidth = transitionContext.containerView.bounds.size.width;
float belowViewWidth = containerWidth * 0.3;
CGAffineTransform rightTransform = CGAffineTransformMakeTranslation(-containerWidth, 0);
CGAffineTransform leftTransform = CGAffineTransformMakeTranslation(belowViewWidth, 0);
if (toViewController.navigationController.view.semanticContentAttribute ==
UISemanticContentAttributeForceRightToLeft) {
rightTransform = CGAffineTransformMakeTranslation(containerWidth, 0);
leftTransform = CGAffineTransformMakeTranslation(-belowViewWidth, 0);
}
if (_operation == UINavigationControllerOperationPush) {
toViewController.view.transform = rightTransform;
[[transitionContext containerView] addSubview:toViewController.view];
UIViewPropertyAnimator *animator =
[[UIViewPropertyAnimator alloc] initWithDuration:[self transitionDuration:transitionContext]
timingParameters:[RNSScreenStackAnimator defaultSpringTimingParametersApprox]];
[animator addAnimations:^{
fromViewController.view.transform = leftTransform;
toViewController.view.transform = CGAffineTransformIdentity;
}];
[animator addCompletion:^(UIViewAnimatingPosition finalPosition) {
fromViewController.view.transform = CGAffineTransformIdentity;
toViewController.view.transform = CGAffineTransformIdentity;
[transitionContext completeTransition:![transitionContext transitionWasCancelled]];
}];
_inFlightAnimator = animator;
[animator startAnimation];
} else if (_operation == UINavigationControllerOperationPop) {
toViewController.view.transform = leftTransform;
[[transitionContext containerView] insertSubview:toViewController.view belowSubview:fromViewController.view];
void (^animationBlock)(void) = ^{
toViewController.view.transform = CGAffineTransformIdentity;
fromViewController.view.transform = rightTransform;
};
void (^completionBlock)(UIViewAnimatingPosition) = ^(UIViewAnimatingPosition finalPosition) {
fromViewController.view.transform = CGAffineTransformIdentity;
toViewController.view.transform = CGAffineTransformIdentity;
[transitionContext completeTransition:![transitionContext transitionWasCancelled]];
};
if (!transitionContext.isInteractive) {
UIViewPropertyAnimator *animator = [[UIViewPropertyAnimator alloc]
initWithDuration:[self transitionDuration:transitionContext]
timingParameters:[RNSScreenStackAnimator defaultSpringTimingParametersApprox]];
[animator addAnimations:animationBlock];
[animator addCompletion:completionBlock];
_inFlightAnimator = animator;
[animator startAnimation];
} else {
// we don't want the EaseInOut option when swiping to dismiss the view, it is the same in default animation option
UIViewPropertyAnimator *animator =
[[UIViewPropertyAnimator alloc] initWithDuration:[self transitionDuration:transitionContext]
curve:UIViewAnimationCurveLinear
animations:animationBlock];
[animator addCompletion:completionBlock];
[animator setUserInteractionEnabled:YES];
_inFlightAnimator = animator;
}
}
}
- (void)animateFadeWithTransitionContext:(id<UIViewControllerContextTransitioning>)transitionContext
toVC:(UIViewController *)toViewController
fromVC:(UIViewController *)fromViewController
{
toViewController.view.frame = [transitionContext finalFrameForViewController:toViewController];
if (_operation == UINavigationControllerOperationPush) {
[[transitionContext containerView] addSubview:toViewController.view];
toViewController.view.alpha = 0.0;
auto animator = [[UIViewPropertyAnimator alloc] initWithDuration:[self transitionDuration:transitionContext]
curve:UIViewAnimationCurveEaseInOut
animations:^{
toViewController.view.alpha = 1.0;
}];
[animator addCompletion:^(UIViewAnimatingPosition finalPosition) {
toViewController.view.alpha = 1.0;
[transitionContext completeTransition:![transitionContext transitionWasCancelled]];
}];
_inFlightAnimator = animator;
[animator startAnimation];
} else if (_operation == UINavigationControllerOperationPop) {
[[transitionContext containerView] insertSubview:toViewController.view belowSubview:fromViewController.view];
auto animator = [[UIViewPropertyAnimator alloc] initWithDuration:[self transitionDuration:transitionContext]
curve:UIViewAnimationCurveEaseInOut
animations:^{
fromViewController.view.alpha = 0.0;
}];
[animator addCompletion:^(UIViewAnimatingPosition finalPosition) {
fromViewController.view.alpha = 1.0;
[transitionContext completeTransition:![transitionContext transitionWasCancelled]];
}];
_inFlightAnimator = animator;
[animator startAnimation];
}
}
- (void)animateSlideFromBottomWithTransitionContext:(id<UIViewControllerContextTransitioning>)transitionContext
toVC:(UIViewController *)toViewController
fromVC:(UIViewController *)fromViewController
{
CGAffineTransform topBottomTransform =
CGAffineTransformMakeTranslation(0, transitionContext.containerView.bounds.size.height);
if (_operation == UINavigationControllerOperationPush) {
toViewController.view.transform = topBottomTransform;
[[transitionContext containerView] addSubview:toViewController.view];
auto animator = [[UIViewPropertyAnimator alloc] initWithDuration:[self transitionDuration:transitionContext]
curve:UIViewAnimationCurveEaseInOut
animations:^{
fromViewController.view.transform =
CGAffineTransformIdentity;
toViewController.view.transform = CGAffineTransformIdentity;
}];
[animator addCompletion:^(UIViewAnimatingPosition finalPosition) {
fromViewController.view.transform = CGAffineTransformIdentity;
toViewController.view.transform = CGAffineTransformIdentity;
[transitionContext completeTransition:![transitionContext transitionWasCancelled]];
}];
_inFlightAnimator = animator;
[animator startAnimation];
} else if (_operation == UINavigationControllerOperationPop) {
toViewController.view.transform = CGAffineTransformIdentity;
[[transitionContext containerView] insertSubview:toViewController.view belowSubview:fromViewController.view];
void (^animationBlock)(void) = ^{
toViewController.view.transform = CGAffineTransformIdentity;
fromViewController.view.transform = topBottomTransform;
};
void (^completionBlock)(UIViewAnimatingPosition) = ^(UIViewAnimatingPosition finalPosition) {
fromViewController.view.transform = CGAffineTransformIdentity;
toViewController.view.transform = CGAffineTransformIdentity;
[transitionContext completeTransition:![transitionContext transitionWasCancelled]];
};
if (!transitionContext.isInteractive) {
auto animator = [[UIViewPropertyAnimator alloc] initWithDuration:[self transitionDuration:transitionContext]
curve:UIViewAnimationCurveEaseInOut
animations:animationBlock];
[animator addCompletion:completionBlock];
_inFlightAnimator = animator;
[animator startAnimation];
} else {
// we don't want the EaseInOut option when swiping to dismiss the view, it is the same in default animation option
auto animator = [[UIViewPropertyAnimator alloc] initWithDuration:[self transitionDuration:transitionContext]
curve:UIViewAnimationCurveLinear
animations:animationBlock];
[animator addCompletion:completionBlock];
_inFlightAnimator = animator;
}
}
}
- (void)animateFadeFromBottomWithTransitionContext:(id<UIViewControllerContextTransitioning>)transitionContext
toVC:(UIViewController *)toViewController
fromVC:(UIViewController *)fromViewController
{
CGAffineTransform topBottomTransform =
CGAffineTransformMakeTranslation(0, 0.08 * transitionContext.containerView.bounds.size.height);
const float baseTransitionDuration = [self transitionDuration:transitionContext];
if (_operation == UINavigationControllerOperationPush) {
toViewController.view.transform = topBottomTransform;
toViewController.view.alpha = 0.0;
[[transitionContext containerView] addSubview:toViewController.view];
// Android Nougat open animation
// http://aosp.opersys.com/xref/android-7.1.2_r37/xref/frameworks/base/core/res/res/anim/activity_open_enter.xml
auto slideAnimator = [[UIViewPropertyAnimator alloc]
initWithDuration:baseTransitionDuration * RNSSlideOpenTransitionDurationProportion
curve:UIViewAnimationCurveEaseOut
animations:^{
fromViewController.view.transform = CGAffineTransformIdentity;
toViewController.view.transform = CGAffineTransformIdentity;
}];
[slideAnimator addCompletion:^(UIViewAnimatingPosition finalPosition) {
fromViewController.view.transform = CGAffineTransformIdentity;
[transitionContext completeTransition:![transitionContext transitionWasCancelled]];
}];
auto fadeAnimator = [[UIViewPropertyAnimator alloc]
initWithDuration:baseTransitionDuration * RNSFadeOpenTransitionDurationProportion
curve:UIViewAnimationCurveEaseOut
animations:^{
toViewController.view.alpha = 1.0;
}];
_inFlightAnimator = slideAnimator;
[slideAnimator startAnimation];
[fadeAnimator startAnimation];
} else if (_operation == UINavigationControllerOperationPop) {
toViewController.view.transform = CGAffineTransformIdentity;
[[transitionContext containerView] insertSubview:toViewController.view belowSubview:fromViewController.view];
// Android Nougat exit animation
// http://aosp.opersys.com/xref/android-7.1.2_r37/xref/frameworks/base/core/res/res/anim/activity_close_exit.xml
auto slideAnimator = [[UIViewPropertyAnimator alloc]
initWithDuration:baseTransitionDuration * RNSSlideCloseTransitionDurationProportion
curve:UIViewAnimationCurveEaseIn
animations:^{
toViewController.view.transform = CGAffineTransformIdentity;
fromViewController.view.transform = topBottomTransform;
}];
[slideAnimator addCompletion:^(UIViewAnimatingPosition finalPosition) {
fromViewController.view.transform = CGAffineTransformIdentity;
toViewController.view.transform = CGAffineTransformIdentity;
fromViewController.view.alpha = 1.0;
toViewController.view.alpha = 1.0;
[transitionContext completeTransition:![transitionContext transitionWasCancelled]];
}];
auto fadeAnimator = [[UIViewPropertyAnimator alloc]
initWithDuration:baseTransitionDuration * RNSFadeCloseTransitionDurationProportion
curve:UIViewAnimationCurveLinear
animations:^{
fromViewController.view.alpha = 0.0;
}];
_inFlightAnimator = slideAnimator;
[slideAnimator startAnimation];
[fadeAnimator startAnimationAfterDelay:baseTransitionDuration * RNSFadeCloseDelayTransitionDurationProportion];
}
}
- (void)animateWithNoAnimation:(id<UIViewControllerContextTransitioning>)transitionContext
toVC:(UIViewController *)toViewController
fromVC:(UIViewController *)fromViewController
{
if (_operation == UINavigationControllerOperationPush) {
[[transitionContext containerView] addSubview:toViewController.view];
[UIView animateWithDuration:[self transitionDuration:transitionContext]
animations:^{
}
completion:^(BOOL finished) {
[transitionContext completeTransition:![transitionContext transitionWasCancelled]];
}];
} else if (_operation == UINavigationControllerOperationPop) {
[[transitionContext containerView] insertSubview:toViewController.view belowSubview:fromViewController.view];
[UIView animateWithDuration:[self transitionDuration:transitionContext]
animations:^{
}
completion:^(BOOL finished) {
[transitionContext completeTransition:![transitionContext transitionWasCancelled]];
}];
}
}
- (void)animateNoneWithTransitionContext:(id<UIViewControllerContextTransitioning>)transitionContext
toVC:(UIViewController *)toViewController
fromVC:(UIViewController *)fromViewController
{
toViewController.view.frame = [transitionContext finalFrameForViewController:toViewController];
if (_operation == UINavigationControllerOperationPush) {
[[transitionContext containerView] addSubview:toViewController.view];
toViewController.view.alpha = 0.0;
[UIView animateWithDuration:[self transitionDuration:transitionContext]
animations:^{
toViewController.view.alpha = 1.0;
}
completion:^(BOOL finished) {
toViewController.view.alpha = 1.0;
[transitionContext completeTransition:![transitionContext transitionWasCancelled]];
}];
} else if (_operation == UINavigationControllerOperationPop) {
[[transitionContext containerView] insertSubview:toViewController.view belowSubview:fromViewController.view];
[UIView animateWithDuration:[self transitionDuration:transitionContext]
animations:^{
fromViewController.view.alpha = 0.0;
}
completion:^(BOOL finished) {
fromViewController.view.alpha = 1.0;
[transitionContext completeTransition:![transitionContext transitionWasCancelled]];
}];
}
}
#pragma mark - Public API
- (nullable id<UITimingCurveProvider>)timingParamsForAnimationCompletion
{
return [RNSScreenStackAnimator defaultSpringTimingParametersApprox];
}
+ (BOOL)isCustomAnimation:(RNSScreenStackAnimation)animation
{
return (animation != RNSScreenStackAnimationFlip && animation != RNSScreenStackAnimationDefault);
}
#pragma mark - Helpers
- (void)animateTransitionWithStackAnimation:(RNSScreenStackAnimation)animation
shadowEnabled:(BOOL)shadowEnabled
transitionContext:(id<UIViewControllerContextTransitioning>)transitionContext
toVC:(UIViewController *)toVC
fromVC:(UIViewController *)fromVC
{
switch (animation) {
case RNSScreenStackAnimationSimplePush:
[self animateSimplePushWithShadowEnabled:shadowEnabled
transitionContext:transitionContext
toVC:toVC
fromVC:fromVC];
return;
case RNSScreenStackAnimationSlideFromLeft:
[self animateSlideFromLeftWithTransitionContext:transitionContext toVC:toVC fromVC:fromVC];
return;
case RNSScreenStackAnimationFade:
[self animateFadeWithTransitionContext:transitionContext toVC:toVC fromVC:fromVC];
return;
case RNSScreenStackAnimationSlideFromBottom:
[self animateSlideFromBottomWithTransitionContext:transitionContext toVC:toVC fromVC:fromVC];
return;
case RNSScreenStackAnimationFadeFromBottom:
[self animateFadeFromBottomWithTransitionContext:transitionContext toVC:toVC fromVC:fromVC];
return;
case RNSScreenStackAnimationNone:
[self animateNoneWithTransitionContext:transitionContext toVC:toVC fromVC:fromVC];
return;
default:
// simple_push is the default custom animation
[self animateSimplePushWithShadowEnabled:shadowEnabled
transitionContext:transitionContext
toVC:toVC
fromVC:fromVC];
}
}
+ (UISpringTimingParameters *)defaultSpringTimingParametersApprox
{
// Default curve provider is as defined below, however spring timing defined this way
// ignores the requested duration of the animation, effectively impairing our `animationDuration` prop.
// We want to keep `animationDuration` functional.
// id<UITimingCurveProvider> timingCurveProvider = [[UISpringTimingParameters alloc] init];
// According to "Programming iOS 14" by Matt Neuburg, the params for the default spring are as follows:
// mass = 3, stiffness = 1000, damping = 500. Damping ratio is computed using formula
// ratio = damping / (2 * sqrt(stiffness * mass)) ==> default damping ratio should be ~= 4,56.
// I've found afterwards that this is even indicated here:
// https://developer.apple.com/documentation/uikit/uispringtimingparameters/1649802-init?language=objc
return [[UISpringTimingParameters alloc] initWithDampingRatio:4.56];
}
@end

View File

@@ -0,0 +1,178 @@
#pragma once
#ifdef RCT_NEW_ARCH_ENABLED
#import <React/RCTViewComponentView.h>
#else
#import <React/RCTShadowView.h>
#import <React/RCTViewManager.h>
#endif
#import <React/RCTConvert.h>
#import "RNSScreen.h"
#import "RNSScreenStackHeaderSubview.h"
#import "RNSSearchBar.h"
@interface NSString (RNSStringUtil)
+ (BOOL)rnscreens_isBlankOrNull:(nullable NSString *)string;
@end
@interface RNSScreenStackHeaderConfig :
#ifdef RCT_NEW_ARCH_ENABLED
RCTViewComponentView
#else
UIView
#endif
@property (nonatomic, weak) RNSScreenView *screenView;
#ifdef RCT_NEW_ARCH_ENABLED
@property (nonatomic) BOOL show;
#else
@property (nonatomic) BOOL hide;
#endif
NS_ASSUME_NONNULL_BEGIN
@property (nonatomic, retain) NSString *title;
@property (nonatomic, retain) NSString *titleFontFamily;
@property (nonatomic, retain) NSNumber *titleFontSize;
@property (nonatomic, retain) NSString *titleFontWeight;
@property (nonatomic, retain) UIColor *titleColor;
@property (nonatomic, retain) NSString *backTitle;
@property (nonatomic, retain) NSString *backTitleFontFamily;
@property (nonatomic, retain) NSNumber *backTitleFontSize;
@property (nonatomic, getter=isBackTitleVisible) BOOL backTitleVisible;
@property (nonatomic, retain) UIColor *backgroundColor;
@property (nonatomic, retain) UIColor *color;
@property (nonatomic) BOOL largeTitle;
@property (nonatomic, retain) NSString *largeTitleFontFamily;
@property (nonatomic, retain) NSNumber *largeTitleFontSize;
@property (nonatomic, retain) NSString *largeTitleFontWeight;
@property (nonatomic, retain) UIColor *largeTitleBackgroundColor;
@property (nonatomic) BOOL largeTitleHideShadow;
@property (nonatomic, retain) UIColor *largeTitleColor;
@property (nonatomic) BOOL hideBackButton;
@property (nonatomic) BOOL disableBackButtonMenu;
@property (nonatomic) BOOL hideShadow;
@property (nonatomic) BOOL translucent;
@property (nonatomic) BOOL backButtonInCustomView;
@property (nonatomic) UISemanticContentAttribute direction;
@property (nonatomic) UINavigationItemBackButtonDisplayMode backButtonDisplayMode;
@property (nonatomic) RNSBlurEffectStyle blurEffect;
@property (nonatomic, copy, nullable) NSArray<NSDictionary<NSString *, id> *> *headerRightBarButtonItems;
@property (nonatomic, copy, nullable) NSArray<NSDictionary<NSString *, id> *> *headerLeftBarButtonItems;
#if !RCT_NEW_ARCH_ENABLED
@property (nonatomic) RCTDirectEventBlock onPressHeaderBarButtonItem;
@property (nonatomic) RCTDirectEventBlock onPressHeaderBarButtonMenuItem;
#endif
@property (nonatomic, readwrite) BOOL synchronousShadowStateUpdatesEnabled;
NS_ASSUME_NONNULL_END
+ (void)willShowViewController:(nonnull UIViewController *)vc
animated:(BOOL)animated
withConfig:(nonnull RNSScreenStackHeaderConfig *)config;
/**
* Returns true iff subview of given `type` is present.
*
* **Please note that the subviews are not mounted under the header config in HostTree**
* This method should serve only to check whether given subview type has been rendered.
*/
- (BOOL)hasSubviewOfType:(RNSScreenStackHeaderSubviewType)type;
/**
* Returns `true` iff subview of type `left` is present.
*
* **Please note that the subviews are not mounted under the header config in HostTree**
* This method should serve only to check whether given subview type has been rendered.
*/
- (BOOL)hasSubviewLeft;
/**
* Returns
* - `YES` on Paper, when `self.hide == NO`
* - `YES` on Fabric, when `self.show == YES`
* - `NO` otherwise.
*
* Convenience method, so that we do not need ifdefs in every callsite.
*/
- (BOOL)shouldHeaderBeVisible;
/**
* Returns `true` iff the applying this header config instance to a view controller will
* result in visible back button if feasible.
*/
- (BOOL)shouldBackButtonBeVisibleInNavigationBar:(nullable UINavigationBar *)navBar;
#ifdef RCT_NEW_ARCH_ENABLED
/**
* Allows to send information with size to the corresponding node in shadow tree.
* This method updates state of header config shadow node only.
*/
- (void)updateShadowStateWithSize:(CGSize)size edgeInsets:(NSDirectionalEdgeInsets)edgeInsets;
/**
* Updates state of header config shadow node and all subview shadow nodes in context of given UINavigationBar.
* When `navBar == nil` this method does nothing.
*/
- (void)updateHeaderStateInShadowTreeInContextOfNavigationBar:(nullable UINavigationBar *)navBar;
#else
/**
* Allows to send information with insets to the corresponding node in shadow tree.
* Currently only horizontal insets are send through. Vertical ones are filtered out.
*/
- (void)updateHeaderConfigState:(NSDirectionalEdgeInsets)insets;
#endif
@end
#pragma mark - Experimental
@interface RNSScreenStackHeaderConfig ()
@property (nonatomic) UIUserInterfaceStyle userInterfaceStyle;
@end
#pragma mark - View Manager
@interface RNSScreenStackHeaderConfigManager : RCTViewManager
@end
#ifdef RCT_NEW_ARCH_ENABLED
#else
#pragma mark - Legacy Shadow View
/**
* Used as local data send to shadow view on Paper. This helps us to provide Yoga
* with knowledge of native insets in the navigation bar.
*/
@interface RNSHeaderConfigInsetsPayload : NSObject
@property (nonatomic) NSDirectionalEdgeInsets insets;
- (instancetype)initWithInsets:(NSDirectionalEdgeInsets)insets NS_DESIGNATED_INITIALIZER;
@end
/**
* Custom shadow view for header config. This is used on Paper to provide Yoga
* with knowledge of native header insets (horizontal padding).
*/
@interface RNSScreenStackHeaderConfigShadowView : RCTShadowView
@end
#endif
#pragma mark - RCTConvert
@interface RCTConvert (RNSScreenStackHeader)
+ (UISemanticContentAttribute)UISemanticContentAttribute:(nonnull id)json;
+ (UINavigationItemBackButtonDisplayMode)UINavigationItemBackButtonDisplayMode:(nonnull id)json;
@end

File diff suppressed because it is too large Load Diff

View File

@@ -0,0 +1,68 @@
#pragma once
#ifdef RCT_NEW_ARCH_ENABLED
#import <React/RCTViewComponentView.h>
#endif
#import <React/RCTConvert.h>
#import <React/RCTViewManager.h>
#import "RNSEnums.h"
NS_ASSUME_NONNULL_BEGIN
@interface RNSScreenStackHeaderSubview :
#ifdef RCT_NEW_ARCH_ENABLED
RCTViewComponentView
#else
UIView
#endif
@property (nonatomic) RNSScreenStackHeaderSubviewType type;
@property (nonatomic, readwrite) BOOL synchronousShadowStateUpdatesEnabled;
@property (nonatomic, weak) UIView *reactSuperview;
#ifdef RCT_NEW_ARCH_ENABLED
/**
* Updates state of the header subview shadow node in shadow tree.
* This method updates state of header subview shadow node only.
*/
- (void)updateShadowStateWithFrame:(CGRect)frame;
/**
* Updates state of the header subview shadow node in shadow tree in context of given ancestor view.
* This method updates state of header subview shadow node only.
*
* @param ancestorView - ancestor view in relation to which, the frame send in state update is computed; if this is
* `nil` the method does nothing.
*/
- (void)updateShadowStateInContextOfAncestorView:(nullable UIView *)ancestorView;
/**
* Updates state of the header subview shadow node in shadow tree in context of given ancestor view.
* This method updates state of header subview shadow node only.
*
* @param ancestorView ancestor view in relation to which, the frame send in state update is computed; if this is
* `nil` the method does nothing.
* @param frame source frame, which will be transformed in relation to `ancestorView`.
*/
- (void)updateShadowStateInContextOfAncestorView:(nullable UIView *)ancestorView withFrame:(CGRect)frame;
#endif
- (UIBarButtonItem *)getUIBarButtonItem;
@end
@interface RNSScreenStackHeaderSubviewManager : RCTViewManager
@property (nonatomic) RNSScreenStackHeaderSubviewType type;
@end
@interface RCTConvert (RNSScreenStackHeaderSubview)
+ (RNSScreenStackHeaderSubviewType)RNSScreenStackHeaderSubviewType:(id)json;
@end
NS_ASSUME_NONNULL_END

View File

@@ -0,0 +1,367 @@
#import "RNSScreenStackHeaderSubview.h"
#import "RNSConvert.h"
#import "RNSDefines.h"
#import "RNSScreenStackHeaderConfig.h"
#ifdef RCT_NEW_ARCH_ENABLED
#import <cxxreact/ReactNativeVersion.h>
#import <react/renderer/components/rnscreens/ComponentDescriptors.h>
#import <react/renderer/components/rnscreens/EventEmitters.h>
#import <react/renderer/components/rnscreens/RCTComponentViewHelpers.h>
#import <React/RCTConversions.h>
#import <React/RCTFabricComponentsPlugins.h>
#import <rnscreens/RNSScreenStackHeaderSubviewComponentDescriptor.h>
#endif // RCT_NEW_ARCH_ENABLED
#ifdef RCT_NEW_ARCH_ENABLED
namespace react = facebook::react;
#endif // RCT_NEW_ARCH_ENABLED
@implementation RNSScreenStackHeaderSubview {
#if RCT_NEW_ARCH_ENABLED
react::RNSScreenStackHeaderSubviewShadowNode::ConcreteState::Shared _state;
CGRect _lastScheduledFrame;
#endif // RCT_NEW_ARCH_ENABLED
#if !RCT_NEW_ARCH_ENABLED && RNS_IPHONE_OS_VERSION_AVAILABLE(26_0)
CGSize _lastReactFrameSize;
#endif // !RCT_NEW_ARCH_ENABLED && RNS_IPHONE_OS_VERSION_AVAILABLE(26_0)
// TODO: Refactor this, so that we don't keep reference here at all.
// Currently this likely creates retain cycle between subview & the bar button item.
UIBarButtonItem *_barButtonItem;
BOOL _hidesSharedBackground;
}
#pragma mark - Common
- (nullable RNSScreenStackHeaderConfig *)getHeaderConfig
{
RNSScreenStackHeaderConfig *headerConfig = (RNSScreenStackHeaderConfig *_Nullable)self.reactSuperview;
#ifndef NDEBUG
if (headerConfig != nil && ![headerConfig isKindOfClass:[RNSScreenStackHeaderConfig class]]) {
RCTLogError(@"[RNScreens] Invalid view type, expecting RNSScreenStackHeaderConfig, got: %@", headerConfig);
return nil;
}
#endif
return headerConfig;
}
- (nullable UINavigationBar *)findNavigationBar
{
return [[[[[self getHeaderConfig] screenView] reactViewController] navigationController] navigationBar];
}
// We're forcing the navigation controller's view to re-layout
// see: https://github.com/software-mansion/react-native-screens/pull/2385
- (void)layoutNavigationBar
{
// If we're not attached yet, we should not layout the navigation bar,
// because the layout flow won't reach us & we will clear "isLayoutDirty" flags
// on view above us, causing subsequent layout request to not reach us.
if (self.window == nil) {
return;
}
UIView *toLayoutView = [self findNavigationBar];
// TODO: It is possible, that this needs to be called only on old architecture.
// Make sure that Test432 keeps working.
[toLayoutView setNeedsLayout];
// TODO: Determine why this must be called & deferring layout to next "update cycle"
// is not sufficient. See Test2552 and Test432. (Talking Paper here).
[toLayoutView layoutIfNeeded];
}
#ifdef RCT_NEW_ARCH_ENABLED
#pragma mark - Fabric specific
- (void)updateShadowStateInContextOfAncestorView:(nullable UIView *)ancestorView withFrame:(CGRect)frame
{
if (ancestorView == nil) {
// We can not compute valid value
return;
}
CGRect convertedFrame = [self convertRect:frame toView:ancestorView];
[self updateShadowStateWithFrame:convertedFrame];
}
- (void)updateShadowStateInContextOfAncestorView:(nullable UIView *)ancestorView
{
[self updateShadowStateInContextOfAncestorView:ancestorView withFrame:self.bounds];
}
- (void)updateShadowStateWithFrame:(CGRect)frame
{
if (_state == nullptr) {
return;
}
if (!CGRectEqualToRect(frame, _lastScheduledFrame)) {
auto newState =
react::RNSScreenStackHeaderSubviewState(RCTSizeFromCGSize(frame.size), RCTPointFromCGPoint(frame.origin));
_state->updateState(
std::move(newState)
#if REACT_NATIVE_VERSION_MINOR >= 82
,
_synchronousShadowStateUpdatesEnabled ? facebook::react::EventQueue::UpdateMode::unstable_Immediate
: facebook::react::EventQueue::UpdateMode::Asynchronous
#endif
);
_lastScheduledFrame = frame;
}
}
- (void)layoutSubviews
{
[super layoutSubviews];
[self updateShadowStateInContextOfAncestorView:[self findNavigationBar]];
}
// Needed because of this: https://github.com/facebook/react-native/pull/37274
+ (void)load
{
[super load];
}
- (instancetype)initWithFrame:(CGRect)frame
{
if (self = [super initWithFrame:frame]) {
static const auto defaultProps = std::make_shared<const react::RNSScreenStackHeaderSubviewProps>();
_props = defaultProps;
_lastScheduledFrame = CGRectZero;
}
return self;
}
#pragma mark - RCTComponentViewProtocol
- (void)updateProps:(react::Props::Shared const &)props oldProps:(react::Props::Shared const &)oldProps
{
const auto &newHeaderSubviewProps = *std::static_pointer_cast<const react::RNSScreenStackHeaderSubviewProps>(props);
[self setType:[RNSConvert RNSScreenStackHeaderSubviewTypeFromCppEquivalent:newHeaderSubviewProps.type]];
[self setHidesSharedBackground:newHeaderSubviewProps.hidesSharedBackground];
[self setSynchronousShadowStateUpdatesEnabled:newHeaderSubviewProps.synchronousShadowStateUpdatesEnabled];
[super updateProps:props oldProps:oldProps];
}
+ (react::ComponentDescriptorProvider)componentDescriptorProvider
{
return react::concreteComponentDescriptorProvider<react::RNSScreenStackHeaderSubviewComponentDescriptor>();
}
// System layouts the subviews.
RNS_IGNORE_SUPER_CALL_BEGIN
- (void)updateLayoutMetrics:(const react::LayoutMetrics &)layoutMetrics
oldLayoutMetrics:(const react::LayoutMetrics &)oldLayoutMetrics
{
CGRect frame = RCTCGRectFromRect(layoutMetrics.frame);
// CALayer will crash if we pass NaN or Inf values.
// It's unclear how to detect this case on cross-platform manner holistically, so we have to do it on the mounting
// layer as well. NaN/Inf is a kinda valid result of some math operations. Even if we can (and should) detect (and
// report early) incorrect (NaN and Inf) values which come from JavaScript side, we sometimes cannot backtrace the
// sources of a calculation that produced an incorrect/useless result.
if (!std::isfinite(frame.size.width) || !std::isfinite(frame.size.height)) {
RCTLogWarn(
@"-[UIView(ComponentViewProtocol) updateLayoutMetrics:oldLayoutMetrics:]: Received invalid layout metrics (%@) for a view (%@).",
NSStringFromCGRect(frame),
self);
} else {
#if RNS_IPHONE_OS_VERSION_AVAILABLE(26_0)
if (self.needsAutoLayout) {
BOOL sizeHasChanged = _layoutMetrics.frame.size != layoutMetrics.frame.size;
_layoutMetrics = layoutMetrics;
if (sizeHasChanged) {
[self invalidateIntrinsicContentSize];
}
} else
#endif // RNS_IPHONE_OS_VERSION_AVAILABLE(26_0)
{
self.bounds = CGRect{CGPointZero, frame.size};
}
[self layoutNavigationBar];
}
}
RNS_IGNORE_SUPER_CALL_END
+ (BOOL)shouldBeRecycled
{
return NO;
}
- (void)updateState:(const facebook::react::State::Shared &)state
oldState:(const facebook::react::State::Shared &)oldState
{
_state = std::static_pointer_cast<const react::RNSScreenStackHeaderSubviewShadowNode::ConcreteState>(state);
}
#else // RCT_NEW_ARCH_ENABLED
#pragma mark - Paper specific
- (void)reactSetFrame:(CGRect)frame
{
// Block any attempt to set coordinates on RNSScreenStackHeaderSubview. This
// makes UINavigationBar the only one to control the position of header content.
if (!CGSizeEqualToSize(frame.size, self.frame.size)) {
#if RNS_IPHONE_OS_VERSION_AVAILABLE(26_0)
if (self.needsAutoLayout) {
_lastReactFrameSize = frame.size;
[self invalidateIntrinsicContentSize];
} else
#endif // RNS_IPHONE_OS_VERSION_AVAILABLE(26_0)
{
[super reactSetFrame:CGRectMake(0, 0, frame.size.width, frame.size.height)];
}
[self layoutNavigationBar];
}
}
#endif // RCT_NEW_ARCH_ENABLED
#if RNS_IPHONE_OS_VERSION_AVAILABLE(26_0)
// Starting from iOS 26, to center left and right subviews inside liquid glass backdrop,
// we need to use auto layout. To make Yoga's layout work with auto layout, we pass information
// from Yoga via `intrinsicContentSize`.
- (BOOL)needsAutoLayout
{
BOOL needsAutoLayout = NO;
if (@available(iOS 26.0, *)) {
needsAutoLayout = _type == RNSScreenStackHeaderSubviewTypeLeft || _type == RNSScreenStackHeaderSubviewTypeRight;
}
return needsAutoLayout;
}
#endif // RNS_IPHONE_OS_VERSION_AVAILABLE(26_0)
#pragma mark - UIBarButtonItem specific
- (UIBarButtonItem *)getUIBarButtonItem
{
RCTAssert(
_type == RNSScreenStackHeaderSubviewTypeLeft || _type == RNSScreenStackHeaderSubviewTypeRight,
@"[RNScreens] Unexpected subview type.");
if (_barButtonItem == nil) {
#if RNS_IPHONE_OS_VERSION_AVAILABLE(26_0)
if (@available(iOS 26.0, *)) {
// Starting from iOS 26, UIBarButtonItem's customView is streched to have at least 36 width.
// Stretching RNSScreenStackHeaderSubview means that its subviews are aligned to left instead
// of the center. To mitigate this, we add a wrapper view that will center
// RNSScreenStackHeaderSubview inside of itself.
UIView *wrapperView = [UIView new];
wrapperView.translatesAutoresizingMaskIntoConstraints = NO;
self.translatesAutoresizingMaskIntoConstraints = NO;
[wrapperView addSubview:self];
[self.centerXAnchor constraintEqualToAnchor:wrapperView.centerXAnchor].active = YES;
[self.centerYAnchor constraintEqualToAnchor:wrapperView.centerYAnchor].active = YES;
// To prevent UIKit from stretching subviews to all available width, we need to:
// 1. Set width of wrapperView to match RNSScreenStackHeaderSubview BUT when
// RNSScreenStackHeaderSubview's width is smaller that minimal required 36 width, it breaks
// UIKit's constraint. That's why we need to lower the priority of the constraint.
NSLayoutConstraint *widthEqual = [wrapperView.widthAnchor constraintEqualToAnchor:self.widthAnchor];
widthEqual.priority = UILayoutPriorityDefaultHigh;
widthEqual.active = YES;
NSLayoutConstraint *heightEqual = [wrapperView.heightAnchor constraintEqualToAnchor:self.heightAnchor];
heightEqual.priority = UILayoutPriorityDefaultHigh;
heightEqual.active = YES;
// 2. Set content hugging priority for RNSScreenStackHeaderSubview.
[self setContentHuggingPriority:UILayoutPriorityRequired forAxis:UILayoutConstraintAxisHorizontal];
[self setContentHuggingPriority:UILayoutPriorityRequired forAxis:UILayoutConstraintAxisVertical];
// 3. Set compression resistance to prevent UIKit from shrinking the subview below its intrinsic size.
[self setContentCompressionResistancePriority:UILayoutPriorityRequired forAxis:UILayoutConstraintAxisVertical];
[self setContentCompressionResistancePriority:UILayoutPriorityRequired forAxis:UILayoutConstraintAxisHorizontal];
_barButtonItem = [[UIBarButtonItem alloc] initWithCustomView:wrapperView];
} else
#endif // RNS_IPHONE_OS_VERSION_AVAILABLE(26_0)
{
_barButtonItem = [[UIBarButtonItem alloc] initWithCustomView:self];
}
[self configureBarButtonItem];
}
return _barButtonItem;
}
#if RNS_IPHONE_OS_VERSION_AVAILABLE(26_0)
- (CGSize)intrinsicContentSize
{
#if RCT_NEW_ARCH_ENABLED
return RCTCGSizeFromSize(_layoutMetrics.frame.size);
#else // RCT_NEW_ARCH_ENABLED
return _lastReactFrameSize;
#endif // RCT_NEW_ARCH_ENABLED
}
#endif // RNS_IPHONE_OS_VERSION_AVAILABLE(26_0)
- (void)configureBarButtonItem
{
#if RNS_IPHONE_OS_VERSION_AVAILABLE(26_0)
if (@available(iOS 26.0, *)) {
if (_barButtonItem != nil) {
[_barButtonItem setHidesSharedBackground:_hidesSharedBackground];
}
}
#endif // RNS_IPHONE_OS_VERSION_AVAILABLE(26_0)
}
- (void)setHidesSharedBackground:(BOOL)hidesSharedBackground
{
_hidesSharedBackground = hidesSharedBackground;
[self configureBarButtonItem];
}
@end
@implementation RNSScreenStackHeaderSubviewManager
RCT_EXPORT_MODULE()
RCT_EXPORT_VIEW_PROPERTY(type, RNSScreenStackHeaderSubviewType)
RCT_EXPORT_VIEW_PROPERTY(hidesSharedBackground, BOOL)
#ifdef RCT_NEW_ARCH_ENABLED
#else
- (UIView *)view
{
return [RNSScreenStackHeaderSubview new];
}
#endif
@end
#ifdef RCT_NEW_ARCH_ENABLED
Class<RCTComponentViewProtocol> RNSScreenStackHeaderSubviewCls(void)
{
return RNSScreenStackHeaderSubview.class;
}
#endif
@implementation RCTConvert (RNSScreenStackHeaderSubview)
RCT_ENUM_CONVERTER(
RNSScreenStackHeaderSubviewType,
(@{
@"back" : @(RNSScreenStackHeaderSubviewTypeBackButton),
@"left" : @(RNSScreenStackHeaderSubviewTypeLeft),
@"right" : @(RNSScreenStackHeaderSubviewTypeRight),
@"title" : @(RNSScreenStackHeaderSubviewTypeTitle),
@"center" : @(RNSScreenStackHeaderSubviewTypeCenter),
@"searchBar" : @(RNSScreenStackHeaderSubviewTypeSearchBar),
}),
RNSScreenStackHeaderSubviewTypeTitle,
integerValue)
@end

View File

@@ -0,0 +1,28 @@
#pragma once
#import "RNSScreen.h"
@interface RNSScreenWindowTraits : NSObject
+ (void)updateWindowTraits;
#if !TARGET_OS_TV && !TARGET_OS_VISION
+ (void)assertViewControllerBasedStatusBarAppearenceSet;
#endif
+ (void)updateStatusBarAppearance;
+ (void)enforceDesiredDeviceOrientation;
+ (void)updateHomeIndicatorAutoHidden;
#if !TARGET_OS_TV
+ (UIStatusBarStyle)statusBarStyleForRNSStatusBarStyle:(RNSStatusBarStyle)statusBarStyle;
+ (UIInterfaceOrientation)defaultOrientationForOrientationMask:(UIInterfaceOrientationMask)orientationMask;
+ (UIInterfaceOrientation)interfaceOrientationFromDeviceOrientation:(UIDeviceOrientation)deviceOrientation;
+ (UIInterfaceOrientationMask)maskFromOrientation:(UIInterfaceOrientation)orientation;
#endif
+ (BOOL)shouldAskScreensForTrait:(RNSWindowTrait)trait
includingModals:(BOOL)includingModals
inViewController:(UIViewController *)vc;
+ (BOOL)shouldAskScreensForScreenOrientationInViewController:(UIViewController *)vc;
@end

View File

@@ -0,0 +1,221 @@
#import "RNSScreenWindowTraits.h"
#import "RNSScreenContainer.h"
#import "RNSScreenStack.h"
@implementation RNSScreenWindowTraits
#if !TARGET_OS_TV && !TARGET_OS_VISION
+ (void)assertViewControllerBasedStatusBarAppearenceSet
{
static dispatch_once_t once;
static bool viewControllerBasedAppearence;
dispatch_once(&once, ^{
viewControllerBasedAppearence =
[[[NSBundle mainBundle] objectForInfoDictionaryKey:@"UIViewControllerBasedStatusBarAppearance"] boolValue];
});
if (!viewControllerBasedAppearence) {
RCTLogError(@"If you want to change the appearance of status bar, you have to change \
UIViewControllerBasedStatusBarAppearance key in the Info.plist to YES");
}
}
#endif
+ (void)updateStatusBarAppearance
{
#if !TARGET_OS_TV && !TARGET_OS_VISION
[UIView animateWithDuration:0.4
animations:^{ // duration based on "Programming iOS 13" p. 311 implementation
[RCTKeyWindow().rootViewController setNeedsStatusBarAppearanceUpdate];
}];
#endif
}
+ (void)updateHomeIndicatorAutoHidden
{
#if !TARGET_OS_TV
[RCTKeyWindow().rootViewController setNeedsUpdateOfHomeIndicatorAutoHidden];
#endif
}
#if !TARGET_OS_TV
+ (UIStatusBarStyle)statusBarStyleForRNSStatusBarStyle:(RNSStatusBarStyle)statusBarStyle
{
switch (statusBarStyle) {
case RNSStatusBarStyleAuto:
return [UITraitCollection.currentTraitCollection userInterfaceStyle] == UIUserInterfaceStyleDark
? UIStatusBarStyleLightContent
: UIStatusBarStyleDarkContent;
case RNSStatusBarStyleInverted:
return [UITraitCollection.currentTraitCollection userInterfaceStyle] == UIUserInterfaceStyleDark
? UIStatusBarStyleDarkContent
: UIStatusBarStyleLightContent;
case RNSStatusBarStyleLight:
return UIStatusBarStyleLightContent;
case RNSStatusBarStyleDark:
return UIStatusBarStyleDarkContent;
default:
return UIStatusBarStyleLightContent;
}
}
#endif
#if !TARGET_OS_TV
+ (UIInterfaceOrientation)defaultOrientationForOrientationMask:(UIInterfaceOrientationMask)orientationMask
{
if (UIInterfaceOrientationMaskPortrait & orientationMask) {
return UIInterfaceOrientationPortrait;
} else if (UIInterfaceOrientationMaskLandscapeLeft & orientationMask) {
return UIInterfaceOrientationLandscapeLeft;
} else if (UIInterfaceOrientationMaskLandscapeRight & orientationMask) {
return UIInterfaceOrientationLandscapeRight;
} else if (UIInterfaceOrientationMaskPortraitUpsideDown & orientationMask) {
return UIInterfaceOrientationPortraitUpsideDown;
}
return UIInterfaceOrientationUnknown;
}
+ (UIInterfaceOrientation)interfaceOrientationFromDeviceOrientation:(UIDeviceOrientation)deviceOrientation
{
switch (deviceOrientation) {
case UIDeviceOrientationPortrait:
return UIInterfaceOrientationPortrait;
case UIDeviceOrientationPortraitUpsideDown:
return UIInterfaceOrientationPortraitUpsideDown;
// UIDevice and UIInterface landscape orientations are switched
case UIDeviceOrientationLandscapeLeft:
return UIInterfaceOrientationLandscapeRight;
case UIDeviceOrientationLandscapeRight:
return UIInterfaceOrientationLandscapeLeft;
default:
return UIInterfaceOrientationUnknown;
}
}
+ (UIInterfaceOrientationMask)maskFromOrientation:(UIInterfaceOrientation)orientation
{
return 1 << orientation;
}
#endif
+ (void)enforceDesiredDeviceOrientation
{
#if !TARGET_OS_TV && !TARGET_OS_VISION
dispatch_async(dispatch_get_main_queue(), ^{
UIInterfaceOrientationMask orientationMask = [RCTKeyWindow().rootViewController supportedInterfaceOrientations];
UIInterfaceOrientation currentDeviceOrientation =
[RNSScreenWindowTraits interfaceOrientationFromDeviceOrientation:[[UIDevice currentDevice] orientation]];
UIInterfaceOrientation currentInterfaceOrientation = [RNSScreenWindowTraits interfaceOrientation];
UIInterfaceOrientation newOrientation = UIInterfaceOrientationUnknown;
if ([RNSScreenWindowTraits maskFromOrientation:currentDeviceOrientation] & orientationMask) {
if (!([RNSScreenWindowTraits maskFromOrientation:currentInterfaceOrientation] & orientationMask)) {
// if the device orientation is in the mask, but interface orientation is not, we rotate to device's orientation
newOrientation = currentDeviceOrientation;
} else {
if (currentDeviceOrientation != currentInterfaceOrientation) {
// if both device orientation and interface orientation are in the mask, but in different orientations, we
// rotate to device's orientation
newOrientation = currentDeviceOrientation;
}
}
} else {
if (!([RNSScreenWindowTraits maskFromOrientation:currentInterfaceOrientation] & orientationMask)) {
// if both device orientation and interface orientation are not in the mask, we rotate to closest available
// rotation from mask
newOrientation = [RNSScreenWindowTraits defaultOrientationForOrientationMask:orientationMask];
} else {
// if the device orientation is not in the mask, but interface orientation is in the mask, do nothing
}
}
if (newOrientation != UIInterfaceOrientationUnknown) {
#if RNS_IPHONE_OS_VERSION_AVAILABLE(16_0)
if (@available(iOS 16.0, *)) {
NSArray *array = [[[UIApplication sharedApplication] connectedScenes] allObjects];
// when an app supports multiple scenes (e.g. CarPlay), it is possible that
// UIWindowScene is not the first scene, or it may not be present at all
UIWindowScene *scene = nil;
for (id connectedScene in array) {
if ([connectedScene isKindOfClass:[UIWindowScene class]]) {
scene = connectedScene;
break;
}
}
if (scene == nil) {
return;
}
UIWindowSceneGeometryPreferencesIOS *geometryPreferences =
[[UIWindowSceneGeometryPreferencesIOS alloc] initWithInterfaceOrientations:orientationMask];
[scene requestGeometryUpdateWithPreferences:geometryPreferences
errorHandler:^(NSError *_Nonnull error){
}];
// `attemptRotationToDeviceOrientation` is deprecated for modern OS versions
// so we need to use `setNeedsUpdateOfSupportedInterfaceOrientations`
UIViewController *topController = [UIApplication sharedApplication].keyWindow.rootViewController;
while (topController.presentedViewController) {
topController = topController.presentedViewController;
}
[topController setNeedsUpdateOfSupportedInterfaceOrientations];
} else
#endif // Check for iOS 16
{
[[UIDevice currentDevice] setValue:@(newOrientation) forKey:@"orientation"];
[UIViewController attemptRotationToDeviceOrientation];
}
}
});
#endif // !TARGET_TV_OS
}
+ (void)updateWindowTraits
{
[RNSScreenWindowTraits updateStatusBarAppearance];
[RNSScreenWindowTraits enforceDesiredDeviceOrientation];
[RNSScreenWindowTraits updateHomeIndicatorAutoHidden];
}
#if !TARGET_OS_TV && !TARGET_OS_VISION
// based on
// https://stackoverflow.com/questions/57965701/statusbarorientation-was-deprecated-in-ios-13-0-when-attempting-to-get-app-ori/61249908#61249908
+ (UIInterfaceOrientation)interfaceOrientation
{
UIWindowScene *windowScene = RCTKeyWindow().windowScene;
if (windowScene == nil) {
return UIInterfaceOrientationUnknown;
}
return windowScene.interfaceOrientation;
}
#endif
// method to be used in Expo for checking if RNScreens have trait set
+ (BOOL)shouldAskScreensForTrait:(RNSWindowTrait)trait
includingModals:(BOOL)includingModals
inViewController:(UIViewController *)vc
{
UIViewController *lastViewController = [[vc childViewControllers] lastObject];
if ([lastViewController conformsToProtocol:@protocol(RNSViewControllerDelegate)]) {
UIViewController *vc = nil;
if ([lastViewController isKindOfClass:[RNSViewController class]]) {
vc = [(RNSViewController *)lastViewController findActiveChildVC];
} else if ([lastViewController isKindOfClass:[RNSNavigationController class]]) {
vc = [(RNSNavigationController *)lastViewController topViewController];
}
return [vc isKindOfClass:[RNSScreen class]] &&
[(RNSScreen *)vc findChildVCForConfigAndTrait:trait includingModals:includingModals] != nil;
}
return NO;
}
// same method as above, but directly for orientation
+ (BOOL)shouldAskScreensForScreenOrientationInViewController:(UIViewController *)vc
{
return [RNSScreenWindowTraits shouldAskScreensForTrait:RNSWindowTraitOrientation
includingModals:YES
inViewController:vc];
}
@end

View File

@@ -0,0 +1,26 @@
#pragma once
#include <UIKit/UIKit.h>
NS_ASSUME_NONNULL_BEGIN
/**
* Views that require ScrollView contentInsetAdjustmentBehavior overriding should conform to this protocol.
*/
@protocol RNSScrollViewBehaviorOverriding
/**
* Returns whether view should override contentInsetAdjustmentBehavior for first ScrollView in first descendant chain.
* It can be a property or method involving some logic to determine if ScrollView's behavior should be overriden.
*/
- (BOOL)shouldOverrideScrollViewContentInsetAdjustmentBehavior;
/**
* Overrides contentInsetAdjustmentBehavior for first ScrollView in first descendant chain
* if overrideScrollViewContentInsetAdjustmentBehavior returns true.
*/
- (void)overrideScrollViewBehaviorInFirstDescendantChainIfNeeded;
@end
NS_ASSUME_NONNULL_END

47
node_modules/react-native-screens/ios/RNSSearchBar.h generated vendored Normal file
View File

@@ -0,0 +1,47 @@
#pragma once
#import <UIKit/UIKit.h>
#ifdef RCT_NEW_ARCH_ENABLED
#import <React/RCTViewComponentView.h>
#import <react/renderer/components/rnscreens/RCTComponentViewHelpers.h>
#endif
#import <React/RCTBridge.h>
#import <React/RCTComponent.h>
#import <React/RCTViewManager.h>
#import "RNSDefines.h"
#import "RNSEnums.h"
@interface RNSSearchBar :
#ifdef RCT_NEW_ARCH_ENABLED
RCTViewComponentView <UISearchBarDelegate, RCTRNSSearchBarViewProtocol>
#else
UIView <UISearchBarDelegate>
#endif
@property (nonatomic) BOOL hideWhenScrolling;
@property (nonatomic) RNSSearchBarPlacement placement;
@property (nonatomic) BOOL allowToolbarIntegration;
@property (nonatomic, retain) UISearchController *controller;
#if RNS_IPHONE_OS_VERSION_AVAILABLE(16_0) && !TARGET_OS_TV
- (UINavigationItemSearchBarPlacement)placementAsUINavigationItemSearchBarPlacement API_AVAILABLE(ios(16.0))
API_UNAVAILABLE(tvos, watchos);
#endif // Check for iOS >= 16 && !TARGET_OS_TV
#ifdef RCT_NEW_ARCH_ENABLED
#else
@property (nonatomic, copy) RCTDirectEventBlock onChangeText;
@property (nonatomic, copy) RCTDirectEventBlock onCancelButtonPress;
@property (nonatomic, copy) RCTDirectEventBlock onSearchButtonPress;
@property (nonatomic, copy) RCTDirectEventBlock onSearchFocus;
@property (nonatomic, copy) RCTDirectEventBlock onSearchBlur;
#endif
@end
@interface RNSSearchBarManager : RCTViewManager
@end

590
node_modules/react-native-screens/ios/RNSSearchBar.mm generated vendored Normal file
View File

@@ -0,0 +1,590 @@
#import <UIKit/UIKit.h>
#import "RNSDefines.h"
#import "RNSSearchBar.h"
#import <React/RCTBridge.h>
#import <React/RCTComponent.h>
#import <React/RCTUIManager.h>
#ifdef RCT_NEW_ARCH_ENABLED
#import <React/RCTConversions.h>
#import <React/RCTFabricComponentsPlugins.h>
#import <react/renderer/components/rnscreens/ComponentDescriptors.h>
#import <react/renderer/components/rnscreens/EventEmitters.h>
#import <react/renderer/components/rnscreens/Props.h>
#import "RNSConvert.h"
#endif // RCT_NEW_ARCH_ENABLED
#ifdef RCT_NEW_ARCH_ENABLED
namespace react = facebook::react;
#endif // RCT_NEW_ARCH_ENABLED
@implementation RNSSearchBar {
__weak RCTBridge *_bridge;
UISearchController *_controller;
UIColor *_textColor;
// We use those booleans to log a warning if user attempts to restore
// default behavior after setting explicit value for the prop.
BOOL _isObscureBackgroundSet;
BOOL _isHideNavigationBarSet;
}
@synthesize controller = _controller;
- (instancetype)initWithBridge:(RCTBridge *)bridge
{
if (self = [super init]) {
_bridge = bridge;
[self initCommonProps];
}
return self;
}
#ifdef RCT_NEW_ARCH_ENABLED
// Needed because of this: https://github.com/facebook/react-native/pull/37274
+ (void)load
{
[super load];
}
- (instancetype)init
{
if (self = [super init]) {
static const auto defaultProps = std::make_shared<const react::RNSSearchBarProps>();
_props = defaultProps;
[self initCommonProps];
}
return self;
}
#endif
- (void)initCommonProps
{
#if !TARGET_OS_TV
_controller = [[UISearchController alloc] initWithSearchResultsController:nil];
#else
// on TVOS UISearchController must contain searchResultsController.
_controller = [[UISearchController alloc] initWithSearchResultsController:[UIViewController new]];
#endif
_controller.searchBar.delegate = self;
_isObscureBackgroundSet = NO;
_isHideNavigationBarSet = NO;
_hideWhenScrolling = YES;
_placement = RNSSearchBarPlacementAutomatic;
_allowToolbarIntegration = YES;
}
- (void)emitOnFocusEvent
{
#ifdef RCT_NEW_ARCH_ENABLED
if (_eventEmitter != nullptr) {
std::dynamic_pointer_cast<const react::RNSSearchBarEventEmitter>(_eventEmitter)
->onSearchFocus(react::RNSSearchBarEventEmitter::OnSearchFocus{});
}
#else
if (self.onSearchFocus) {
self.onSearchFocus(@{});
}
#endif
}
- (void)emitOnBlurEvent
{
#ifdef RCT_NEW_ARCH_ENABLED
if (_eventEmitter != nullptr) {
std::dynamic_pointer_cast<const react::RNSSearchBarEventEmitter>(_eventEmitter)
->onSearchBlur(react::RNSSearchBarEventEmitter::OnSearchBlur{});
}
#else
if (self.onSearchBlur) {
self.onSearchBlur(@{});
}
#endif
}
- (void)emitOnSearchButtonPressEventWithText:(NSString *)text
{
#ifdef RCT_NEW_ARCH_ENABLED
if (_eventEmitter != nullptr) {
std::dynamic_pointer_cast<const react::RNSSearchBarEventEmitter>(_eventEmitter)
->onSearchButtonPress(
react::RNSSearchBarEventEmitter::OnSearchButtonPress{.text = RCTStringFromNSString(text)});
}
#else
if (self.onSearchButtonPress) {
self.onSearchButtonPress(@{
@"text" : text,
});
}
#endif
}
- (void)emitOnCancelButtonPressEvent
{
#ifdef RCT_NEW_ARCH_ENABLED
if (_eventEmitter != nullptr) {
std::dynamic_pointer_cast<const react::RNSSearchBarEventEmitter>(_eventEmitter)
->onCancelButtonPress(react::RNSSearchBarEventEmitter::OnCancelButtonPress{});
}
#else
if (self.onCancelButtonPress) {
self.onCancelButtonPress(@{});
}
#endif
}
- (void)emitOnChangeTextEventWithText:(NSString *)text
{
#ifdef RCT_NEW_ARCH_ENABLED
if (_eventEmitter != nullptr) {
std::dynamic_pointer_cast<const react::RNSSearchBarEventEmitter>(_eventEmitter)
->onChangeText(react::RNSSearchBarEventEmitter::OnChangeText{.text = RCTStringFromNSString(text)});
}
#else
if (self.onChangeText) {
self.onChangeText(@{
@"text" : text,
});
}
#endif
}
- (void)setObscureBackground:(RNSOptionalBoolean)obscureBackground
{
switch (obscureBackground) {
case RNSOptionalBooleanTrue:
[_controller setObscuresBackgroundDuringPresentation:YES];
_isObscureBackgroundSet = YES;
break;
case RNSOptionalBooleanFalse:
[_controller setObscuresBackgroundDuringPresentation:NO];
_isObscureBackgroundSet = YES;
break;
default:
if (_isObscureBackgroundSet) {
RCTLogWarn(@"[RNScreens] Dynamically restoring obscureBackground to default native behavior is unsupported.");
}
break;
}
}
- (void)setHideNavigationBar:(RNSOptionalBoolean)hideNavigationBar
{
switch (hideNavigationBar) {
case RNSOptionalBooleanTrue:
[_controller setHidesNavigationBarDuringPresentation:YES];
_isHideNavigationBarSet = YES;
break;
case RNSOptionalBooleanFalse:
[_controller setHidesNavigationBarDuringPresentation:NO];
_isHideNavigationBarSet = YES;
break;
default:
if (_isHideNavigationBarSet) {
RCTLogWarn(@"[RNScreens] Dynamically restoring hideNavigationBar to default native behavior is unsupported.");
}
break;
}
}
- (void)setHideWhenScrolling:(BOOL)hideWhenScrolling
{
_hideWhenScrolling = hideWhenScrolling;
}
- (void)setAutoCapitalize:(UITextAutocapitalizationType)autoCapitalize
{
[_controller.searchBar setAutocapitalizationType:autoCapitalize];
}
- (void)setPlaceholder:(NSString *)placeholder
{
[_controller.searchBar setPlaceholder:placeholder];
}
- (void)setBarTintColor:(UIColor *)barTintColor
{
#if !TARGET_OS_TV
[_controller.searchBar.searchTextField setBackgroundColor:barTintColor];
#endif
}
- (void)setTintColor:(UIColor *)tintColor
{
[_controller.searchBar setTintColor:tintColor];
}
- (void)setTextColor:(UIColor *)textColor
{
#if !TARGET_OS_TV
_textColor = textColor;
[_controller.searchBar.searchTextField setTextColor:_textColor];
#endif
}
- (void)setCancelButtonText:(NSString *)text
{
[_controller.searchBar setValue:text forKey:@"cancelButtonText"];
}
- (void)hideCancelButton
{
#if !TARGET_OS_TV
if (!_controller.automaticallyShowsCancelButton) {
[_controller.searchBar setShowsCancelButton:NO animated:YES];
} else {
// On iOS 13+ UISearchController automatically shows/hides cancel button
// https://developer.apple.com/documentation/uikit/uisearchcontroller/3152926-automaticallyshowscancelbutton?language=objc
}
#endif
}
- (void)showCancelButton
{
#if !TARGET_OS_TV
if (!_controller.automaticallyShowsCancelButton) {
[_controller.searchBar setShowsCancelButton:YES animated:YES];
} else {
// On iOS 13+ UISearchController automatically shows/hides cancel button
// https://developer.apple.com/documentation/uikit/uisearchcontroller/3152926-automaticallyshowscancelbutton?language=objc
}
#endif
}
#if RNS_IPHONE_OS_VERSION_AVAILABLE(16_0) && !TARGET_OS_TV
- (UINavigationItemSearchBarPlacement)placementAsUINavigationItemSearchBarPlacement API_AVAILABLE(ios(16.0))
API_UNAVAILABLE(tvos, watchos)
{
switch (_placement) {
case RNSSearchBarPlacementStacked:
return UINavigationItemSearchBarPlacementStacked;
case RNSSearchBarPlacementAutomatic:
return UINavigationItemSearchBarPlacementAutomatic;
case RNSSearchBarPlacementInline:
return UINavigationItemSearchBarPlacementInline;
#if RNS_IPHONE_OS_VERSION_AVAILABLE(26_0)
case RNSSearchBarPlacementIntegrated:
if (@available(iOS 26, *)) {
return UINavigationItemSearchBarPlacementIntegrated;
} else {
return UINavigationItemSearchBarPlacementInline;
}
case RNSSearchBarPlacementIntegratedButton:
if (@available(iOS 26, *)) {
return UINavigationItemSearchBarPlacementIntegratedButton;
} else {
return UINavigationItemSearchBarPlacementInline;
}
case RNSSearchBarPlacementIntegratedCentered:
if (@available(iOS 26, *)) {
return UINavigationItemSearchBarPlacementIntegratedCentered;
} else {
return UINavigationItemSearchBarPlacementInline;
}
#else // Check for iOS >= 26
case RNSSearchBarPlacementIntegrated:
case RNSSearchBarPlacementIntegratedButton:
case RNSSearchBarPlacementIntegratedCentered:
return UINavigationItemSearchBarPlacementInline;
#endif // Check for iOS >= 26
default:
RCTLogError(@"[RNScreens] unsupported search bar placement");
return UINavigationItemSearchBarPlacementStacked;
}
}
#endif // Check for iOS >= 16 && !TARGET_OS_TV
#pragma mark delegate methods
- (void)searchBarTextDidBeginEditing:(UISearchBar *)searchBar
{
#if !TARGET_OS_TV
// for some reason, the color does not change when set at the beginning,
// so we apply it again here
if (_textColor != nil) {
[_controller.searchBar.searchTextField setTextColor:_textColor];
}
#endif
[self showCancelButton];
[self becomeFirstResponder];
[self emitOnFocusEvent];
}
- (void)searchBarTextDidEndEditing:(UISearchBar *)searchBar
{
[self emitOnBlurEvent];
[self hideCancelButton];
}
- (void)searchBar:(UISearchBar *)searchBar textDidChange:(NSString *)searchText
{
[self emitOnChangeTextEventWithText:_controller.searchBar.text];
}
- (void)searchBarSearchButtonClicked:(UISearchBar *)searchBar
{
[self emitOnSearchButtonPressEventWithText:_controller.searchBar.text];
}
#if !TARGET_OS_TV
- (void)searchBarCancelButtonClicked:(UISearchBar *)searchBar
{
_controller.searchBar.text = @"";
[self resignFirstResponder];
[self hideCancelButton];
[self emitOnCancelButtonPressEvent];
[self emitOnChangeTextEventWithText:_controller.searchBar.text];
}
#endif // !TARGET_OS_TV
- (void)blur
{
[_controller.searchBar resignFirstResponder];
}
- (void)focus
{
[_controller.searchBar becomeFirstResponder];
}
- (void)clearText
{
[_controller.searchBar setText:@""];
}
- (void)toggleCancelButton:(BOOL)flag
{
#if !TARGET_OS_TV
[_controller.searchBar setShowsCancelButton:flag animated:YES];
#endif
}
- (void)setText:(NSString *)text
{
[_controller.searchBar setText:text];
}
- (void)cancelSearch
{
#if !TARGET_OS_TV
[self searchBarCancelButtonClicked:_controller.searchBar];
_controller.active = NO;
#endif
}
#pragma mark-- Fabric specific
#ifdef RCT_NEW_ARCH_ENABLED
- (void)updateProps:(react::Props::Shared const &)props oldProps:(react::Props::Shared const &)oldProps
{
const auto &oldScreenProps = *std::static_pointer_cast<const react::RNSSearchBarProps>(_props);
const auto &newScreenProps = *std::static_pointer_cast<const react::RNSSearchBarProps>(props);
if (oldScreenProps.hideWhenScrolling != newScreenProps.hideWhenScrolling) {
[self setHideWhenScrolling:newScreenProps.hideWhenScrolling];
}
if (oldScreenProps.cancelButtonText != newScreenProps.cancelButtonText) {
[self setCancelButtonText:RCTNSStringFromStringNilIfEmpty(newScreenProps.cancelButtonText)];
}
if (oldScreenProps.obscureBackground != newScreenProps.obscureBackground) {
[self
setObscureBackground:[RNSConvert
RNSOptionalBooleanFromRNSSearchBarObscureBackground:newScreenProps.obscureBackground]];
}
if (oldScreenProps.hideNavigationBar != newScreenProps.hideNavigationBar) {
[self
setHideNavigationBar:[RNSConvert
RNSOptionalBooleanFromRNSSearchBarHideNavigationBar:newScreenProps.hideNavigationBar]];
}
if (oldScreenProps.placeholder != newScreenProps.placeholder) {
[self setPlaceholder:RCTNSStringFromStringNilIfEmpty(newScreenProps.placeholder)];
}
#if !TARGET_OS_VISION
if (oldScreenProps.autoCapitalize != newScreenProps.autoCapitalize) {
[self setAutoCapitalize:[RNSConvert UITextAutocapitalizationTypeFromCppEquivalent:newScreenProps.autoCapitalize]];
}
#endif
if (oldScreenProps.tintColor != newScreenProps.tintColor) {
[self setTintColor:RCTUIColorFromSharedColor(newScreenProps.tintColor)];
}
if (oldScreenProps.barTintColor != newScreenProps.barTintColor) {
[self setBarTintColor:RCTUIColorFromSharedColor(newScreenProps.barTintColor)];
}
if (oldScreenProps.textColor != newScreenProps.textColor) {
[self setTextColor:RCTUIColorFromSharedColor(newScreenProps.textColor)];
}
if (oldScreenProps.placement != newScreenProps.placement) {
self.placement = [RNSConvert RNSScreenSearchBarPlacementFromCppEquivalent:newScreenProps.placement];
}
if (oldScreenProps.allowToolbarIntegration != newScreenProps.allowToolbarIntegration) {
self.allowToolbarIntegration = newScreenProps.allowToolbarIntegration;
}
[super updateProps:props oldProps:oldProps];
}
+ (react::ComponentDescriptorProvider)componentDescriptorProvider
{
return react::concreteComponentDescriptorProvider<react::RNSSearchBarComponentDescriptor>();
}
- (void)handleCommand:(const NSString *)commandName args:(const NSArray *)args
{
RCTRNSSearchBarHandleCommand(self, commandName, args);
}
+ (BOOL)shouldBeRecycled
{
// Recycling RNSSearchBar causes multiple bugs on iOS 26+, resulting in search bar
// not appearing at all.
// Details: https://github.com/software-mansion/react-native-screens/pull/3168
return NO;
}
#else
#endif // RCT_NEW_ARCH_ENABLED
@end
#ifdef RCT_NEW_ARCH_ENABLED
Class<RCTComponentViewProtocol> RNSSearchBarCls(void)
{
return RNSSearchBar.class;
}
#endif
@implementation RNSSearchBarManager
RCT_EXPORT_MODULE()
#ifdef RCT_NEW_ARCH_ENABLED
#else
- (UIView *)view
{
return [[RNSSearchBar alloc] initWithBridge:self.bridge];
}
#endif
RCT_EXPORT_VIEW_PROPERTY(obscureBackground, RNSOptionalBoolean)
RCT_EXPORT_VIEW_PROPERTY(hideNavigationBar, RNSOptionalBoolean)
RCT_EXPORT_VIEW_PROPERTY(hideWhenScrolling, BOOL)
// We want to use "systemDefault" option which is not in UITextAutocapitalizationType
// but RCTConvert enum conversion already exists.
RCT_CUSTOM_VIEW_PROPERTY(autoCapitalize, UITextAutocapitalizationType, RNSSearchBar)
{
RNSSearchBar *searchBarView = static_cast<RNSSearchBar *>(view);
if ([json isKindOfClass:[NSString class]] && [static_cast<NSString *>(json) isEqualToString:@"systemDefault"]) {
[searchBarView setAutoCapitalize:UITextAutocapitalizationTypeSentences];
} else {
[searchBarView setAutoCapitalize:[RCTConvert UITextAutocapitalizationType:json]];
}
}
RCT_EXPORT_VIEW_PROPERTY(placeholder, NSString)
RCT_EXPORT_VIEW_PROPERTY(barTintColor, UIColor)
RCT_EXPORT_VIEW_PROPERTY(tintColor, UIColor)
RCT_EXPORT_VIEW_PROPERTY(textColor, UIColor)
RCT_EXPORT_VIEW_PROPERTY(cancelButtonText, NSString)
RCT_EXPORT_VIEW_PROPERTY(placement, RNSSearchBarPlacement)
RCT_EXPORT_VIEW_PROPERTY(allowToolbarIntegration, BOOL)
RCT_EXPORT_VIEW_PROPERTY(onChangeText, RCTDirectEventBlock)
RCT_EXPORT_VIEW_PROPERTY(onCancelButtonPress, RCTDirectEventBlock)
RCT_EXPORT_VIEW_PROPERTY(onSearchButtonPress, RCTDirectEventBlock)
RCT_EXPORT_VIEW_PROPERTY(onSearchFocus, RCTDirectEventBlock)
RCT_EXPORT_VIEW_PROPERTY(onSearchBlur, RCTDirectEventBlock)
#ifndef RCT_NEW_ARCH_ENABLED
RCT_EXPORT_METHOD(focus : (NSNumber *_Nonnull)reactTag)
{
[self.bridge.uiManager addUIBlock:^(__unused RCTUIManager *uiManager, NSDictionary *viewRegistry) {
RNSSearchBar *searchBar = viewRegistry[reactTag];
[searchBar focus];
}];
}
RCT_EXPORT_METHOD(blur : (NSNumber *_Nonnull)reactTag)
{
[self.bridge.uiManager addUIBlock:^(__unused RCTUIManager *uiManager, NSDictionary *viewRegistry) {
RNSSearchBar *searchBar = viewRegistry[reactTag];
[searchBar blur];
}];
}
RCT_EXPORT_METHOD(clearText : (NSNumber *_Nonnull)reactTag)
{
[self.bridge.uiManager addUIBlock:^(__unused RCTUIManager *uiManager, NSDictionary *viewRegistry) {
RNSSearchBar *searchBar = viewRegistry[reactTag];
[searchBar clearText];
}];
}
RCT_EXPORT_METHOD(toggleCancelButton : (NSNumber *_Nonnull)reactTag flag : (BOOL)flag)
{
[self.bridge.uiManager addUIBlock:^(__unused RCTUIManager *uiManager, NSDictionary *viewRegistry) {
RNSSearchBar *searchBar = viewRegistry[reactTag];
[searchBar toggleCancelButton:flag];
}];
}
RCT_EXPORT_METHOD(setText : (NSNumber *_Nonnull)reactTag text : (NSString *)text)
{
[self.bridge.uiManager addUIBlock:^(__unused RCTUIManager *uiManager, NSDictionary *viewRegistry) {
RNSSearchBar *searchBar = viewRegistry[reactTag];
[searchBar setText:text];
}];
}
RCT_EXPORT_METHOD(cancelSearch : (NSNumber *_Nonnull)reactTag)
{
[self.bridge.uiManager addUIBlock:^(__unused RCTUIManager *uiManager, NSDictionary *viewRegistry) {
RNSSearchBar *searchBar = viewRegistry[reactTag];
[searchBar cancelSearch];
}];
}
#endif /* !RCT_NEW_ARCH_ENABLED */
@end
@implementation RCTConvert (RNSScreen)
RCT_ENUM_CONVERTER(
RNSSearchBarPlacement,
(@{
@"automatic" : @(RNSSearchBarPlacementAutomatic),
@"inline" : @(RNSSearchBarPlacementInline),
@"stacked" : @(RNSSearchBarPlacementStacked),
@"integrated" : @(RNSSearchBarPlacementIntegrated),
@"integratedButton" : @(RNSSearchBarPlacementIntegratedButton),
@"integratedCentered" : @(RNSSearchBarPlacementIntegratedCentered),
}),
RNSSearchBarPlacementAutomatic,
integerValue)
@end

View File

@@ -0,0 +1,15 @@
#pragma once
#ifdef RCT_NEW_ARCH_ENABLED
#include <react/renderer/mounting/ShadowViewMutation.h>
@protocol RNSViewControllerInvalidating
- (void)invalidateController;
- (BOOL)shouldInvalidateOnMutation:(const facebook::react::ShadowViewMutation &)mutation;
@end
#endif // RCT_NEW_ARCH_ENABLED

View File

@@ -0,0 +1,17 @@
#pragma once
#ifdef RCT_NEW_ARCH_ENABLED
#import <UIKit/UIKit.h>
#import "RNSInvalidatedComponentsRegistry.h"
#import "RNSViewControllerInvalidating.h"
@interface RNSViewControllerInvalidator : NSObject
+ (void)invalidateViewIfDetached:(UIView<RNSViewControllerInvalidating> *_Nonnull)view
forRegistry:(RNSInvalidatedComponentsRegistry *_Nonnull)registry;
@end
#endif // RCT_NEW_ARCH_ENABLED

View File

@@ -0,0 +1,23 @@
#ifdef RCT_NEW_ARCH_ENABLED
#import "RNSViewControllerInvalidator.h"
#import <React/RCTAssert.h>
#import "RNSInvalidatedComponentsRegistry.h"
#import "RNSViewControllerInvalidating.h"
@implementation RNSViewControllerInvalidator
+ (void)invalidateViewIfDetached:(UIView<RNSViewControllerInvalidating> *_Nonnull)view
forRegistry:(RNSInvalidatedComponentsRegistry *_Nonnull)registry
{
if (view.window == nil) {
[view invalidateController];
} else {
[registry pushForInvalidation:view];
}
}
@end
#endif // RCT_NEW_ARCH_ENABLED

View File

@@ -0,0 +1,4 @@
//
// Use this file to import your target's public headers that you would like to
// expose to Swift.
//

View File

@@ -0,0 +1,461 @@
// !$*UTF8*$!
{
archiveVersion = 1;
classes = {
};
objectVersion = 46;
objects = {
/* Begin PBXBuildFile section */
442389EC22DF259000611BBE /* RNSScreen.m in Sources */ = {isa = PBXBuildFile; fileRef = 442389EB22DF259000611BBE /* RNSScreen.m */; };
448078F52114595900280661 /* RNSScreenContainer.m in Sources */ = {isa = PBXBuildFile; fileRef = 448078F12114595900280661 /* RNSScreenContainer.m */; };
4482D5EF22CB391800D5A5B9 /* RNSScreenStackHeaderConfig.m in Sources */ = {isa = PBXBuildFile; fileRef = 4482D5EE22CB391800D5A5B9 /* RNSScreenStackHeaderConfig.m */; };
44A67C3122C3B8B40017156F /* RNSScreenStack.m in Sources */ = {isa = PBXBuildFile; fileRef = 44A67C3022C3B8B40017156F /* RNSScreenStack.m */; };
B5C32A48220C6379000FFB8D /* RNSScreenContainer.m in Sources */ = {isa = PBXBuildFile; fileRef = 448078F12114595900280661 /* RNSScreenContainer.m */; };
/* End PBXBuildFile section */
/* Begin PBXCopyFilesBuildPhase section */
58B511D91A9E6C8500147676 /* CopyFiles */ = {
isa = PBXCopyFilesBuildPhase;
buildActionMask = 2147483647;
dstPath = "include/$(PRODUCT_NAME)";
dstSubfolderSpec = 16;
files = (
);
runOnlyForDeploymentPostprocessing = 0;
};
B5C32A4A220C6379000FFB8D /* CopyFiles */ = {
isa = PBXCopyFilesBuildPhase;
buildActionMask = 2147483647;
dstPath = "include/$(PRODUCT_NAME)";
dstSubfolderSpec = 16;
files = (
);
runOnlyForDeploymentPostprocessing = 0;
};
/* End PBXCopyFilesBuildPhase section */
/* Begin PBXFileReference section */
134814201AA4EA6300B7C361 /* libRNScreens.a */ = {isa = PBXFileReference; explicitFileType = archive.ar; includeInIndex = 0; path = libRNScreens.a; sourceTree = BUILT_PRODUCTS_DIR; };
442389EB22DF259000611BBE /* RNSScreen.m */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.objc; path = RNSScreen.m; sourceTree = "<group>"; };
448078EF2114595900280661 /* RNSScreen.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = RNSScreen.h; sourceTree = "<group>"; };
448078F02114595900280661 /* RNSScreenContainer.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = RNSScreenContainer.h; sourceTree = "<group>"; };
448078F12114595900280661 /* RNSScreenContainer.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = RNSScreenContainer.m; sourceTree = "<group>"; };
4482D5ED22CB391800D5A5B9 /* RNSScreenStackHeaderConfig.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = RNSScreenStackHeaderConfig.h; sourceTree = "<group>"; };
4482D5EE22CB391800D5A5B9 /* RNSScreenStackHeaderConfig.m */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.objc; path = RNSScreenStackHeaderConfig.m; sourceTree = "<group>"; };
44A67C2F22C3B8B40017156F /* RNSScreenStack.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = RNSScreenStack.h; sourceTree = "<group>"; };
44A67C3022C3B8B40017156F /* RNSScreenStack.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = RNSScreenStack.m; sourceTree = "<group>"; };
A3AF377826398E26003653D6 /* RNSSearchBar.m */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.objc; path = RNSSearchBar.m; sourceTree = "<group>"; };
A3AF377926398E27003653D6 /* RNSSearchBar.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = RNSSearchBar.h; sourceTree = "<group>"; };
B5C32A4F220C6379000FFB8D /* libRNScreens-tvOS.a */ = {isa = PBXFileReference; explicitFileType = archive.ar; includeInIndex = 0; path = "libRNScreens-tvOS.a"; sourceTree = BUILT_PRODUCTS_DIR; };
/* End PBXFileReference section */
/* Begin PBXFrameworksBuildPhase section */
58B511D81A9E6C8500147676 /* Frameworks */ = {
isa = PBXFrameworksBuildPhase;
buildActionMask = 2147483647;
files = (
);
runOnlyForDeploymentPostprocessing = 0;
};
B5C32A49220C6379000FFB8D /* Frameworks */ = {
isa = PBXFrameworksBuildPhase;
buildActionMask = 2147483647;
files = (
);
runOnlyForDeploymentPostprocessing = 0;
};
/* End PBXFrameworksBuildPhase section */
/* Begin PBXGroup section */
134814211AA4EA7D00B7C361 /* Products */ = {
isa = PBXGroup;
children = (
134814201AA4EA6300B7C361 /* libRNScreens.a */,
);
name = Products;
sourceTree = "<group>";
};
58B511D21A9E6C8500147676 = {
isa = PBXGroup;
children = (
A3AF377926398E27003653D6 /* RNSSearchBar.h */,
A3AF377826398E26003653D6 /* RNSSearchBar.m */,
4482D5ED22CB391800D5A5B9 /* RNSScreenStackHeaderConfig.h */,
4482D5EE22CB391800D5A5B9 /* RNSScreenStackHeaderConfig.m */,
44A67C2F22C3B8B40017156F /* RNSScreenStack.h */,
44A67C3022C3B8B40017156F /* RNSScreenStack.m */,
448078EF2114595900280661 /* RNSScreen.h */,
442389EB22DF259000611BBE /* RNSScreen.m */,
448078F02114595900280661 /* RNSScreenContainer.h */,
448078F12114595900280661 /* RNSScreenContainer.m */,
134814211AA4EA7D00B7C361 /* Products */,
B5C32A4F220C6379000FFB8D /* libRNScreens-tvOS.a */,
);
sourceTree = "<group>";
};
/* End PBXGroup section */
/* Begin PBXNativeTarget section */
58B511DA1A9E6C8500147676 /* RNScreens */ = {
isa = PBXNativeTarget;
buildConfigurationList = 58B511EF1A9E6C8500147676 /* Build configuration list for PBXNativeTarget "RNScreens" */;
buildPhases = (
58B511D71A9E6C8500147676 /* Sources */,
58B511D81A9E6C8500147676 /* Frameworks */,
58B511D91A9E6C8500147676 /* CopyFiles */,
);
buildRules = (
);
dependencies = (
);
name = RNScreens;
productName = RCTDataManager;
productReference = 134814201AA4EA6300B7C361 /* libRNScreens.a */;
productType = "com.apple.product-type.library.static";
};
B5C32A45220C6379000FFB8D /* RNScreens-tvOS */ = {
isa = PBXNativeTarget;
buildConfigurationList = B5C32A4B220C6379000FFB8D /* Build configuration list for PBXNativeTarget "RNScreens-tvOS" */;
buildPhases = (
B5C32A46220C6379000FFB8D /* Sources */,
B5C32A49220C6379000FFB8D /* Frameworks */,
B5C32A4A220C6379000FFB8D /* CopyFiles */,
);
buildRules = (
);
dependencies = (
);
name = "RNScreens-tvOS";
productName = RCTDataManager;
productReference = B5C32A4F220C6379000FFB8D /* libRNScreens-tvOS.a */;
productType = "com.apple.product-type.library.static";
};
/* End PBXNativeTarget section */
/* Begin PBXProject section */
58B511D31A9E6C8500147676 /* Project object */ = {
isa = PBXProject;
attributes = {
LastUpgradeCheck = 920;
ORGANIZATIONNAME = Facebook;
TargetAttributes = {
58B511DA1A9E6C8500147676 = {
CreatedOnToolsVersion = 6.1.1;
};
};
};
buildConfigurationList = 58B511D61A9E6C8500147676 /* Build configuration list for PBXProject "RNScreens" */;
compatibilityVersion = "Xcode 3.2";
developmentRegion = English;
hasScannedForEncodings = 0;
knownRegions = (
English,
en,
);
mainGroup = 58B511D21A9E6C8500147676;
productRefGroup = 58B511D21A9E6C8500147676;
projectDirPath = "";
projectRoot = "";
targets = (
58B511DA1A9E6C8500147676 /* RNScreens */,
B5C32A45220C6379000FFB8D /* RNScreens-tvOS */,
);
};
/* End PBXProject section */
/* Begin PBXSourcesBuildPhase section */
58B511D71A9E6C8500147676 /* Sources */ = {
isa = PBXSourcesBuildPhase;
buildActionMask = 2147483647;
files = (
442389EC22DF259000611BBE /* RNSScreen.m in Sources */,
448078F52114595900280661 /* RNSScreenContainer.m in Sources */,
44A67C3122C3B8B40017156F /* RNSScreenStack.m in Sources */,
4482D5EF22CB391800D5A5B9 /* RNSScreenStackHeaderConfig.m in Sources */,
);
runOnlyForDeploymentPostprocessing = 0;
};
B5C32A46220C6379000FFB8D /* Sources */ = {
isa = PBXSourcesBuildPhase;
buildActionMask = 2147483647;
files = (
B5C32A48220C6379000FFB8D /* RNSScreenContainer.m in Sources */,
);
runOnlyForDeploymentPostprocessing = 0;
};
/* End PBXSourcesBuildPhase section */
/* Begin XCBuildConfiguration section */
0CE596A6BAEE45CA860361AD /* Testflight */ = {
isa = XCBuildConfiguration;
buildSettings = {
HEADER_SEARCH_PATHS = (
"$(inherited)",
/Applications/Xcode.app/Contents/Developer/Toolchains/XcodeDefault.xctoolchain/usr/include,
"$(SRCROOT)/../../../React/**",
"$(SRCROOT)/../../react-native/React/**",
);
LIBRARY_SEARCH_PATHS = "$(inherited)";
OTHER_LDFLAGS = "-ObjC";
PRODUCT_NAME = RNScreens;
SKIP_INSTALL = YES;
};
name = Testflight;
};
58B511ED1A9E6C8500147676 /* Debug */ = {
isa = XCBuildConfiguration;
buildSettings = {
ALWAYS_SEARCH_USER_PATHS = NO;
CLANG_CXX_LANGUAGE_STANDARD = "gnu++0x";
CLANG_CXX_LIBRARY = "libc++";
CLANG_ENABLE_MODULES = YES;
CLANG_ENABLE_OBJC_ARC = YES;
CLANG_WARN_BLOCK_CAPTURE_AUTORELEASING = YES;
CLANG_WARN_BOOL_CONVERSION = YES;
CLANG_WARN_COMMA = YES;
CLANG_WARN_CONSTANT_CONVERSION = YES;
CLANG_WARN_DIRECT_OBJC_ISA_USAGE = YES_ERROR;
CLANG_WARN_EMPTY_BODY = YES;
CLANG_WARN_ENUM_CONVERSION = YES;
CLANG_WARN_INFINITE_RECURSION = YES;
CLANG_WARN_INT_CONVERSION = YES;
CLANG_WARN_NON_LITERAL_NULL_CONVERSION = YES;
CLANG_WARN_OBJC_LITERAL_CONVERSION = YES;
CLANG_WARN_OBJC_ROOT_CLASS = YES_ERROR;
CLANG_WARN_RANGE_LOOP_ANALYSIS = YES;
CLANG_WARN_STRICT_PROTOTYPES = YES;
CLANG_WARN_SUSPICIOUS_MOVE = YES;
CLANG_WARN_UNREACHABLE_CODE = YES;
CLANG_WARN__DUPLICATE_METHOD_MATCH = YES;
COPY_PHASE_STRIP = NO;
ENABLE_STRICT_OBJC_MSGSEND = YES;
ENABLE_TESTABILITY = YES;
GCC_C_LANGUAGE_STANDARD = gnu99;
GCC_DYNAMIC_NO_PIC = NO;
GCC_NO_COMMON_BLOCKS = YES;
GCC_OPTIMIZATION_LEVEL = 0;
GCC_PREPROCESSOR_DEFINITIONS = (
"DEBUG=1",
"$(inherited)",
);
GCC_SYMBOLS_PRIVATE_EXTERN = NO;
GCC_WARN_64_TO_32_BIT_CONVERSION = YES;
GCC_WARN_ABOUT_RETURN_TYPE = YES_ERROR;
GCC_WARN_UNDECLARED_SELECTOR = YES;
GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE;
GCC_WARN_UNUSED_FUNCTION = YES;
GCC_WARN_UNUSED_VARIABLE = YES;
IPHONEOS_DEPLOYMENT_TARGET = 8.0;
MTL_ENABLE_DEBUG_INFO = YES;
ONLY_ACTIVE_ARCH = YES;
SDKROOT = iphoneos;
};
name = Debug;
};
58B511EE1A9E6C8500147676 /* Release */ = {
isa = XCBuildConfiguration;
buildSettings = {
ALWAYS_SEARCH_USER_PATHS = NO;
CLANG_CXX_LANGUAGE_STANDARD = "gnu++0x";
CLANG_CXX_LIBRARY = "libc++";
CLANG_ENABLE_MODULES = YES;
CLANG_ENABLE_OBJC_ARC = YES;
CLANG_WARN_BLOCK_CAPTURE_AUTORELEASING = YES;
CLANG_WARN_BOOL_CONVERSION = YES;
CLANG_WARN_COMMA = YES;
CLANG_WARN_CONSTANT_CONVERSION = YES;
CLANG_WARN_DIRECT_OBJC_ISA_USAGE = YES_ERROR;
CLANG_WARN_EMPTY_BODY = YES;
CLANG_WARN_ENUM_CONVERSION = YES;
CLANG_WARN_INFINITE_RECURSION = YES;
CLANG_WARN_INT_CONVERSION = YES;
CLANG_WARN_NON_LITERAL_NULL_CONVERSION = YES;
CLANG_WARN_OBJC_LITERAL_CONVERSION = YES;
CLANG_WARN_OBJC_ROOT_CLASS = YES_ERROR;
CLANG_WARN_RANGE_LOOP_ANALYSIS = YES;
CLANG_WARN_STRICT_PROTOTYPES = YES;
CLANG_WARN_SUSPICIOUS_MOVE = YES;
CLANG_WARN_UNREACHABLE_CODE = YES;
CLANG_WARN__DUPLICATE_METHOD_MATCH = YES;
COPY_PHASE_STRIP = YES;
ENABLE_NS_ASSERTIONS = NO;
ENABLE_STRICT_OBJC_MSGSEND = YES;
GCC_C_LANGUAGE_STANDARD = gnu99;
GCC_NO_COMMON_BLOCKS = YES;
GCC_WARN_64_TO_32_BIT_CONVERSION = YES;
GCC_WARN_ABOUT_RETURN_TYPE = YES_ERROR;
GCC_WARN_UNDECLARED_SELECTOR = YES;
GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE;
GCC_WARN_UNUSED_FUNCTION = YES;
GCC_WARN_UNUSED_VARIABLE = YES;
IPHONEOS_DEPLOYMENT_TARGET = 8.0;
MTL_ENABLE_DEBUG_INFO = NO;
SDKROOT = iphoneos;
VALIDATE_PRODUCT = YES;
};
name = Release;
};
58B511F01A9E6C8500147676 /* Debug */ = {
isa = XCBuildConfiguration;
buildSettings = {
HEADER_SEARCH_PATHS = (
"$(inherited)",
/Applications/Xcode.app/Contents/Developer/Toolchains/XcodeDefault.xctoolchain/usr/include,
"$(SRCROOT)/../../../React/**",
"$(SRCROOT)/../../react-native/React/**",
);
LIBRARY_SEARCH_PATHS = "$(inherited)";
OTHER_LDFLAGS = "-ObjC";
PRODUCT_NAME = RNScreens;
SKIP_INSTALL = YES;
};
name = Debug;
};
58B511F11A9E6C8500147676 /* Release */ = {
isa = XCBuildConfiguration;
buildSettings = {
HEADER_SEARCH_PATHS = (
"$(inherited)",
/Applications/Xcode.app/Contents/Developer/Toolchains/XcodeDefault.xctoolchain/usr/include,
"$(SRCROOT)/../../../React/**",
"$(SRCROOT)/../../react-native/React/**",
);
LIBRARY_SEARCH_PATHS = "$(inherited)";
OTHER_LDFLAGS = "-ObjC";
PRODUCT_NAME = RNScreens;
SKIP_INSTALL = YES;
};
name = Release;
};
B5C32A4C220C6379000FFB8D /* Debug */ = {
isa = XCBuildConfiguration;
buildSettings = {
HEADER_SEARCH_PATHS = (
"$(inherited)",
/Applications/Xcode.app/Contents/Developer/Toolchains/XcodeDefault.xctoolchain/usr/include,
"$(SRCROOT)/../../../React/**",
"$(SRCROOT)/../../react-native/React/**",
);
LIBRARY_SEARCH_PATHS = "$(inherited)";
OTHER_LDFLAGS = "-ObjC";
PRODUCT_NAME = "$(TARGET_NAME)";
SDKROOT = appletvos;
SKIP_INSTALL = YES;
};
name = Debug;
};
B5C32A4D220C6379000FFB8D /* Release */ = {
isa = XCBuildConfiguration;
buildSettings = {
HEADER_SEARCH_PATHS = (
"$(inherited)",
/Applications/Xcode.app/Contents/Developer/Toolchains/XcodeDefault.xctoolchain/usr/include,
"$(SRCROOT)/../../../React/**",
"$(SRCROOT)/../../react-native/React/**",
);
LIBRARY_SEARCH_PATHS = "$(inherited)";
OTHER_LDFLAGS = "-ObjC";
PRODUCT_NAME = "$(TARGET_NAME)";
SDKROOT = appletvos;
SKIP_INSTALL = YES;
};
name = Release;
};
B5C32A4E220C6379000FFB8D /* Testflight */ = {
isa = XCBuildConfiguration;
buildSettings = {
HEADER_SEARCH_PATHS = (
"$(inherited)",
/Applications/Xcode.app/Contents/Developer/Toolchains/XcodeDefault.xctoolchain/usr/include,
"$(SRCROOT)/../../../React/**",
"$(SRCROOT)/../../react-native/React/**",
);
LIBRARY_SEARCH_PATHS = "$(inherited)";
OTHER_LDFLAGS = "-ObjC";
PRODUCT_NAME = "$(TARGET_NAME)";
SDKROOT = appletvos;
SKIP_INSTALL = YES;
};
name = Testflight;
};
C7F03305A3464E75B4F5A6CE /* Testflight */ = {
isa = XCBuildConfiguration;
buildSettings = {
ALWAYS_SEARCH_USER_PATHS = NO;
CLANG_CXX_LANGUAGE_STANDARD = "gnu++0x";
CLANG_CXX_LIBRARY = "libc++";
CLANG_ENABLE_MODULES = YES;
CLANG_ENABLE_OBJC_ARC = YES;
CLANG_WARN_BLOCK_CAPTURE_AUTORELEASING = YES;
CLANG_WARN_BOOL_CONVERSION = YES;
CLANG_WARN_COMMA = YES;
CLANG_WARN_CONSTANT_CONVERSION = YES;
CLANG_WARN_DIRECT_OBJC_ISA_USAGE = YES_ERROR;
CLANG_WARN_EMPTY_BODY = YES;
CLANG_WARN_ENUM_CONVERSION = YES;
CLANG_WARN_INFINITE_RECURSION = YES;
CLANG_WARN_INT_CONVERSION = YES;
CLANG_WARN_NON_LITERAL_NULL_CONVERSION = YES;
CLANG_WARN_OBJC_LITERAL_CONVERSION = YES;
CLANG_WARN_OBJC_ROOT_CLASS = YES_ERROR;
CLANG_WARN_RANGE_LOOP_ANALYSIS = YES;
CLANG_WARN_STRICT_PROTOTYPES = YES;
CLANG_WARN_SUSPICIOUS_MOVE = YES;
CLANG_WARN_UNREACHABLE_CODE = YES;
CLANG_WARN__DUPLICATE_METHOD_MATCH = YES;
COPY_PHASE_STRIP = YES;
ENABLE_NS_ASSERTIONS = NO;
ENABLE_STRICT_OBJC_MSGSEND = YES;
GCC_C_LANGUAGE_STANDARD = gnu99;
GCC_NO_COMMON_BLOCKS = YES;
GCC_WARN_64_TO_32_BIT_CONVERSION = YES;
GCC_WARN_ABOUT_RETURN_TYPE = YES_ERROR;
GCC_WARN_UNDECLARED_SELECTOR = YES;
GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE;
GCC_WARN_UNUSED_FUNCTION = YES;
GCC_WARN_UNUSED_VARIABLE = YES;
IPHONEOS_DEPLOYMENT_TARGET = 8.0;
MTL_ENABLE_DEBUG_INFO = NO;
SDKROOT = iphoneos;
VALIDATE_PRODUCT = YES;
};
name = Testflight;
};
/* End XCBuildConfiguration section */
/* Begin XCConfigurationList section */
58B511D61A9E6C8500147676 /* Build configuration list for PBXProject "RNScreens" */ = {
isa = XCConfigurationList;
buildConfigurations = (
58B511ED1A9E6C8500147676 /* Debug */,
58B511EE1A9E6C8500147676 /* Release */,
C7F03305A3464E75B4F5A6CE /* Testflight */,
);
defaultConfigurationIsVisible = 0;
defaultConfigurationName = Release;
};
58B511EF1A9E6C8500147676 /* Build configuration list for PBXNativeTarget "RNScreens" */ = {
isa = XCConfigurationList;
buildConfigurations = (
58B511F01A9E6C8500147676 /* Debug */,
58B511F11A9E6C8500147676 /* Release */,
0CE596A6BAEE45CA860361AD /* Testflight */,
);
defaultConfigurationIsVisible = 0;
defaultConfigurationName = Release;
};
B5C32A4B220C6379000FFB8D /* Build configuration list for PBXNativeTarget "RNScreens-tvOS" */ = {
isa = XCConfigurationList;
buildConfigurations = (
B5C32A4C220C6379000FFB8D /* Debug */,
B5C32A4D220C6379000FFB8D /* Release */,
B5C32A4E220C6379000FFB8D /* Testflight */,
);
defaultConfigurationIsVisible = 0;
defaultConfigurationName = Release;
};
/* End XCConfigurationList section */
};
rootObject = 58B511D31A9E6C8500147676 /* Project object */;
}

View File

@@ -0,0 +1,16 @@
#pragma once
#import <UIKit/UIKit.h>
NS_ASSUME_NONNULL_BEGIN
@interface UIScrollView (RNScreens)
/**
* Scrolls to top taking into account adjustedContentInset.
* Returns true if scroll was necessary, false if ScrollView was already at the top.
*/
- (BOOL)rnscreens_scrollToTop;
@end
NS_ASSUME_NONNULL_END

View File

@@ -0,0 +1,15 @@
#import "UIScrollView+RNScreens.h"
@implementation UIScrollView (RNScreens)
- (BOOL)rnscreens_scrollToTop
{
if ([self contentOffset].y != -self.adjustedContentInset.top) {
[self setContentOffset:CGPointMake(0, -self.adjustedContentInset.top) animated:YES];
return YES;
}
return NO;
}
@end

View File

@@ -0,0 +1,11 @@
#pragma once
#import <UIKit/UIKit.h>
NS_ASSUME_NONNULL_BEGIN
@interface UIViewController (RNScreens)
@end
NS_ASSUME_NONNULL_END

View File

@@ -0,0 +1,100 @@
#import "RNSConversions.h"
#import "RNSEnums.h"
#import "RNSOrientationProviding.h"
#import "RNSScreenContainer.h"
#import "UIViewController+RNScreens.h"
#import <objc/runtime.h>
@implementation UIViewController (RNScreens)
#if !TARGET_OS_TV
- (UIViewController *)reactNativeScreensChildViewControllerForStatusBarStyle
{
UIViewController *childVC = [self findChildRNSScreensViewController];
return childVC ?: [self reactNativeScreensChildViewControllerForStatusBarStyle];
}
- (UIViewController *)reactNativeScreensChildViewControllerForStatusBarHidden
{
UIViewController *childVC = [self findChildRNSScreensViewController];
return childVC ?: [self reactNativeScreensChildViewControllerForStatusBarHidden];
}
- (UIStatusBarAnimation)reactNativeScreensPreferredStatusBarUpdateAnimation
{
UIViewController *childVC = [self findChildRNSScreensViewController];
return childVC ? childVC.preferredStatusBarUpdateAnimation
: [self reactNativeScreensPreferredStatusBarUpdateAnimation];
}
- (UIInterfaceOrientationMask)reactNativeScreensSupportedInterfaceOrientations
{
id<RNSOrientationProviding> childOrientationProvidingVC = [self findChildRNSOrientationProvidingViewController];
if (childOrientationProvidingVC != nil) {
RNSOrientation orientation = [childOrientationProvidingVC evaluateOrientation];
if (orientation == RNSOrientationInherit) {
return [[UIApplication sharedApplication] supportedInterfaceOrientationsForWindow:self.view.window];
}
return rnscreens::conversion::UIInterfaceOrientationMaskFromRNSOrientation(orientation);
}
return [self reactNativeScreensSupportedInterfaceOrientations];
}
- (UIViewController *)reactNativeScreensChildViewControllerForHomeIndicatorAutoHidden
{
UIViewController *childVC = [self findChildRNSScreensViewController];
return childVC ?: [self reactNativeScreensChildViewControllerForHomeIndicatorAutoHidden];
}
- (id<RNSOrientationProviding>)findChildRNSOrientationProvidingViewController
{
UIViewController *lastViewController = [[self childViewControllers] lastObject];
if ([lastViewController respondsToSelector:@selector(evaluateOrientation)]) {
return static_cast<id<RNSOrientationProviding>>(lastViewController);
}
return nil;
}
- (UIViewController *)findChildRNSScreensViewController
{
UIViewController *lastViewController = [[self childViewControllers] lastObject];
if ([lastViewController conformsToProtocol:@protocol(RNSViewControllerDelegate)]) {
return lastViewController;
}
return nil;
}
+ (void)load
{
static dispatch_once_t once_token;
dispatch_once(&once_token, ^{
Class uiVCClass = [UIViewController class];
method_exchangeImplementations(
class_getInstanceMethod(uiVCClass, @selector(childViewControllerForStatusBarStyle)),
class_getInstanceMethod(uiVCClass, @selector(reactNativeScreensChildViewControllerForStatusBarStyle)));
method_exchangeImplementations(
class_getInstanceMethod(uiVCClass, @selector(childViewControllerForStatusBarHidden)),
class_getInstanceMethod(uiVCClass, @selector(reactNativeScreensChildViewControllerForStatusBarHidden)));
method_exchangeImplementations(
class_getInstanceMethod(uiVCClass, @selector(preferredStatusBarUpdateAnimation)),
class_getInstanceMethod(uiVCClass, @selector(reactNativeScreensPreferredStatusBarUpdateAnimation)));
method_exchangeImplementations(
class_getInstanceMethod(uiVCClass, @selector(supportedInterfaceOrientations)),
class_getInstanceMethod(uiVCClass, @selector(reactNativeScreensSupportedInterfaceOrientations)));
method_exchangeImplementations(
class_getInstanceMethod(uiVCClass, @selector(childViewControllerForHomeIndicatorAutoHidden)),
class_getInstanceMethod(uiVCClass, @selector(reactNativeScreensChildViewControllerForHomeIndicatorAutoHidden)));
});
}
#endif
@end

View File

@@ -0,0 +1,11 @@
#pragma once
#import <UIKit/UIKit.h>
NS_ASSUME_NONNULL_BEGIN
@interface UIWindow (RNScreens)
@end
NS_ASSUME_NONNULL_END

View File

@@ -0,0 +1,17 @@
#import "RNSFullWindowOverlay.h"
#import "UIWindow+RNScreens.h"
@implementation UIWindow (RNScreens)
- (void)didAddSubview:(UIView *)subview
{
if (![subview isKindOfClass:[RNSFullWindowOverlayContainer class]]) {
for (UIView *view in self.subviews) {
if ([view isKindOfClass:[RNSFullWindowOverlayContainer class]]) {
[self bringSubviewToFront:view];
}
}
}
}
@end

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@@ -0,0 +1,52 @@
#pragma once
#import "RNSDefines.h"
#if RNS_BOTTOM_ACCESSORY_AVAILABLE
#import <Foundation/Foundation.h>
#import <UIKit/UIKit.h>
// Hide C++ symbols from C compiler used when building Swift module
#if defined(__cplusplus) && RCT_NEW_ARCH_ENABLED
#import <react/renderer/components/rnscreens/EventEmitters.h>
namespace react = facebook::react;
#endif // defined(__cplusplus) && RCT_NEW_ARCH_ENABLED
#if !RCT_NEW_ARCH_ENABLED
#import <React/RCTComponent.h>
#endif // !RCT_NEW_ARCH_ENABLED
NS_ASSUME_NONNULL_BEGIN
@interface RNSBottomTabsAccessoryEventEmitter : NSObject
- (BOOL)emitOnEnvironmentChangeIfNeeded:(UITabAccessoryEnvironment)environment API_AVAILABLE(ios(26.0));
@end
#pragma mark - Hidden from Swift
#if defined(__cplusplus)
@interface RNSBottomTabsAccessoryEventEmitter ()
#if RCT_NEW_ARCH_ENABLED
- (void)updateEventEmitter:(const std::shared_ptr<const react::RNSBottomTabsAccessoryEventEmitter> &)emitter;
#else
#pragma mark - LEGACY Event emitter blocks
@property (nonatomic, copy) RCTDirectEventBlock onEnvironmentChange;
#endif // RCT_NEW_ARCH_ENABLED
@end
#endif // defined(__cplusplus)
NS_ASSUME_NONNULL_END
#endif // RNS_BOTTOM_ACCESSORY_AVAILABLE

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@@ -0,0 +1,613 @@
#import "RNSBottomTabsHostComponentView.h"
#if RCT_NEW_ARCH_ENABLED
#import <React/RCTConversions.h>
#import <React/RCTImageLoader.h>
#import <React/RCTMountingTransactionObserving.h>
#import <react/renderer/components/rnscreens/ComponentDescriptors.h>
#import <react/renderer/components/rnscreens/EventEmitters.h>
#import <react/renderer/components/rnscreens/Props.h>
#import <react/renderer/components/rnscreens/RCTComponentViewHelpers.h>
#import <rnscreens/RNSBottomTabsComponentDescriptor.h>
#import "RNSBottomTabsHostComponentView+RNSImageLoader.h"
#import "RNSInvalidatedComponentsRegistry.h"
#import "RNSViewControllerInvalidator.h"
#endif // RCT_NEW_ARCH_ENABLED
#import "RNSBottomAccessoryHelper.h"
#import "RNSBottomTabsAccessoryComponentView.h"
#import "RNSBottomTabsScreenComponentView.h"
#import "RNSConversions.h"
#import "RNSConvert.h"
#import "RNSDefines.h"
#import "RNSLog.h"
#import "RNSTabBarController.h"
#import "RNSTabBarControllerDelegate.h"
namespace react = facebook::react;
#pragma mark - Modified React Subviews extension
@interface RNSBottomTabsHostComponentView ()
@property (nonatomic, readonly) BOOL hasModifiedReactSubviewsInCurrentTransaction;
@end
#pragma mark - View implementation
@interface RNSBottomTabsHostComponentView ()
#if RCT_NEW_ARCH_ENABLED
<RCTMountingTransactionObserving>
#endif // RCT_NEW_ARCH_ENABLED
@end
@implementation RNSBottomTabsHostComponentView {
RNSTabBarController *_Nonnull _controller;
RNSTabBarControllerDelegate *_controllerDelegate;
RNSBottomTabsHostEventEmitter *_Nonnull _reactEventEmitter;
RCTImageLoader *_Nullable _imageLoader;
#if RCT_NEW_ARCH_ENABLED
RNSInvalidatedComponentsRegistry *_Nonnull _invalidatedComponentsRegistry;
#endif // RCT_NEW_ARCH_ENABLED
// RCTViewComponentView does not expose this field, therefore we maintain
// it on our side.
NSMutableArray<UIView *> *_reactSubviews;
BOOL _hasModifiedTabsScreensInCurrentTransaction;
BOOL _hasModifiedBottomAccessoryInCurrentTransation;
BOOL _needsTabBarAppearanceUpdate;
}
- (instancetype)initWithFrame:(CGRect)frame
{
if (self = [super initWithFrame:frame]) {
[self initState];
}
return self;
}
#if !RCT_NEW_ARCH_ENABLED
- (instancetype)initWithFrame:(CGRect)frame reactImageLoader:(RCTImageLoader *)imageLoader
{
if (self = [self initWithFrame:frame]) {
_imageLoader = imageLoader;
}
return self;
}
#endif // !RCT_NEW_ARCH_ENABLED
- (nonnull RNSTabBarController *)controller
{
RCTAssert(_controller != nil, @"[RNScreens] Controller must not be nil");
return _controller;
}
- (void)initState
{
[self resetProps];
_controller = [[RNSTabBarController alloc] initWithTabsHostComponentView:self];
_controllerDelegate = [RNSTabBarControllerDelegate new];
_controller.delegate = _controllerDelegate;
_reactSubviews = [NSMutableArray new];
_reactEventEmitter = [RNSBottomTabsHostEventEmitter new];
#if RCT_NEW_ARCH_ENABLED
_invalidatedComponentsRegistry = [RNSInvalidatedComponentsRegistry new];
#endif // RCT_NEW_ARCH_ENABLED
_hasModifiedTabsScreensInCurrentTransaction = NO;
_hasModifiedBottomAccessoryInCurrentTransation = NO;
_needsTabBarAppearanceUpdate = NO;
}
- (void)resetProps
{
#if RCT_NEW_ARCH_ENABLED
static const auto defaultProps = std::make_shared<const react::RNSBottomTabsProps>();
_props = defaultProps;
#endif
_tabBarTintColor = nil;
#if !TARGET_OS_TV
_nativeContainerBackgroundColor = [UIColor systemBackgroundColor];
#else // !TARGET_OS_TV
_nativeContainerBackgroundColor = nil;
#endif // !TARGET_OS_TV
}
#pragma mark - UIView methods
- (void)willMoveToWindow:(UIWindow *)newWindow
{
#if RCT_NEW_ARCH_ENABLED
if (newWindow == nil) {
[_invalidatedComponentsRegistry flushInvalidViews];
}
#endif // RCT_NEW_ARCH_ENABLED
}
- (void)didMoveToWindow
{
if ([self window] != nil) {
[self reactAddControllerToClosestParent:_controller];
#if !RCT_NEW_ARCH_ENABLED
// This is required on legacy architecture to prevent a bug with doubled size of UIViewControllerWrapperView.
_controller.view.frame = self.bounds;
#endif // !RCT_NEW_ARCH_ENABLED
}
}
- (void)reactAddControllerToClosestParent:(UIViewController *)controller
{
if (!controller.parentViewController) {
UIView *parentView = (UIView *)self.reactSuperview;
while (parentView) {
if (parentView.reactViewController) {
[parentView.reactViewController addChildViewController:controller];
[self addSubview:controller.view];
// Enable auto-layout to ensure valid size of tabBarController.view.
// In host tree, tabBarController.view is the only child of HostComponentView.
controller.view.translatesAutoresizingMaskIntoConstraints = NO;
[NSLayoutConstraint activateConstraints:@[
[controller.view.topAnchor constraintEqualToAnchor:self.topAnchor],
[controller.view.bottomAnchor constraintEqualToAnchor:self.bottomAnchor],
[controller.view.leadingAnchor constraintEqualToAnchor:self.leadingAnchor],
[controller.view.trailingAnchor constraintEqualToAnchor:self.trailingAnchor]
]];
[controller didMoveToParentViewController:parentView.reactViewController];
break;
}
parentView = (UIView *)parentView.reactSuperview;
}
return;
}
}
#pragma mark - RNSScreenContainerDelegate
- (void)updateContainer
{
if (!self.hasModifiedReactSubviewsInCurrentTransaction) {
return;
}
NSMutableArray<RNSTabsScreenViewController *> *tabControllers =
[[NSMutableArray alloc] initWithCapacity:_reactSubviews.count];
RNSBottomTabsAccessoryComponentView *bottomAccessory = nil;
for (UIView *childView in _reactSubviews) {
if ([childView isKindOfClass:[RNSBottomTabsScreenComponentView class]]) {
RNSBottomTabsScreenComponentView *childScreen = static_cast<RNSBottomTabsScreenComponentView *>(childView);
[tabControllers addObject:childScreen.controller];
} else if ([childView isKindOfClass:[RNSBottomTabsAccessoryComponentView class]]) {
RCTAssert(bottomAccessory == nil, @"[RNScreens] There can only be one child RNSBottomTabsAccessoryComponentView");
bottomAccessory = static_cast<RNSBottomTabsAccessoryComponentView *>(childView);
} else {
RCTLogError(
@"[RNScreens] BottomTabs only accepts children of type BottomTabScreen and BottomTabsAccessory. Detected %@ instead.",
childView);
}
}
if (_hasModifiedTabsScreensInCurrentTransaction) {
RNSLog(@"updateContainer: tabControllers: %@", tabControllers);
[_controller childViewControllersHaveChangedTo:tabControllers];
}
if (_hasModifiedBottomAccessoryInCurrentTransation) {
RNSLog(@"updateContainer: bottomAccessory: %@", bottomAccessory);
#if RNS_IPHONE_OS_VERSION_AVAILABLE(26_0) && !TARGET_OS_TV && !TARGET_OS_VISION
if (@available(iOS 26.0, *)) {
if (bottomAccessory != nil) {
// We wrap RNSBottomTabsAccessoryComponentView in plain UIView to maintain native
// corner radius. RCTViewComponentView overrides it to 0 by default and we're unable
// to restore default value in an easy way. By wrapping it in UIView, it is clipped
// to default corner radius.
UIView *wrapperView = [UIView new];
[wrapperView addSubview:bottomAccessory];
[_controller setBottomAccessory:[[UITabAccessory alloc] initWithContentView:wrapperView] animated:YES];
} else {
[_controller setBottomAccessory:nil animated:YES];
}
}
#endif // RNS_IPHONE_OS_VERSION_AVAILABLE(26_0) && !TARGET_OS_TV && !TARGET_OS_VISION
}
}
- (void)markChildUpdated
{
[self updateContainer];
}
#if RCT_NEW_ARCH_ENABLED
#pragma mark - RNSViewControllerInvalidating
- (void)invalidateController
{
_controller = nil;
}
- (BOOL)shouldInvalidateOnMutation:(const facebook::react::ShadowViewMutation &)mutation
{
return (mutation.oldChildShadowView.tag == self.tag && mutation.type == facebook::react::ShadowViewMutation::Delete);
}
#else
#pragma mark - RCTInvalidating
- (void)invalidate
{
// We assume that bottom tabs host is removed from view hierarchy **only** when
// whole component is destroyed & therefore we do the necessary cleanup here.
// If at some point that statement does not hold anymore, this cleanup
// should be moved to a different place.
for (UIView<RCTInvalidating> *subview in _reactSubviews) {
[subview invalidate];
}
_controller = nil;
}
#endif
#pragma mark - React events
- (nonnull RNSBottomTabsHostEventEmitter *)reactEventEmitter
{
RCTAssert(_reactEventEmitter != nil, @"[RNScreens] Attempt to access uninitialized _reactEventEmitter");
return _reactEventEmitter;
}
- (BOOL)emitOnNativeFocusChangeRequestSelectedTabScreen:(nonnull RNSBottomTabsScreenComponentView *)tabScreen
repeatedSelectionHandledBySpecialEffect:(BOOL)repeatedSelectionHandledBySpecialEffect
{
return [_reactEventEmitter
emitOnNativeFocusChange:OnNativeFocusChangePayload{
.tabKey = tabScreen.tabKey,
.repeatedSelectionHandledBySpecialEffect = repeatedSelectionHandledBySpecialEffect}];
}
#pragma mark - RCTComponentViewProtocol
#if RCT_NEW_ARCH_ENABLED
- (void)mountChildComponentView:(UIView<RCTComponentViewProtocol> *)childComponentView index:(NSInteger)index
{
[self validateAndHandleReactSubview:childComponentView atIndex:index shouldMount:YES];
}
- (void)unmountChildComponentView:(UIView<RCTComponentViewProtocol> *)childComponentView index:(NSInteger)index
{
[self validateAndHandleReactSubview:childComponentView atIndex:index shouldMount:NO];
}
- (void)updateProps:(const facebook::react::Props::Shared &)props
oldProps:(const facebook::react::Props::Shared &)oldProps
{
const auto &oldComponentProps = *std::static_pointer_cast<const react::RNSBottomTabsProps>(_props);
const auto &newComponentProps = *std::static_pointer_cast<const react::RNSBottomTabsProps>(props);
if (newComponentProps.controlNavigationStateInJS != oldComponentProps.controlNavigationStateInJS) {
_experimental_controlNavigationStateInJS = newComponentProps.controlNavigationStateInJS;
}
if (newComponentProps.tabBarTintColor != oldComponentProps.tabBarTintColor) {
_needsTabBarAppearanceUpdate = YES;
_tabBarTintColor = RCTUIColorFromSharedColor(newComponentProps.tabBarTintColor);
}
if (newComponentProps.tabBarHidden != oldComponentProps.tabBarHidden) {
_tabBarHidden = newComponentProps.tabBarHidden;
#if RNS_IPHONE_OS_VERSION_AVAILABLE(18_0)
if (@available(iOS 18.0, *)) {
[_controller setTabBarHidden:_tabBarHidden animated:NO];
} else
#endif // RNS_IPHONE_OS_VERSION_AVAILABLE(18_0)
{
_controller.tabBar.hidden = _tabBarHidden;
}
}
if (newComponentProps.nativeContainerBackgroundColor != oldComponentProps.nativeContainerBackgroundColor) {
_nativeContainerBackgroundColor = RCTUIColorFromSharedColor(newComponentProps.nativeContainerBackgroundColor);
#if !TARGET_OS_TV
if (_nativeContainerBackgroundColor == nil) {
_nativeContainerBackgroundColor = [UIColor systemBackgroundColor];
}
#endif // !TARGET_OS_TV
_controller.view.backgroundColor = _nativeContainerBackgroundColor;
}
if (newComponentProps.tabBarMinimizeBehavior != oldComponentProps.tabBarMinimizeBehavior) {
#if RNS_IPHONE_OS_VERSION_AVAILABLE(26_0)
if (@available(iOS 26.0, *)) {
_tabBarMinimizeBehavior = rnscreens::conversion::UITabBarMinimizeBehaviorFromRNSBottomTabsTabBarMinimizeBehavior(
newComponentProps.tabBarMinimizeBehavior);
_controller.tabBarMinimizeBehavior = _tabBarMinimizeBehavior;
} else
#endif // Check for iOS >= 26
if (newComponentProps.tabBarMinimizeBehavior != react::RNSBottomTabsTabBarMinimizeBehavior::Automatic) {
RCTLogWarn(@"[RNScreens] tabBarMinimizeBehavior is supported for iOS >= 26");
}
}
if (newComponentProps.tabBarControllerMode != oldComponentProps.tabBarControllerMode) {
#if RNS_IPHONE_OS_VERSION_AVAILABLE(18_0)
if (@available(iOS 18.0, *)) {
_tabBarControllerMode = rnscreens::conversion::UITabBarControllerModeFromRNSBottomTabsTabBarControllerMode(
newComponentProps.tabBarControllerMode);
_controller.mode = _tabBarControllerMode;
} else
#endif // Check for iOS >= 18
if (newComponentProps.tabBarControllerMode != react::RNSBottomTabsTabBarControllerMode::Automatic) {
RCTLogWarn(@"[RNScreens] tabBarControllerMode is supported for iOS >= 18");
}
}
// Super call updates _props pointer. We should NOT update it before calling super.
[super updateProps:props oldProps:oldProps];
}
- (void)updateState:(const facebook::react::State::Shared &)state
oldState:(const facebook::react::State::Shared &)oldState
{
react::RNSBottomTabsShadowNode::ConcreteState::Shared receivedState =
std::static_pointer_cast<const react::RNSBottomTabsShadowNode::ConcreteState>(state);
_imageLoader = [self retrieveImageLoaderFromState:receivedState];
}
- (void)updateEventEmitter:(const facebook::react::EventEmitter::Shared &)eventEmitter
{
[super updateEventEmitter:eventEmitter];
const auto &castedEventEmitter = std::static_pointer_cast<const react::RNSBottomTabsEventEmitter>(eventEmitter);
[_reactEventEmitter updateEventEmitter:castedEventEmitter];
}
- (void)finalizeUpdates:(RNComponentViewUpdateMask)updateMask
{
if (_needsTabBarAppearanceUpdate) {
_needsTabBarAppearanceUpdate = NO;
[_controller setNeedsUpdateOfTabBarAppearance:true];
}
[super finalizeUpdates:updateMask];
}
+ (react::ComponentDescriptorProvider)componentDescriptorProvider
{
return react::concreteComponentDescriptorProvider<react::RNSBottomTabsComponentDescriptor>();
}
+ (BOOL)shouldBeRecycled
{
// There won't be tens of instances of this component usually & it's easier for now.
// We could consider enabling it someday though.
return NO;
}
#pragma mark - RCTMountingTransactionObserving
- (void)mountingTransactionWillMount:(const facebook::react::MountingTransaction &)transaction
withSurfaceTelemetry:(const facebook::react::SurfaceTelemetry &)surfaceTelemetry
{
_hasModifiedTabsScreensInCurrentTransaction = NO;
_hasModifiedBottomAccessoryInCurrentTransation = NO;
[_controller reactMountingTransactionWillMount];
#if RCT_NEW_ARCH_ENABLED
for (const auto &mutation : transaction.getMutations()) {
if ([self shouldInvalidateOnMutation:mutation]) {
for (UIView<RNSViewControllerInvalidating> *childView in _reactSubviews) {
[RNSViewControllerInvalidator invalidateViewIfDetached:childView forRegistry:_invalidatedComponentsRegistry];
}
[RNSViewControllerInvalidator invalidateViewIfDetached:self forRegistry:_invalidatedComponentsRegistry];
}
}
#endif // RCT_NEW_ARCH_ENABLED
}
- (void)mountingTransactionDidMount:(const facebook::react::MountingTransaction &)transaction
withSurfaceTelemetry:(const facebook::react::SurfaceTelemetry &)surfaceTelemetry
{
if (self.hasModifiedReactSubviewsInCurrentTransaction) {
[self updateContainer];
}
[_controller reactMountingTransactionDidMount];
}
#else
#pragma mark - LEGACY architecture implementation
#pragma mark - LEGACY RCTComponent protocol
- (void)insertReactSubview:(UIView *)subview atIndex:(NSInteger)index
{
[super insertReactSubview:subview atIndex:index];
[self validateAndHandleReactSubview:subview atIndex:index shouldMount:YES];
}
- (void)removeReactSubview:(UIView *)subview
{
[super removeReactSubview:subview];
// index is not used for unmount
[self validateAndHandleReactSubview:subview atIndex:-1 shouldMount:NO];
}
RNS_IGNORE_SUPER_CALL_BEGIN
- (void)didUpdateReactSubviews
{
[self invalidateFlagsOnControllerIfNeeded];
}
RNS_IGNORE_SUPER_CALL_END
- (void)didSetProps:(NSArray<NSString *> *)changedProps
{
[super didSetProps:changedProps];
_needsTabBarAppearanceUpdate = YES;
[self invalidateFlagsOnControllerIfNeeded];
}
#pragma mark - LEGACY update methods
- (void)invalidateFlagsOnControllerIfNeeded
{
if (_needsTabBarAppearanceUpdate) {
_needsTabBarAppearanceUpdate = NO;
[_controller setNeedsUpdateOfTabBarAppearance:true];
}
if (self.hasModifiedReactSubviewsInCurrentTransaction) {
[self updateContainer];
_hasModifiedTabsScreensInCurrentTransaction = NO;
_hasModifiedBottomAccessoryInCurrentTransation = NO;
}
}
- (void)invalidateTabBarAppearance
{
_needsTabBarAppearanceUpdate = YES;
[self invalidateFlagsOnControllerIfNeeded];
}
#pragma mark - LEGACY prop setters
// Paper will call property setters
- (void)setTabBarTintColor:(UIColor *_Nullable)tabBarTintColor
{
_tabBarTintColor = tabBarTintColor;
[self invalidateTabBarAppearance];
}
- (void)setTabBarHidden:(BOOL)tabBarHidden
{
_tabBarHidden = tabBarHidden;
#if RNS_IPHONE_OS_VERSION_AVAILABLE(18_0)
if (@available(iOS 18.0, *)) {
[_controller setTabBarHidden:_tabBarHidden animated:NO];
} else
#endif // RNS_IPHONE_OS_VERSION_AVAILABLE(18_0)
{
_controller.tabBar.hidden = _tabBarHidden;
}
}
- (void)setNativeContainerBackgroundColor:(UIColor *_Nullable)nativeContainerBackgroundColor
{
_nativeContainerBackgroundColor = nativeContainerBackgroundColor;
#if !TARGET_OS_TV
if (_nativeContainerBackgroundColor == nil) {
_nativeContainerBackgroundColor = [UIColor systemBackgroundColor];
}
#endif // !TARGET_OS_TV
_controller.view.backgroundColor = _nativeContainerBackgroundColor;
}
// This is a Paper-only setter method that will be called by the mounting code.
// It allows us to store UITabBarMinimizeBehavior in the component while accepting a custom enum as input from JS.
- (void)setTabBarMinimizeBehaviorFromRNSTabBarMinimizeBehavior:(RNSTabBarMinimizeBehavior)tabBarMinimizeBehavior
{
#if RNS_IPHONE_OS_VERSION_AVAILABLE(26_0)
if (@available(iOS 26.0, *)) {
_tabBarMinimizeBehavior =
rnscreens::conversion::UITabBarMinimizeBehaviorFromRNSTabBarMinimizeBehavior(tabBarMinimizeBehavior);
_controller.tabBarMinimizeBehavior = _tabBarMinimizeBehavior;
} else
#endif // Check for iOS >= 26
if (tabBarMinimizeBehavior != RNSTabBarMinimizeBehaviorAutomatic) {
RCTLogWarn(@"[RNScreens] tabBarMinimizeBehavior is supported for iOS >= 26");
}
}
- (void)setTabBarControllerModeFromRNSTabBarControllerMode:(RNSTabBarControllerMode)tabBarControllerMode
{
#if RNS_IPHONE_OS_VERSION_AVAILABLE(18_0)
if (@available(iOS 18.0, *)) {
_tabBarControllerMode =
rnscreens::conversion::UITabBarControllerModeFromRNSTabBarControllerMode(tabBarControllerMode);
_controller.mode = _tabBarControllerMode;
} else
#endif // Check for iOS >= 18
if (tabBarControllerMode != RNSTabBarControllerModeAutomatic) {
RCTLogWarn(@"[RNScreens] tabBarControllerMode is supported for iOS >= 18");
}
}
- (void)setOnNativeFocusChange:(RCTDirectEventBlock)onNativeFocusChange
{
[self.reactEventEmitter setOnNativeFocusChange:onNativeFocusChange];
}
#endif // RCT_NEW_ARCH_ENABLED
#pragma mark - Common
- (void)validateAndHandleReactSubview:(UIView *)subview atIndex:(NSInteger)index shouldMount:(BOOL)mount
{
BOOL isBottomAccessory = [subview isKindOfClass:[RNSBottomTabsAccessoryComponentView class]];
BOOL isTabsScreen = [subview isKindOfClass:[RNSBottomTabsScreenComponentView class]];
RCTAssert(
isBottomAccessory || isTabsScreen,
@"%@",
[NSString
stringWithFormat:
@"BottomTabs only accepts children of type BottomTabScreen and BottomTabsAccessory. Attempted to %@ %@",
mount ? @"mount" : @"unmount",
subview]);
if (isTabsScreen) {
auto *childScreen = static_cast<RNSBottomTabsScreenComponentView *>(subview);
childScreen.reactSuperview = mount ? self : nil;
_hasModifiedTabsScreensInCurrentTransaction = YES;
} else if (isBottomAccessory) {
auto *bottomAccessory = static_cast<RNSBottomTabsAccessoryComponentView *>(subview);
bottomAccessory.bottomTabsHostView = mount ? self : nil;
_hasModifiedBottomAccessoryInCurrentTransation = YES;
}
if (mount) {
[_reactSubviews insertObject:subview atIndex:index];
} else {
[_reactSubviews removeObject:subview];
}
}
#pragma mark - React Image Loader
- (nullable RCTImageLoader *)reactImageLoader
{
return _imageLoader;
}
@end
#pragma mark - Modified React Subviews implementation
@implementation RNSBottomTabsHostComponentView (ModifiedReactSubviews)
- (BOOL)hasModifiedReactSubviewsInCurrentTransaction
{
return _hasModifiedTabsScreensInCurrentTransaction || _hasModifiedBottomAccessoryInCurrentTransation;
}
@end
#if RCT_NEW_ARCH_ENABLED
#pragma mark - View class exposure
Class<RCTComponentViewProtocol> RNSBottomTabsCls(void)
{
return RNSBottomTabsHostComponentView.class;
}
#endif // RCT_NEW_ARCH_ENABLED

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@@ -0,0 +1,752 @@
#import "RNSBottomTabsScreenComponentView.h"
#import "NSString+RNSUtility.h"
#import "RNSConversions.h"
#import "RNSDefines.h"
#import "RNSLog.h"
#import "RNSSafeAreaViewNotifications.h"
#import "RNSScrollViewFinder.h"
#import "RNSScrollViewHelper.h"
#import "RNSTabBarAppearanceCoordinator.h"
#import "RNSTabBarController.h"
#if RCT_NEW_ARCH_ENABLED
#import <React/RCTConversions.h>
#import <React/RCTImageSource.h>
#import <react/renderer/components/rnscreens/ComponentDescriptors.h>
#import <react/renderer/components/rnscreens/EventEmitters.h>
#import <react/renderer/components/rnscreens/Props.h>
#import <react/renderer/components/rnscreens/RCTComponentViewHelpers.h>
#endif // RCT_NEW_ARCH_ENABLED
#if RCT_NEW_ARCH_ENABLED
namespace react = facebook::react;
#endif // RCT_NEW_ARCH_ENABLED
#pragma mark - View implementation
@implementation RNSBottomTabsScreenComponentView {
RNSTabsScreenViewController *_controller;
RNSBottomTabsHostComponentView *__weak _Nullable _reactSuperview;
RNSBottomTabsScreenEventEmitter *_Nonnull _reactEventEmitter;
// We need this information to warn users about dynamic changes to behavior being currently unsupported.
BOOL _isOverrideScrollViewContentInsetAdjustmentBehaviorSet;
#if !RCT_NEW_ARCH_ENABLED
BOOL _tabItemNeedsAppearanceUpdate;
BOOL _tabScreenOrientationNeedsUpdate;
BOOL _tabBarItemNeedsRecreation;
BOOL _tabBarItemNeedsUpdate;
BOOL _scrollEdgeEffectsNeedUpdate;
#endif // !RCT_NEW_ARCH_ENABLED
}
- (instancetype)initWithFrame:(CGRect)frame
{
if (self = [super initWithFrame:frame]) {
[self initState];
}
return self;
}
- (void)initState
{
#if RCT_NEW_ARCH_ENABLED
static const auto defaultProps = std::make_shared<const react::RNSBottomTabsScreenProps>();
_props = defaultProps;
#endif // RCT_NEW_ARCH_ENABLED
_controller = [RNSTabsScreenViewController new];
_controller.view = self;
_reactSuperview = nil;
_reactEventEmitter = [RNSBottomTabsScreenEventEmitter new];
#if !RCT_NEW_ARCH_ENABLED
_tabItemNeedsAppearanceUpdate = NO;
_tabScreenOrientationNeedsUpdate = NO;
_tabBarItemNeedsRecreation = NO;
_tabBarItemNeedsUpdate = NO;
_scrollEdgeEffectsNeedUpdate = NO;
#endif
[self resetProps];
}
- (void)resetProps
{
_isSelectedScreen = NO;
_badgeValue = nil;
_title = nil;
_isTitleUndefined = YES;
_orientation = RNSOrientationInherit;
_standardAppearance = [UITabBarAppearance new];
_scrollEdgeAppearance = nil;
_shouldUseRepeatedTabSelectionPopToRootSpecialEffect = YES;
_shouldUseRepeatedTabSelectionScrollToTopSpecialEffect = YES;
_overrideScrollViewContentInsetAdjustmentBehavior = YES;
_isOverrideScrollViewContentInsetAdjustmentBehaviorSet = NO;
_iconType = RNSBottomTabsIconTypeSfSymbol;
_iconImageSource = nil;
_iconResourceName = nil;
_selectedIconImageSource = nil;
_selectedIconResourceName = nil;
_systemItem = RNSBottomTabsScreenSystemItemNone;
_userInterfaceStyle = UIUserInterfaceStyleUnspecified;
}
RNS_IGNORE_SUPER_CALL_BEGIN
- (nullable RNSBottomTabsHostComponentView *)reactSuperview
{
return _reactSuperview;
}
RNS_IGNORE_SUPER_CALL_END
#ifdef RCT_NEW_ARCH_ENABLED
#pragma mark - RNSViewControllerInvalidating
- (void)invalidateController
{
_controller = nil;
}
- (BOOL)shouldInvalidateOnMutation:(const facebook::react::ShadowViewMutation &)mutation
{
// For bottom tabs, Host is responsible for invalidating children.
return NO;
}
#else
#pragma mark - RCTInvalidating
- (void)invalidate
{
_controller = nil;
}
#endif
#pragma mark - Events
- (nonnull RNSBottomTabsScreenEventEmitter *)reactEventEmitter
{
RCTAssert(_reactEventEmitter != nil, @"[RNScreens] Attempt to access uninitialized _reactEventEmitter");
return _reactEventEmitter;
}
- (nullable RNSTabBarController *)findTabBarController
{
return static_cast<RNSTabBarController *>(_controller.tabBarController);
}
#pragma mark - RNSScrollViewBehaviorOverriding
- (BOOL)shouldOverrideScrollViewContentInsetAdjustmentBehavior
{
return self.overrideScrollViewContentInsetAdjustmentBehavior;
}
- (void)overrideScrollViewBehaviorInFirstDescendantChainIfNeeded
{
if ([self shouldOverrideScrollViewContentInsetAdjustmentBehavior]) {
[RNSScrollViewHelper overrideScrollViewBehaviorInFirstDescendantChainFrom:self];
}
}
- (void)updateContentScrollViewEdgeEffectsIfExists
{
[RNSScrollEdgeEffectApplicator applyToScrollView:[RNSScrollViewFinder findScrollViewInFirstDescendantChainFrom:self]
withProvider:self];
}
#pragma mark - Prop update utils
- (void)createTabBarItem
{
UITabBarItem *tabBarItem = nil;
if (_systemItem != RNSBottomTabsScreenSystemItemNone) {
UITabBarSystemItem systemItem =
rnscreens::conversion::RNSBottomTabsScreenSystemItemToUITabBarSystemItem(_systemItem);
tabBarItem = [[UITabBarItem alloc] initWithTabBarSystemItem:systemItem tag:0];
} else {
tabBarItem = [[UITabBarItem alloc] init];
}
_controller.tabBarItem = tabBarItem;
}
- (void)updateTabBarItem
{
UITabBarItem *tabBarItem = _controller.tabBarItem;
NSString *evaluatedTitle = _title;
if (_title == nil && _systemItem != RNSBottomTabsScreenSystemItemNone) {
// Restore default system item title
UITabBarSystemItem systemItem =
rnscreens::conversion::RNSBottomTabsScreenSystemItemToUITabBarSystemItem(_systemItem);
evaluatedTitle = [[UITabBarItem alloc] initWithTabBarSystemItem:systemItem tag:0].title;
}
[self updateTabBarItemTitle:evaluatedTitle];
if (![tabBarItem.badgeValue isEqualToString:_badgeValue]) {
tabBarItem.badgeValue = _badgeValue;
}
}
- (void)updateTabBarItemTitle:(NSString *)newTitle
{
// Setting _controller.title updates also _controller.tabBarItem.title but only if there
// is a change to _controller.title. After creating new tabBarItem, _controller.title
// remains the same but _controller.tabBarItem.title is nil. For consistency, we always
// update both.
if (![_controller.tabBarItem.title isEqualToString:newTitle] || ![_controller.title isEqualToString:newTitle]) {
_controller.title = newTitle;
_controller.tabBarItem.title = newTitle;
}
}
#pragma mark - RNSSafeAreaProviding
- (UIEdgeInsets)providerSafeAreaInsets
{
return self.safeAreaInsets;
}
- (void)dispatchSafeAreaDidChangeNotification
{
[NSNotificationCenter.defaultCenter postNotificationName:RNSSafeAreaDidChange object:self userInfo:nil];
}
#pragma mark - RNSSafeAreaProviding related methods
// TODO: register for UIKeyboard notifications
- (void)safeAreaInsetsDidChange
{
[super safeAreaInsetsDidChange];
[self dispatchSafeAreaDidChangeNotification];
}
#if RCT_NEW_ARCH_ENABLED
#pragma mark - RCTComponentViewProtocol
- (void)updateProps:(const facebook::react::Props::Shared &)props
oldProps:(const facebook::react::Props::Shared &)oldProps
{
const auto &oldComponentProps = *std::static_pointer_cast<const react::RNSBottomTabsScreenProps>(_props);
const auto &newComponentProps = *std::static_pointer_cast<const react::RNSBottomTabsScreenProps>(props);
bool tabItemNeedsAppearanceUpdate{false};
bool tabScreenOrientationNeedsUpdate{false};
bool tabBarItemNeedsRecreation{false};
bool tabBarItemNeedsUpdate{false};
bool scrollEdgeEffectsNeedUpdate{false};
if (newComponentProps.title != oldComponentProps.title ||
newComponentProps.isTitleUndefined != oldComponentProps.isTitleUndefined) {
_isTitleUndefined = newComponentProps.isTitleUndefined;
if (_isTitleUndefined) {
_title = nil;
} else {
_title = RCTNSStringFromString(newComponentProps.title);
}
tabBarItemNeedsUpdate = YES;
}
if (newComponentProps.orientation != oldComponentProps.orientation) {
_orientation =
rnscreens::conversion::RNSOrientationFromRNSBottomTabsScreenOrientation(newComponentProps.orientation);
tabScreenOrientationNeedsUpdate = YES;
}
if (newComponentProps.tabKey != oldComponentProps.tabKey) {
RCTAssert(!newComponentProps.tabKey.empty(), @"[RNScreens] tabKey must not be empty!");
_tabKey = RCTNSStringFromString(newComponentProps.tabKey);
}
if (newComponentProps.isFocused != oldComponentProps.isFocused) {
_isSelectedScreen = newComponentProps.isFocused;
[_controller tabScreenFocusHasChanged];
}
if (newComponentProps.badgeValue != oldComponentProps.badgeValue) {
_badgeValue = RCTNSStringFromStringNilIfEmpty(newComponentProps.badgeValue);
tabBarItemNeedsUpdate = YES;
}
if (newComponentProps.tabBarItemTestID != oldComponentProps.tabBarItemTestID) {
_tabItemTestID = RCTNSStringFromStringNilIfEmpty(newComponentProps.tabBarItemTestID);
_tabBarItemNeedsA11yUpdate = YES;
}
if (newComponentProps.tabBarItemAccessibilityLabel != oldComponentProps.tabBarItemAccessibilityLabel) {
_tabItemAccessibilityLabel = RCTNSStringFromStringNilIfEmpty(newComponentProps.tabBarItemAccessibilityLabel);
_tabBarItemNeedsA11yUpdate = YES;
}
if (newComponentProps.standardAppearance != oldComponentProps.standardAppearance) {
_standardAppearance = [UITabBarAppearance new];
[RNSTabBarAppearanceCoordinator configureTabBarAppearance:_standardAppearance
fromAppearanceProps:rnscreens::conversion::RNSConvertFollyDynamicToId(
newComponentProps.standardAppearance)];
tabItemNeedsAppearanceUpdate = YES;
}
if (newComponentProps.scrollEdgeAppearance != oldComponentProps.scrollEdgeAppearance) {
if (newComponentProps.scrollEdgeAppearance.type() == folly::dynamic::OBJECT) {
_scrollEdgeAppearance = [UITabBarAppearance new];
[RNSTabBarAppearanceCoordinator configureTabBarAppearance:_scrollEdgeAppearance
fromAppearanceProps:rnscreens::conversion::RNSConvertFollyDynamicToId(
newComponentProps.scrollEdgeAppearance)];
} else {
_scrollEdgeAppearance = nil;
}
tabItemNeedsAppearanceUpdate = YES;
}
if (newComponentProps.iconType != oldComponentProps.iconType) {
_iconType = rnscreens::conversion::RNSBottomTabsIconTypeFromIcon(newComponentProps.iconType);
tabItemNeedsAppearanceUpdate = YES;
}
if (newComponentProps.iconImageSource != oldComponentProps.iconImageSource) {
_iconImageSource =
rnscreens::conversion::RCTImageSourceFromImageSourceAndIconType(&newComponentProps.iconImageSource, _iconType);
tabItemNeedsAppearanceUpdate = YES;
}
if (newComponentProps.iconResourceName != oldComponentProps.iconResourceName) {
_iconResourceName = RCTNSStringFromStringNilIfEmpty(newComponentProps.iconResourceName);
tabItemNeedsAppearanceUpdate = YES;
}
if (newComponentProps.selectedIconImageSource != oldComponentProps.selectedIconImageSource) {
_selectedIconImageSource = rnscreens::conversion::RCTImageSourceFromImageSourceAndIconType(
&newComponentProps.selectedIconImageSource, _iconType);
tabItemNeedsAppearanceUpdate = YES;
}
if (newComponentProps.selectedIconResourceName != oldComponentProps.selectedIconResourceName) {
_selectedIconResourceName = RCTNSStringFromStringNilIfEmpty(newComponentProps.selectedIconResourceName);
tabItemNeedsAppearanceUpdate = YES;
}
if (newComponentProps.specialEffects.repeatedTabSelection.popToRoot !=
oldComponentProps.specialEffects.repeatedTabSelection.popToRoot) {
_shouldUseRepeatedTabSelectionPopToRootSpecialEffect =
newComponentProps.specialEffects.repeatedTabSelection.popToRoot;
}
if (newComponentProps.specialEffects.repeatedTabSelection.scrollToTop !=
oldComponentProps.specialEffects.repeatedTabSelection.scrollToTop) {
_shouldUseRepeatedTabSelectionScrollToTopSpecialEffect =
newComponentProps.specialEffects.repeatedTabSelection.scrollToTop;
}
if (newComponentProps.overrideScrollViewContentInsetAdjustmentBehavior !=
oldComponentProps.overrideScrollViewContentInsetAdjustmentBehavior) {
_overrideScrollViewContentInsetAdjustmentBehavior =
newComponentProps.overrideScrollViewContentInsetAdjustmentBehavior;
if (_isOverrideScrollViewContentInsetAdjustmentBehaviorSet) {
RCTLogWarn(
@"[RNScreens] changing overrideScrollViewContentInsetAdjustmentBehavior dynamically is currently unsupported");
}
}
// This flag is set to YES when overrideScrollViewContentInsetAdjustmentBehavior prop
// is assigned for the first time. This allows us to identify any subsequent changes to this prop,
// enabling us to warn users that dynamic changes are not supported.
_isOverrideScrollViewContentInsetAdjustmentBehaviorSet = YES;
if (newComponentProps.systemItem != oldComponentProps.systemItem) {
_systemItem = rnscreens::conversion::RNSBottomTabsScreenSystemItemFromReactRNSBottomTabsScreenSystemItem(
newComponentProps.systemItem);
tabBarItemNeedsRecreation = YES;
}
if (newComponentProps.bottomScrollEdgeEffect != oldComponentProps.bottomScrollEdgeEffect) {
[self
setBottomScrollEdgeEffect:
rnscreens::conversion::RNSBottomTabsScrollEdgeEffectFromBottomTabsScreenBottomScrollEdgeEffectCppEquivalent(
newComponentProps.bottomScrollEdgeEffect)];
scrollEdgeEffectsNeedUpdate = YES;
}
if (newComponentProps.leftScrollEdgeEffect != oldComponentProps.leftScrollEdgeEffect) {
[self setLeftScrollEdgeEffect:
rnscreens::conversion::RNSBottomTabsScrollEdgeEffectFromBottomTabsScreenLeftScrollEdgeEffectCppEquivalent(
newComponentProps.leftScrollEdgeEffect)];
scrollEdgeEffectsNeedUpdate = YES;
}
if (newComponentProps.rightScrollEdgeEffect != oldComponentProps.rightScrollEdgeEffect) {
[self
setRightScrollEdgeEffect:
rnscreens::conversion::RNSBottomTabsScrollEdgeEffectFromBottomTabsScreenRightScrollEdgeEffectCppEquivalent(
newComponentProps.rightScrollEdgeEffect)];
scrollEdgeEffectsNeedUpdate = YES;
}
if (newComponentProps.topScrollEdgeEffect != oldComponentProps.topScrollEdgeEffect) {
[self setTopScrollEdgeEffect:rnscreens::conversion::
RNSBottomTabsScrollEdgeEffectFromBottomTabsScreenTopScrollEdgeEffectCppEquivalent(
newComponentProps.topScrollEdgeEffect)];
scrollEdgeEffectsNeedUpdate = YES;
}
if (newComponentProps.userInterfaceStyle != oldComponentProps.userInterfaceStyle) {
_userInterfaceStyle = rnscreens::conversion::UIUserInterfaceStyleFromBottomTabsScreenCppEquivalent(
newComponentProps.userInterfaceStyle);
}
if (tabBarItemNeedsRecreation) {
[self createTabBarItem];
tabBarItemNeedsUpdate = YES;
_tabBarItemNeedsA11yUpdate = YES;
}
if (tabBarItemNeedsUpdate) {
[self updateTabBarItem];
// Force appearance update to make sure correct image for tab bar item is used
tabItemNeedsAppearanceUpdate = YES;
}
if (tabItemNeedsAppearanceUpdate) {
[_controller tabItemAppearanceHasChanged];
}
if (tabScreenOrientationNeedsUpdate) {
[_controller tabScreenOrientationHasChanged];
}
if (scrollEdgeEffectsNeedUpdate) {
[self updateContentScrollViewEdgeEffectsIfExists];
scrollEdgeEffectsNeedUpdate = NO;
}
[super updateProps:props oldProps:oldProps];
}
- (void)updateLayoutMetrics:(const facebook::react::LayoutMetrics &)layoutMetrics
oldLayoutMetrics:(const facebook::react::LayoutMetrics &)oldLayoutMetrics
{
RNSLog(
@"TabScreen [%ld] updateLayoutMetrics: %@", self.tag, NSStringFromCGRect(RCTCGRectFromRect(layoutMetrics.frame)));
[super updateLayoutMetrics:layoutMetrics oldLayoutMetrics:oldLayoutMetrics];
}
- (void)updateEventEmitter:(const facebook::react::EventEmitter::Shared &)eventEmitter
{
[super updateEventEmitter:eventEmitter];
[_reactEventEmitter
updateEventEmitter:std::static_pointer_cast<const react::RNSBottomTabsScreenEventEmitter>(eventEmitter)];
}
- (void)mountChildComponentView:(UIView<RCTComponentViewProtocol> *)childComponentView index:(NSInteger)index
{
RNSLog(@"TabScreen [%ld] mount [%ld] at %ld", self.tag, childComponentView.tag, index);
[super mountChildComponentView:childComponentView index:index];
// overrideScrollViewBehavior and updateContentScrollViewEdgeEffects use first descendant chain
// from screen to find ScrollView, that's why we're only interested in child mounted at index 0.
if (index == 0) {
[self overrideScrollViewBehaviorInFirstDescendantChainIfNeeded];
[self updateContentScrollViewEdgeEffectsIfExists];
}
}
- (void)unmountChildComponentView:(UIView<RCTComponentViewProtocol> *)childComponentView index:(NSInteger)index
{
RNSLog(@"TabScreen [%ld] unmount [%ld] from %ld", self.tag, childComponentView.tag, index);
[super unmountChildComponentView:childComponentView index:index];
}
+ (react::ComponentDescriptorProvider)componentDescriptorProvider
{
return react::concreteComponentDescriptorProvider<react::RNSBottomTabsScreenComponentDescriptor>();
}
+ (BOOL)shouldBeRecycled
{
// There won't be tens of instances of this component usually & it's easier for now.
// We could consider enabling it someday though.
return NO;
}
#else
#pragma mark - LEGACY RCTComponent protocol
- (void)didSetProps:(NSArray<NSString *> *)changedProps
{
[super didSetProps:changedProps];
// This flag is set to YES when overrideScrollViewContentInsetAdjustmentBehavior prop
// is assigned for the first time. This allows us to identify any subsequent changes to this prop,
// enabling us to warn users that dynamic changes are not supported.
// On Paper, setter for the prop may not be called (when it is undefined in JS).
// Therefore we set the flag in didSetProps to make sure to handle this case as well.
// didSetProps will always be called because tabKey prop is required.
_isOverrideScrollViewContentInsetAdjustmentBehaviorSet = YES;
if (_tabBarItemNeedsRecreation) {
[self createTabBarItem];
_tabBarItemNeedsRecreation = NO;
_tabBarItemNeedsUpdate = YES;
_tabBarItemNeedsA11yUpdate = YES;
}
if (_tabBarItemNeedsUpdate) {
[self updateTabBarItem];
_tabBarItemNeedsUpdate = NO;
// Force appearance update to make sure correct image for tab bar item is used
_tabItemNeedsAppearanceUpdate = YES;
}
if (_tabItemNeedsAppearanceUpdate) {
[_controller tabItemAppearanceHasChanged];
_tabItemNeedsAppearanceUpdate = NO;
}
if (_tabScreenOrientationNeedsUpdate) {
[_controller tabScreenOrientationHasChanged];
_tabScreenOrientationNeedsUpdate = NO;
}
if (_scrollEdgeEffectsNeedUpdate) {
[self updateContentScrollViewEdgeEffectsIfExists];
_scrollEdgeEffectsNeedUpdate = NO;
}
}
#pragma mark - LEGACY prop setters
- (void)setIsSelectedScreen:(BOOL)isSelectedScreen
{
if (_isSelectedScreen != isSelectedScreen) {
_isSelectedScreen = isSelectedScreen;
[_controller tabScreenFocusHasChanged];
}
}
- (void)setTabKey:(NSString *)tabKey
{
RCTAssert([NSString rnscreens_isBlankOrNull:tabKey] == NO, @"[RNScreens] tabKey must not be empty");
_tabKey = tabKey;
}
- (void)setTitle:(NSString *)title
{
_title = title;
_isTitleUndefined = title == nil;
_tabBarItemNeedsUpdate = YES;
}
- (void)setBadgeValue:(NSString *)badgeValue
{
_badgeValue = [NSString rnscreens_stringOrNilIfBlank:badgeValue];
_tabBarItemNeedsUpdate = YES;
}
- (void)setTabBarItemTestID:(NSString *)tabBarItemTestID
{
_tabItemTestID = tabBarItemTestID;
_tabBarItemNeedsA11yUpdate = YES;
}
- (void)setTabBarItemAccessibilityLabel:(NSString *)tabBarItemAccessibilityLabel
{
_tabItemAccessibilityLabel = tabBarItemAccessibilityLabel;
_tabBarItemNeedsA11yUpdate = YES;
}
- (void)setIconType:(RNSBottomTabsIconType)iconType
{
_iconType = iconType;
_tabItemNeedsAppearanceUpdate = YES;
}
- (void)setIconImageSource:(RCTImageSource *)iconImageSource
{
_iconImageSource = iconImageSource;
_tabItemNeedsAppearanceUpdate = YES;
}
- (void)setIconResourceName:(NSString *)iconResourceName
{
_iconResourceName = [NSString rnscreens_stringOrNilIfEmpty:iconResourceName];
_tabItemNeedsAppearanceUpdate = YES;
}
- (void)setSelectedIconImageSource:(RCTImageSource *)selectedIconImageSource
{
_selectedIconImageSource = selectedIconImageSource;
_tabItemNeedsAppearanceUpdate = YES;
}
- (void)setSelectedIconResourceName:(NSString *)selectedIconResourceName
{
_selectedIconResourceName = [NSString rnscreens_stringOrNilIfEmpty:selectedIconResourceName];
_tabItemNeedsAppearanceUpdate = YES;
}
- (void)setBottomScrollEdgeEffect:(RNSScrollEdgeEffect)bottomScrollEdgeEffect
{
_bottomScrollEdgeEffect = bottomScrollEdgeEffect;
_scrollEdgeEffectsNeedUpdate = YES;
}
- (void)setLeftScrollEdgeEffect:(RNSScrollEdgeEffect)leftScrollEdgeEffect
{
_leftScrollEdgeEffect = leftScrollEdgeEffect;
_scrollEdgeEffectsNeedUpdate = YES;
}
- (void)setRightScrollEdgeEffect:(RNSScrollEdgeEffect)rightScrollEdgeEffect
{
_rightScrollEdgeEffect = rightScrollEdgeEffect;
_scrollEdgeEffectsNeedUpdate = YES;
}
- (void)setTopScrollEdgeEffect:(RNSScrollEdgeEffect)topScrollEdgeEffect
{
_topScrollEdgeEffect = topScrollEdgeEffect;
_scrollEdgeEffectsNeedUpdate = YES;
}
- (void)setOverrideScrollViewContentInsetAdjustmentBehavior:(BOOL)overrideScrollViewContentInsetAdjustmentBehavior
{
_overrideScrollViewContentInsetAdjustmentBehavior = overrideScrollViewContentInsetAdjustmentBehavior;
if (_isOverrideScrollViewContentInsetAdjustmentBehaviorSet) {
RCTLogWarn(
@"[RNScreens] changing overrideScrollViewContentInsetAdjustmentBehavior dynamically is currently unsupported");
}
// _isOverrideScrollViewContentInsetAdjustmentBehaviorSet flag is set in didSetProps to handle a case
// when the prop is undefined in JS and default value is used instead of calling this setter.
}
- (void)setStandardAppearance:(NSDictionary *)standardAppearanceProps
{
_standardAppearance = [UITabBarAppearance new];
if (standardAppearanceProps != nil) {
[RNSTabBarAppearanceCoordinator configureTabBarAppearance:_standardAppearance
fromAppearanceProps:standardAppearanceProps];
}
_tabItemNeedsAppearanceUpdate = YES;
}
- (void)setScrollEdgeAppearance:(NSDictionary *)scrollEdgeAppearanceProps
{
if (scrollEdgeAppearanceProps != nil) {
_scrollEdgeAppearance = [UITabBarAppearance new];
[RNSTabBarAppearanceCoordinator configureTabBarAppearance:_scrollEdgeAppearance
fromAppearanceProps:scrollEdgeAppearanceProps];
} else {
_scrollEdgeAppearance = nil;
}
_tabItemNeedsAppearanceUpdate = YES;
}
// This is a Paper-only setter method that will be called by the mounting code.
// It allows us to store UITabBarMinimizeBehavior in the component while accepting a custom enum as input from JS.
- (void)setSystemItem:(RNSBottomTabsScreenSystemItem)systemItem
{
_systemItem = systemItem;
_tabBarItemNeedsRecreation = YES;
}
- (void)setSpecialEffects:(NSDictionary *)specialEffects
{
if (specialEffects == nil || specialEffects[@"repeatedTabSelection"] == nil ||
![specialEffects[@"repeatedTabSelection"] isKindOfClass:[NSDictionary class]]) {
_shouldUseRepeatedTabSelectionPopToRootSpecialEffect = YES;
_shouldUseRepeatedTabSelectionScrollToTopSpecialEffect = YES;
return;
}
NSDictionary *repeatedTabSelection = specialEffects[@"repeatedTabSelection"];
if (repeatedTabSelection[@"popToRoot"] != nil) {
_shouldUseRepeatedTabSelectionPopToRootSpecialEffect = [RCTConvert BOOL:repeatedTabSelection[@"popToRoot"]];
} else {
_shouldUseRepeatedTabSelectionPopToRootSpecialEffect = YES;
}
if (repeatedTabSelection[@"scrollToTop"] != nil) {
_shouldUseRepeatedTabSelectionScrollToTopSpecialEffect = [RCTConvert BOOL:repeatedTabSelection[@"scrollToTop"]];
} else {
_shouldUseRepeatedTabSelectionScrollToTopSpecialEffect = YES;
}
}
- (void)setOrientation:(RNSOrientation)orientation
{
_orientation = orientation;
_tabScreenOrientationNeedsUpdate = YES;
}
- (void)setOnWillAppear:(RCTDirectEventBlock)onWillAppear
{
[self.reactEventEmitter setOnWillAppear:onWillAppear];
}
- (void)setOnWillDisappear:(RCTDirectEventBlock)onWillDisappear
{
[self.reactEventEmitter setOnWillDisappear:onWillDisappear];
}
- (void)setOnDidAppear:(RCTDirectEventBlock)onDidAppear
{
[self.reactEventEmitter setOnDidAppear:onDidAppear];
}
- (void)setOnDidDisappear:(RCTDirectEventBlock)onDidDisappear
{
[self.reactEventEmitter setOnDidDisappear:onDidDisappear];
}
#define RNS_FAILING_EVENT_GETTER(eventName) \
-(RCTDirectEventBlock)eventName \
{ \
RCTAssert(NO, @"[RNScreens] Events should be emitted through reactEventEmitter"); \
return nil; \
}
RNS_FAILING_EVENT_GETTER(onWillAppear);
RNS_FAILING_EVENT_GETTER(onDidAppear);
RNS_FAILING_EVENT_GETTER(onWillDisappear);
RNS_FAILING_EVENT_GETTER(onDidDisappear);
#undef RNS_FAILING_EVENT_GETTER
#endif // RCT_NEW_ARCH_ENABLED
@end
#if RCT_NEW_ARCH_ENABLED
#pragma mark - View class exposure
Class<RCTComponentViewProtocol> RNSBottomTabsScreen(void)
{
return RNSBottomTabsScreenComponentView.class;
}
#endif // RCT_NEW_ARCH_ENABLED

View File

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

View File

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

View File

@@ -0,0 +1,55 @@
#pragma once
#import <Foundation/Foundation.h>
// Hide C++ symbols from C compiler used when building Swift module
#if defined(__cplusplus) && RCT_NEW_ARCH_ENABLED
#import <react/renderer/components/rnscreens/EventEmitters.h>
namespace react = facebook::react;
#endif // __cplusplus
#if !RCT_NEW_ARCH_ENABLED
#import <React/RCTComponent.h>
#endif // !RCT_NEW_ARCH_ENABLED
NS_ASSUME_NONNULL_BEGIN
/**
* These methods can be called to send an appropriate event to ElementTree.
* Returned value denotes whether the event has been successfully dispatched to React event pipeline.
* The returned value of `true` does not mean, that the event has been successfully delivered.
*/
@interface RNSBottomTabsScreenEventEmitter : NSObject
- (BOOL)emitOnWillAppear;
- (BOOL)emitOnDidAppear;
- (BOOL)emitOnWillDisappear;
- (BOOL)emitOnDidDisappear;
@end
#pragma mark - Hidden from Swift
#if defined(__cplusplus)
@interface RNSBottomTabsScreenEventEmitter ()
#if RCT_NEW_ARCH_ENABLED
- (void)updateEventEmitter:(const std::shared_ptr<const react::RNSBottomTabsScreenEventEmitter> &)emitter;
#else
#pragma mark - LEGACY Event emitting blocks
@property (nonatomic, copy, nullable) RCTDirectEventBlock onWillAppear;
@property (nonatomic, copy, nullable) RCTDirectEventBlock onDidAppear;
@property (nonatomic, copy, nullable) RCTDirectEventBlock onWillDisappear;
@property (nonatomic, copy, nullable) RCTDirectEventBlock onDidDisappear;
#endif // RCT_NEW_ARCH_ENABLED
@end
#endif // __cplusplus
NS_ASSUME_NONNULL_END

View File

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

View File

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

View File

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

View File

@@ -0,0 +1,33 @@
#pragma once
// Hide C++ symbols from C compiler used when building Swift module
#if defined(__cplusplus)
#pragma mark - New architecture definitions
#if RCT_NEW_ARCH_ENABLED
#import <React/RCTViewComponentView.h>
@interface RNSReactBaseView : RCTViewComponentView
@end
#else
#pragma mark - Legacy architecture definitions
#import <React/RCTView.h>
@interface RNSReactBaseView : RCTView
@end
#endif // RCT_NEW_ARCH_ENABLED
#else
NS_ASSUME_NONNULL_BEGIN
@interface RNSReactBaseView : UIView
@end
NS_ASSUME_NONNULL_END
#endif // __cplusplus

View File

@@ -0,0 +1,5 @@
#import "RNSReactBaseView.h"
@implementation RNSReactBaseView
@end

View File

@@ -0,0 +1,7 @@
#pragma once
#if __has_include(<RNScreens/RNScreens-Swift.h>)
#import <RNScreens/RNScreens-Swift.h>
#else
#import "RNScreens-Swift.h"
#endif

View File

@@ -0,0 +1,488 @@
#import <React/RCTConversions.h>
#import <cxxreact/ReactNativeVersion.h>
#import <react/renderer/imagemanager/RCTImagePrimitivesConversions.h>
#import "RNSConversions.h"
#import "RNSDefines.h"
namespace rnscreens::conversion {
UIBlurEffect *RNSUIBlurEffectFromOptionalUIBlurEffectStyle(std::optional<UIBlurEffectStyle> &maybeStyle)
{
if (maybeStyle) {
return [UIBlurEffect effectWithStyle:maybeStyle.value()];
}
return nil;
}
std::optional<UIBlurEffectStyle> RNSMaybeUIBlurEffectStyleFromString(NSString *blurEffectString)
{
if ([blurEffectString isEqualToString:@"none"] || [blurEffectString isEqualToString:@"systemDefault"]) {
return std::nullopt;
} else if ([blurEffectString isEqualToString:@"extraLight"]) {
return {UIBlurEffectStyleExtraLight};
} else if ([blurEffectString isEqualToString:@"light"]) {
return {UIBlurEffectStyleLight};
} else if ([blurEffectString isEqualToString:@"dark"]) {
return {UIBlurEffectStyleDark};
} else if ([blurEffectString isEqualToString:@"regular"]) {
return {UIBlurEffectStyleRegular};
} else if ([blurEffectString isEqualToString:@"prominent"]) {
return {UIBlurEffectStyleProminent};
}
#if !TARGET_OS_TV
else if ([blurEffectString isEqualToString:@"systemUltraThinMaterial"]) {
return {UIBlurEffectStyleSystemUltraThinMaterial};
} else if ([blurEffectString isEqualToString:@"systemThinMaterial"]) {
return {UIBlurEffectStyleSystemThinMaterial};
} else if ([blurEffectString isEqualToString:@"systemMaterial"]) {
return {UIBlurEffectStyleSystemMaterial};
} else if ([blurEffectString isEqualToString:@"systemThickMaterial"]) {
return {UIBlurEffectStyleSystemThickMaterial};
} else if ([blurEffectString isEqualToString:@"systemChromeMaterial"]) {
return {UIBlurEffectStyleSystemChromeMaterial};
} else if ([blurEffectString isEqualToString:@"systemUltraThinMaterialLight"]) {
return {UIBlurEffectStyleSystemUltraThinMaterialLight};
} else if ([blurEffectString isEqualToString:@"systemThinMaterialLight"]) {
return {UIBlurEffectStyleSystemThinMaterialLight};
} else if ([blurEffectString isEqualToString:@"systemMaterialLight"]) {
return {UIBlurEffectStyleSystemMaterialLight};
} else if ([blurEffectString isEqualToString:@"systemThickMaterialLight"]) {
return {UIBlurEffectStyleSystemThickMaterialLight};
} else if ([blurEffectString isEqualToString:@"systemChromeMaterialLight"]) {
return {UIBlurEffectStyleSystemChromeMaterialLight};
} else if ([blurEffectString isEqualToString:@"systemUltraThinMaterialDark"]) {
return {UIBlurEffectStyleSystemUltraThinMaterialDark};
} else if ([blurEffectString isEqualToString:@"systemThinMaterialDark"]) {
return {UIBlurEffectStyleSystemThinMaterialDark};
} else if ([blurEffectString isEqualToString:@"systemMaterialDark"]) {
return {UIBlurEffectStyleSystemMaterialDark};
} else if ([blurEffectString isEqualToString:@"systemThickMaterialDark"]) {
return {UIBlurEffectStyleSystemThickMaterialDark};
} else if ([blurEffectString isEqualToString:@"systemChromeMaterialDark"]) {
return {UIBlurEffectStyleSystemChromeMaterialDark};
}
#endif // !TARGET_OS_TV
else {
#if !TARGET_OS_TV
RCTLogError(@"[RNScreens] Unsupported blur effect style: %@", blurEffectString);
#endif // !TARGET_OS_TV
return std::nullopt;
}
}
UIBlurEffect *RNSUIBlurEffectFromString(NSString *blurEffectString)
{
std::optional<UIBlurEffectStyle> maybeStyle = RNSMaybeUIBlurEffectStyleFromString(blurEffectString);
return RNSUIBlurEffectFromOptionalUIBlurEffectStyle(maybeStyle);
}
std::optional<UIBlurEffectStyle> RNSMaybeUIBlurEffectStyleFromRNSBlurEffectStyle(RNSBlurEffectStyle blurEffect)
{
switch (blurEffect) {
case RNSBlurEffectStyleNone:
case RNSBlurEffectStyleSystemDefault:
return std::nullopt;
case RNSBlurEffectStyleExtraLight:
return {UIBlurEffectStyleExtraLight};
case RNSBlurEffectStyleLight:
return {UIBlurEffectStyleLight};
case RNSBlurEffectStyleDark:
return {UIBlurEffectStyleDark};
case RNSBlurEffectStyleRegular:
return {UIBlurEffectStyleRegular};
case RNSBlurEffectStyleProminent:
return {UIBlurEffectStyleProminent};
#if !TARGET_OS_TV
case RNSBlurEffectStyleSystemUltraThinMaterial:
return {UIBlurEffectStyleSystemUltraThinMaterial};
case RNSBlurEffectStyleSystemThinMaterial:
return {UIBlurEffectStyleSystemThinMaterial};
case RNSBlurEffectStyleSystemMaterial:
return {UIBlurEffectStyleSystemMaterial};
case RNSBlurEffectStyleSystemThickMaterial:
return {UIBlurEffectStyleSystemThickMaterial};
case RNSBlurEffectStyleSystemChromeMaterial:
return {UIBlurEffectStyleSystemChromeMaterial};
case RNSBlurEffectStyleSystemUltraThinMaterialLight:
return {UIBlurEffectStyleSystemUltraThinMaterialLight};
case RNSBlurEffectStyleSystemThinMaterialLight:
return {UIBlurEffectStyleSystemThinMaterialLight};
case RNSBlurEffectStyleSystemMaterialLight:
return {UIBlurEffectStyleSystemMaterialLight};
case RNSBlurEffectStyleSystemThickMaterialLight:
return {UIBlurEffectStyleSystemThickMaterialLight};
case RNSBlurEffectStyleSystemChromeMaterialLight:
return {UIBlurEffectStyleSystemChromeMaterialLight};
case RNSBlurEffectStyleSystemUltraThinMaterialDark:
return {UIBlurEffectStyleSystemUltraThinMaterialDark};
case RNSBlurEffectStyleSystemThinMaterialDark:
return {UIBlurEffectStyleSystemThinMaterialDark};
case RNSBlurEffectStyleSystemMaterialDark:
return {UIBlurEffectStyleSystemMaterialDark};
case RNSBlurEffectStyleSystemThickMaterialDark:
return {UIBlurEffectStyleSystemThickMaterialDark};
case RNSBlurEffectStyleSystemChromeMaterialDark:
return {UIBlurEffectStyleSystemChromeMaterialDark};
default:
RCTLogError(@"[RNScreens] unsupported blur effect style");
return std::nullopt;
#else // !TARGET_OS_TV
default:
return std::nullopt;
#endif // !TARGET_OS_TV
}
}
UIBlurEffect *RNSUIBlurEffectFromRNSBlurEffectStyle(RNSBlurEffectStyle blurEffect)
{
std::optional<UIBlurEffectStyle> maybeStyle = RNSMaybeUIBlurEffectStyleFromRNSBlurEffectStyle(blurEffect);
return RNSUIBlurEffectFromOptionalUIBlurEffectStyle(maybeStyle);
}
#if RNS_IPHONE_OS_VERSION_AVAILABLE(26_0)
#if RCT_NEW_ARCH_ENABLED
API_AVAILABLE(ios(26.0))
UITabBarMinimizeBehavior UITabBarMinimizeBehaviorFromRNSBottomTabsTabBarMinimizeBehavior(
react::RNSBottomTabsTabBarMinimizeBehavior tabBarMinimizeBehavior)
{
using enum facebook::react::RNSBottomTabsTabBarMinimizeBehavior;
switch (tabBarMinimizeBehavior) {
case Never:
return UITabBarMinimizeBehaviorNever;
case OnScrollDown:
return UITabBarMinimizeBehaviorOnScrollDown;
case OnScrollUp:
return UITabBarMinimizeBehaviorOnScrollUp;
default:
return UITabBarMinimizeBehaviorAutomatic;
}
}
#else // RCT_NEW_ARCH_ENABLED
API_AVAILABLE(ios(26.0))
UITabBarMinimizeBehavior UITabBarMinimizeBehaviorFromRNSTabBarMinimizeBehavior(
RNSTabBarMinimizeBehavior tabBarMinimizeBehavior)
{
switch (tabBarMinimizeBehavior) {
case RNSTabBarMinimizeBehaviorNever:
return UITabBarMinimizeBehaviorNever;
case RNSTabBarMinimizeBehaviorOnScrollDown:
return UITabBarMinimizeBehaviorOnScrollDown;
case RNSTabBarMinimizeBehaviorOnScrollUp:
return UITabBarMinimizeBehaviorOnScrollUp;
default:
return UITabBarMinimizeBehaviorAutomatic;
}
}
#endif // RCT_NEW_ARCH_ENABLED
#endif // Check for iOS >= 26
#if RNS_IPHONE_OS_VERSION_AVAILABLE(18_0)
#if RCT_NEW_ARCH_ENABLED
API_AVAILABLE(ios(18.0))
UITabBarControllerMode UITabBarControllerModeFromRNSBottomTabsTabBarControllerMode(
react::RNSBottomTabsTabBarControllerMode tabBarControllerMode)
{
using enum facebook::react::RNSBottomTabsTabBarControllerMode;
switch (tabBarControllerMode) {
case Automatic:
return UITabBarControllerModeAutomatic;
case TabBar:
return UITabBarControllerModeTabBar;
case TabSidebar:
return UITabBarControllerModeTabSidebar;
default:
return UITabBarControllerModeAutomatic;
}
}
#else // RCT_NEW_ARCH_ENABLED
API_AVAILABLE(ios(18.0))
UITabBarControllerMode UITabBarControllerModeFromRNSTabBarControllerMode(RNSTabBarControllerMode tabBarDisplayMode)
{
switch (tabBarDisplayMode) {
case RNSTabBarControllerModeAutomatic:
return UITabBarControllerModeAutomatic;
case RNSTabBarControllerModeTabBar:
return UITabBarControllerModeTabBar;
case RNSTabBarControllerModeTabSidebar:
return UITabBarControllerModeTabSidebar;
default:
return UITabBarControllerModeAutomatic;
}
}
#endif // RCT_NEW_ARCH_ENABLED
#endif // Check for iOS >= 18
RNSBottomTabsIconType RNSBottomTabsIconTypeFromIcon(react::RNSBottomTabsScreenIconType iconType)
{
using enum facebook::react::RNSBottomTabsScreenIconType;
switch (iconType) {
case Image:
return RNSBottomTabsIconTypeImage;
case Template:
return RNSBottomTabsIconTypeTemplate;
case SfSymbol:
return RNSBottomTabsIconTypeSfSymbol;
case Xcasset:
return RNSBottomTabsIconTypeXcasset;
}
}
RCTImageSource *RCTImageSourceFromImageSourceAndIconType(
const facebook::react::ImageSource *imageSource,
RNSBottomTabsIconType iconType)
{
RCTImageSource *iconImageSource;
switch (iconType) {
case RNSBottomTabsIconTypeSfSymbol:
iconImageSource = nil;
break;
case RNSBottomTabsIconTypeImage:
case RNSBottomTabsIconTypeTemplate:
iconImageSource =
[[RCTImageSource alloc] initWithURLRequest:NSURLRequestFromImageSource(*imageSource)
size:CGSizeMake(imageSource->size.width, imageSource->size.height)
scale:imageSource->scale];
break;
default:
RCTLogError(@"[RNScreens] unsupported icon type");
}
return iconImageSource;
}
RNSOrientation RNSOrientationFromRNSBottomTabsScreenOrientation(react::RNSBottomTabsScreenOrientation orientation)
{
using enum facebook::react::RNSBottomTabsScreenOrientation;
switch (orientation) {
case Inherit:
return RNSOrientationInherit;
case All:
return RNSOrientationAll;
case AllButUpsideDown:
return RNSOrientationAllButUpsideDown;
case Portrait:
return RNSOrientationPortrait;
case PortraitUp:
return RNSOrientationPortraitUp;
case PortraitDown:
return RNSOrientationPortraitDown;
case Landscape:
return RNSOrientationLandscape;
case LandscapeLeft:
return RNSOrientationLandscapeLeft;
case LandscapeRight:
return RNSOrientationLandscapeRight;
default:
RCTLogError(@"[RNScreens] unsupported orientation");
return RNSOrientationInherit;
}
}
RNSBottomTabsScreenSystemItem RNSBottomTabsScreenSystemItemFromReactRNSBottomTabsScreenSystemItem(
react::RNSBottomTabsScreenSystemItem systemItem)
{
using enum facebook::react::RNSBottomTabsScreenSystemItem;
switch (systemItem) {
case None:
return RNSBottomTabsScreenSystemItemNone;
case Bookmarks:
return RNSBottomTabsScreenSystemItemBookmarks;
case Contacts:
return RNSBottomTabsScreenSystemItemContacts;
case Downloads:
return RNSBottomTabsScreenSystemItemDownloads;
case Favorites:
return RNSBottomTabsScreenSystemItemFavorites;
case Featured:
return RNSBottomTabsScreenSystemItemFeatured;
case History:
return RNSBottomTabsScreenSystemItemHistory;
case More:
return RNSBottomTabsScreenSystemItemMore;
case MostRecent:
return RNSBottomTabsScreenSystemItemMostRecent;
case MostViewed:
return RNSBottomTabsScreenSystemItemMostViewed;
case Recents:
return RNSBottomTabsScreenSystemItemRecents;
case Search:
return RNSBottomTabsScreenSystemItemSearch;
case TopRated:
return RNSBottomTabsScreenSystemItemTopRated;
default:
RCTLogError(@"[RNScreens] unsupported bottom tabs screen systemItem");
return RNSBottomTabsScreenSystemItemNone;
}
}
UITabBarSystemItem RNSBottomTabsScreenSystemItemToUITabBarSystemItem(RNSBottomTabsScreenSystemItem systemItem)
{
RCTAssert(
systemItem != RNSBottomTabsScreenSystemItemNone,
@"Attempt to convert bottom tabs systemItem none to UITabBarSystemItem");
switch (systemItem) {
case RNSBottomTabsScreenSystemItemBookmarks:
return UITabBarSystemItemBookmarks;
case RNSBottomTabsScreenSystemItemContacts:
return UITabBarSystemItemContacts;
case RNSBottomTabsScreenSystemItemDownloads:
return UITabBarSystemItemDownloads;
case RNSBottomTabsScreenSystemItemFavorites:
return UITabBarSystemItemFavorites;
case RNSBottomTabsScreenSystemItemFeatured:
return UITabBarSystemItemFeatured;
case RNSBottomTabsScreenSystemItemHistory:
return UITabBarSystemItemHistory;
case RNSBottomTabsScreenSystemItemMore:
return UITabBarSystemItemMore;
case RNSBottomTabsScreenSystemItemMostRecent:
return UITabBarSystemItemMostRecent;
case RNSBottomTabsScreenSystemItemMostViewed:
return UITabBarSystemItemMostViewed;
case RNSBottomTabsScreenSystemItemRecents:
return UITabBarSystemItemRecents;
case RNSBottomTabsScreenSystemItemSearch:
return UITabBarSystemItemSearch;
case RNSBottomTabsScreenSystemItemTopRated:
return UITabBarSystemItemTopRated;
}
RCTAssert(true, @"Attempt to convert unknown bottom tabs screen systemItem to UITabBarSystemItem [%d]", systemItem);
return UITabBarSystemItemSearch;
}
#define SWITCH_EDGE_EFFECT(X) \
switch (edgeEffect) { \
using enum react::X; \
case Automatic: \
return RNSScrollEdgeEffectAutomatic; \
case Hard: \
return RNSScrollEdgeEffectHard; \
case Soft: \
return RNSScrollEdgeEffectSoft; \
case Hidden: \
return RNSScrollEdgeEffectHidden; \
default: \
RCTLogError(@"[RNScreens] unsupported edge effect"); \
return RNSScrollEdgeEffectAutomatic; \
}
RNSScrollEdgeEffect RNSBottomTabsScrollEdgeEffectFromBottomTabsScreenBottomScrollEdgeEffectCppEquivalent(
react::RNSBottomTabsScreenBottomScrollEdgeEffect edgeEffect)
{
SWITCH_EDGE_EFFECT(RNSBottomTabsScreenBottomScrollEdgeEffect);
}
RNSScrollEdgeEffect RNSBottomTabsScrollEdgeEffectFromBottomTabsScreenLeftScrollEdgeEffectCppEquivalent(
react::RNSBottomTabsScreenLeftScrollEdgeEffect edgeEffect)
{
SWITCH_EDGE_EFFECT(RNSBottomTabsScreenLeftScrollEdgeEffect);
}
RNSScrollEdgeEffect RNSBottomTabsScrollEdgeEffectFromBottomTabsScreenRightScrollEdgeEffectCppEquivalent(
react::RNSBottomTabsScreenRightScrollEdgeEffect edgeEffect)
{
SWITCH_EDGE_EFFECT(RNSBottomTabsScreenRightScrollEdgeEffect);
}
RNSScrollEdgeEffect RNSBottomTabsScrollEdgeEffectFromBottomTabsScreenTopScrollEdgeEffectCppEquivalent(
react::RNSBottomTabsScreenTopScrollEdgeEffect edgeEffect)
{
SWITCH_EDGE_EFFECT(RNSBottomTabsScreenTopScrollEdgeEffect);
}
#undef SWITCH_EDGE_EFFECT
#if RNS_BOTTOM_ACCESSORY_AVAILABLE
#if RCT_NEW_ARCH_ENABLED
API_AVAILABLE(ios(26.0))
std::optional<react::RNSBottomTabsAccessoryEventEmitter::OnEnvironmentChangeEnvironment>
RNSBottomTabsAccessoryOnEnvironmentChangePayloadFromUITabAccessoryEnvironment(UITabAccessoryEnvironment environment)
{
switch (environment) {
case UITabAccessoryEnvironmentRegular:
return react::RNSBottomTabsAccessoryEventEmitter::OnEnvironmentChangeEnvironment::Regular;
case UITabAccessoryEnvironmentInline:
return react::RNSBottomTabsAccessoryEventEmitter::OnEnvironmentChangeEnvironment::Inline;
default:
// We want to ignore other environments (e.g. `none`), that's why there is no warning here.
return std::nullopt;
}
}
#if REACT_NATIVE_VERSION_MINOR >= 82
RNSBottomTabsAccessoryEnvironment RNSBottomTabsAccessoryEnvironmentFromCppEquivalent(
react::RNSBottomTabsAccessoryContentEnvironment environment)
{
using enum react::RNSBottomTabsAccessoryContentEnvironment;
switch (environment) {
case Regular:
return RNSBottomTabsAccessoryEnvironmentRegular;
case Inline:
return RNSBottomTabsAccessoryEnvironmentInline;
default:
RCTLogError(@"[RNScreens] Unsupported BottomTabsAccessory environment");
}
}
#endif // REACT_NATIVE_VERSION_MINOR >= 82
#else // RCT_NEW_ARCH_ENABLED
static NSString *const BOTTOM_TABS_ACCESSORY_REGULAR_ENVIRONMENT = @"regular";
static NSString *const BOTTOM_TABS_ACCESSORY_INLINE_ENVIRONMENT = @"inline";
API_AVAILABLE(ios(26.0))
NSString *RNSBottomTabsAccessoryOnEnvironmentChangePayloadFromUITabAccessoryEnvironment(
UITabAccessoryEnvironment environment)
{
NSString *environmentString;
switch (environment) {
case UITabAccessoryEnvironmentRegular:
environmentString = BOTTOM_TABS_ACCESSORY_REGULAR_ENVIRONMENT;
break;
case UITabAccessoryEnvironmentInline:
environmentString = BOTTOM_TABS_ACCESSORY_INLINE_ENVIRONMENT;
break;
default:
// We want to ignore other environments (e.g. `none`), that's why there is no warning here.
environmentString = nil;
break;
}
return environmentString;
}
#endif // RCT_NEW_ARCH_ENABLED
#endif // RNS_BOTTOM_ACCESSORY_AVAILABLE
UIUserInterfaceStyle UIUserInterfaceStyleFromBottomTabsScreenCppEquivalent(
react::RNSBottomTabsScreenUserInterfaceStyle userInterfaceStyle)
{
using enum facebook::react::RNSBottomTabsScreenUserInterfaceStyle;
switch (userInterfaceStyle) {
case Unspecified:
return UIUserInterfaceStyleUnspecified;
case Light:
return UIUserInterfaceStyleLight;
case Dark:
return UIUserInterfaceStyleDark;
default:
RCTLogError(@"[RNScreens] unsupported user interface style");
}
}
}; // namespace rnscreens::conversion

View File

@@ -0,0 +1,47 @@
#import "RNSConversions.h"
namespace rnscreens::conversion {
// copied from FollyConvert.mm
id RNSConvertFollyDynamicToId(const folly::dynamic &dyn)
{
// I could imagine an implementation which avoids copies by wrapping the
// dynamic in a derived class of NSDictionary. We can do that if profiling
// implies it will help.
switch (dyn.type()) {
case folly::dynamic::NULLT:
return nil;
case folly::dynamic::BOOL:
return dyn.getBool() ? @YES : @NO;
case folly::dynamic::INT64:
return @(dyn.getInt());
case folly::dynamic::DOUBLE:
return @(dyn.getDouble());
case folly::dynamic::STRING:
return [[NSString alloc] initWithBytes:dyn.c_str() length:dyn.size() encoding:NSUTF8StringEncoding];
case folly::dynamic::ARRAY: {
NSMutableArray *array = [[NSMutableArray alloc] initWithCapacity:dyn.size()];
for (const auto &elem : dyn) {
id value = RNSConvertFollyDynamicToId(elem);
if (value) {
[array addObject:value];
}
}
return array;
}
case folly::dynamic::OBJECT: {
NSMutableDictionary *dict = [[NSMutableDictionary alloc] initWithCapacity:dyn.size()];
for (const auto &elem : dyn.items()) {
id key = RNSConvertFollyDynamicToId(elem.first);
id value = RNSConvertFollyDynamicToId(elem.second);
if (key && value) {
dict[key] = value;
}
}
return dict;
}
}
}
}; // namespace rnscreens::conversion

View File

@@ -0,0 +1,164 @@
#import "RNSConversions.h"
namespace rnscreens::conversion {
#pragma mark SplitViewHost props
UISplitViewControllerSplitBehavior SplitViewPreferredSplitBehaviorFromHostProp(
facebook::react::RNSSplitViewHostPreferredSplitBehavior splitBehavior)
{
using enum facebook::react::RNSSplitViewHostPreferredSplitBehavior;
switch (splitBehavior) {
case Displace:
return UISplitViewControllerSplitBehaviorDisplace;
case Overlay:
return UISplitViewControllerSplitBehaviorOverlay;
case Tile:
return UISplitViewControllerSplitBehaviorTile;
case Automatic:
default:
return UISplitViewControllerSplitBehaviorAutomatic;
}
}
UISplitViewControllerPrimaryEdge SplitViewPrimaryEdgeFromHostProp(
facebook::react::RNSSplitViewHostPrimaryEdge primaryEdge)
{
using enum facebook::react::RNSSplitViewHostPrimaryEdge;
switch (primaryEdge) {
case Trailing:
return UISplitViewControllerPrimaryEdgeTrailing;
case Leading:
default:
return UISplitViewControllerPrimaryEdgeLeading;
}
}
UISplitViewControllerDisplayMode SplitViewPreferredDisplayModeFromHostProp(
facebook::react::RNSSplitViewHostPreferredDisplayMode displayMode)
{
using enum facebook::react::RNSSplitViewHostPreferredDisplayMode;
switch (displayMode) {
case SecondaryOnly:
return UISplitViewControllerDisplayModeSecondaryOnly;
case OneBesideSecondary:
return UISplitViewControllerDisplayModeOneBesideSecondary;
case OneOverSecondary:
return UISplitViewControllerDisplayModeOneOverSecondary;
case TwoBesideSecondary:
return UISplitViewControllerDisplayModeTwoBesideSecondary;
case TwoOverSecondary:
return UISplitViewControllerDisplayModeTwoOverSecondary;
case TwoDisplaceSecondary:
return UISplitViewControllerDisplayModeTwoDisplaceSecondary;
case Automatic:
default:
return UISplitViewControllerDisplayModeAutomatic;
}
}
#if !TARGET_OS_TV
UISplitViewControllerBackgroundStyle SplitViewPrimaryBackgroundStyleFromHostProp(
facebook::react::RNSSplitViewHostPrimaryBackgroundStyle primaryBackgroundStyle)
{
using enum facebook::react::RNSSplitViewHostPrimaryBackgroundStyle;
switch (primaryBackgroundStyle) {
case None:
return UISplitViewControllerBackgroundStyleNone;
case Sidebar:
return UISplitViewControllerBackgroundStyleSidebar;
case Default:
default:
UISplitViewController *tempSplitVC = [[UISplitViewController alloc] init];
return tempSplitVC.primaryBackgroundStyle;
}
}
#endif // !TARGET_OS_TV
UISplitViewControllerDisplayModeButtonVisibility SplitViewDisplayModeButtonVisibilityFromHostProp(
react::RNSSplitViewHostDisplayModeButtonVisibility displayModeButtonVisibility)
{
using enum facebook::react::RNSSplitViewHostDisplayModeButtonVisibility;
switch (displayModeButtonVisibility) {
case Always:
return UISplitViewControllerDisplayModeButtonVisibilityAlways;
case Never:
return UISplitViewControllerDisplayModeButtonVisibilityNever;
case Automatic:
default:
return UISplitViewControllerDisplayModeButtonVisibilityAutomatic;
}
}
std::string UISplitViewControllerDisplayModeToString(UISplitViewControllerDisplayMode displayMode)
{
switch (displayMode) {
case UISplitViewControllerDisplayModeSecondaryOnly:
return "secondaryOnly";
case UISplitViewControllerDisplayModeOneBesideSecondary:
return "oneBesideSecondary";
case UISplitViewControllerDisplayModeOneOverSecondary:
return "oneOverSecondary";
case UISplitViewControllerDisplayModeTwoBesideSecondary:
return "twoBesideSecondary";
case UISplitViewControllerDisplayModeTwoOverSecondary:
return "twoOverSecondary";
case UISplitViewControllerDisplayModeTwoDisplaceSecondary:
return "twoDisplaceSecondary";
case UISplitViewControllerDisplayModeAutomatic:
default:
return "automatic";
}
}
RNSOrientation RNSOrientationFromRNSSplitViewHostOrientation(react::RNSSplitViewHostOrientation orientation)
{
using enum facebook::react::RNSSplitViewHostOrientation;
switch (orientation) {
case Inherit:
return RNSOrientationInherit;
case All:
return RNSOrientationAll;
case AllButUpsideDown:
return RNSOrientationAllButUpsideDown;
case Portrait:
return RNSOrientationPortrait;
case PortraitUp:
return RNSOrientationPortraitUp;
case PortraitDown:
return RNSOrientationPortraitDown;
case Landscape:
return RNSOrientationLandscape;
case LandscapeLeft:
return RNSOrientationLandscapeLeft;
case LandscapeRight:
return RNSOrientationLandscapeRight;
default:
RCTLogError(@"[RNScreens] unsupported orientation");
return RNSOrientationInherit;
}
}
#pragma mark SplitViewScreen props
RNSSplitViewScreenColumnType RNSSplitViewScreenColumnTypeFromScreenProp(
facebook::react::RNSSplitViewScreenColumnType columnType)
{
using enum facebook::react::RNSSplitViewScreenColumnType;
switch (columnType) {
case Inspector:
return RNSSplitViewScreenColumnTypeInspector;
case Column:
default:
return RNSSplitViewScreenColumnTypeColumn;
}
}
}; // namespace rnscreens::conversion

View File

@@ -0,0 +1,23 @@
#pragma once
#if RCT_NEW_ARCH_ENABLED && RNS_GAMMA_ENABLED
#import <react/renderer/components/rnscreens/Props.h>
#import "RNSStackScreenComponentView.h"
#import "always_false.h"
namespace rnscreens::conversion {
template <typename TargetType, typename InputType>
TargetType convert(InputType) {
static_assert(
rnscreens::always_false<TargetType>::value,
"[RNScreens] Missing template specialisation for demanded types!");
}
template <>
RNSStackScreenActivityMode convert(react::RNSStackScreenActivityMode mode);
}; // namespace rnscreens::conversion
#endif // RCT_NEW_ARCH_ENABLED && RNS_GAMMA_ENABLED

View File

@@ -0,0 +1,17 @@
#import "RNSConversions-Stack.h"
#if RCT_NEW_ARCH_ENABLED && RNS_GAMMA_ENABLED
namespace rnscreens::conversion {
namespace react = facebook::react;
template <>
RNSStackScreenActivityMode convert(react::RNSStackScreenActivityMode mode)
{
return static_cast<RNSStackScreenActivityMode>(mode);
};
}; // namespace rnscreens::conversion
#endif // RCT_NEW_ARCH_ENABLED && RNS_GAMMA_ENABLED

View File

@@ -0,0 +1,144 @@
#pragma once
#if defined(__cplusplus)
#import <React/RCTImageSource.h>
#import <react/renderer/components/rnscreens/EventEmitters.h>
#import <react/renderer/components/rnscreens/Props.h>
#import "RNSDefines.h"
#import "RNSEnums.h"
#if RCT_NEW_ARCH_ENABLED
#import <folly/dynamic.h>
#endif // RCT_NEW_ARCH_ENABLED
namespace rnscreens::conversion {
namespace react = facebook::react;
#if RCT_NEW_ARCH_ENABLED
// copied from FollyConvert.mm
id RNSConvertFollyDynamicToId(const folly::dynamic &dyn);
#endif // RCT_NEW_ARCH_ENABLED
std::optional<UIBlurEffectStyle> RNSMaybeUIBlurEffectStyleFromString(NSString *blurEffectString);
UIBlurEffect *RNSUIBlurEffectFromString(NSString *blurEffectString);
std::optional<UIBlurEffectStyle> RNSMaybeUIBlurEffectStyleFromRNSBlurEffectStyle(RNSBlurEffectStyle blurEffect);
UIBlurEffect *RNSUIBlurEffectFromRNSBlurEffectStyle(RNSBlurEffectStyle blurEffect);
#if RNS_IPHONE_OS_VERSION_AVAILABLE(26_0)
#if RCT_NEW_ARCH_ENABLED
API_AVAILABLE(ios(26.0))
UITabBarMinimizeBehavior UITabBarMinimizeBehaviorFromRNSBottomTabsTabBarMinimizeBehavior(
react::RNSBottomTabsTabBarMinimizeBehavior tabBarMinimizeBehavior);
#else // RCT_NEW_ARCH_ENABLED
API_AVAILABLE(ios(26.0))
UITabBarMinimizeBehavior UITabBarMinimizeBehaviorFromRNSTabBarMinimizeBehavior(
RNSTabBarMinimizeBehavior tabBarMinimizeBehavior);
#endif // RCT_NEW_ARCH_ENABLED
#endif // Check for iOS >= 26
#if RNS_IPHONE_OS_VERSION_AVAILABLE(18_0)
#if RCT_NEW_ARCH_ENABLED
API_AVAILABLE(ios(18.0))
UITabBarControllerMode UITabBarControllerModeFromRNSBottomTabsTabBarControllerMode(
react::RNSBottomTabsTabBarControllerMode tabBarControllerMode);
#else // RCT_NEW_ARCH_ENABLED
API_AVAILABLE(ios(18.0))
UITabBarControllerMode UITabBarControllerModeFromRNSTabBarControllerMode(RNSTabBarControllerMode tabBarControllerMode);
#endif // RCT_NEW_ARCH_ENABLED
#endif // Check for iOS >= 18
RNSBottomTabsIconType RNSBottomTabsIconTypeFromIcon(react::RNSBottomTabsScreenIconType iconType);
RNSBottomTabsScreenSystemItem RNSBottomTabsScreenSystemItemFromReactRNSBottomTabsScreenSystemItem(
react::RNSBottomTabsScreenSystemItem systemItem);
UITabBarSystemItem RNSBottomTabsScreenSystemItemToUITabBarSystemItem(RNSBottomTabsScreenSystemItem systemItem);
RNSScrollEdgeEffect RNSBottomTabsScrollEdgeEffectFromBottomTabsScreenBottomScrollEdgeEffectCppEquivalent(
react::RNSBottomTabsScreenBottomScrollEdgeEffect edgeEffect);
RNSScrollEdgeEffect RNSBottomTabsScrollEdgeEffectFromBottomTabsScreenLeftScrollEdgeEffectCppEquivalent(
react::RNSBottomTabsScreenLeftScrollEdgeEffect edgeEffect);
RNSScrollEdgeEffect RNSBottomTabsScrollEdgeEffectFromBottomTabsScreenRightScrollEdgeEffectCppEquivalent(
react::RNSBottomTabsScreenRightScrollEdgeEffect edgeEffect);
RNSScrollEdgeEffect RNSBottomTabsScrollEdgeEffectFromBottomTabsScreenTopScrollEdgeEffectCppEquivalent(
react::RNSBottomTabsScreenTopScrollEdgeEffect edgeEffect);
#if RNS_BOTTOM_ACCESSORY_AVAILABLE
#if RCT_NEW_ARCH_ENABLED
API_AVAILABLE(ios(26.0))
std::optional<react::RNSBottomTabsAccessoryEventEmitter::OnEnvironmentChangeEnvironment>
RNSBottomTabsAccessoryOnEnvironmentChangePayloadFromUITabAccessoryEnvironment(UITabAccessoryEnvironment environment);
#if REACT_NATIVE_VERSION_MINOR >= 82
RNSBottomTabsAccessoryEnvironment RNSBottomTabsAccessoryEnvironmentFromCppEquivalent(
react::RNSBottomTabsAccessoryContentEnvironment environment);
#endif // REACT_NATIVE_VERSION_MINOR >= 82
#else // RCT_NEW_ARCH_ENABLED
API_AVAILABLE(ios(26.0))
NSString *_Nullable RNSBottomTabsAccessoryOnEnvironmentChangePayloadFromUITabAccessoryEnvironment(
UITabAccessoryEnvironment environment);
#endif // RCT_NEW_ARCH_ENABLED
#endif // RNS_BOTTOM_ACCESSORY_AVAILABLE
UIUserInterfaceStyle UIUserInterfaceStyleFromBottomTabsScreenCppEquivalent(
react::RNSBottomTabsScreenUserInterfaceStyle userInterfaceStyle);
RCTImageSource *RCTImageSourceFromImageSourceAndIconType(
const facebook::react::ImageSource *imageSource,
RNSBottomTabsIconType iconType);
RNSOrientation RNSOrientationFromRNSBottomTabsScreenOrientation(react::RNSBottomTabsScreenOrientation orientation);
#if !TARGET_OS_TV
UIInterfaceOrientationMask UIInterfaceOrientationMaskFromRNSOrientation(RNSOrientation orientation);
RNSOrientation RNSOrientationFromUIInterfaceOrientationMask(UIInterfaceOrientationMask orientationMask);
#endif // !TARGET_OS_TV
#pragma mark SplitViewHost props
UISplitViewControllerSplitBehavior SplitViewPreferredSplitBehaviorFromHostProp(
react::RNSSplitViewHostPreferredSplitBehavior behavior);
UISplitViewControllerPrimaryEdge SplitViewPrimaryEdgeFromHostProp(react::RNSSplitViewHostPrimaryEdge primaryEdge);
UISplitViewControllerDisplayMode SplitViewPreferredDisplayModeFromHostProp(
react::RNSSplitViewHostPreferredDisplayMode displayMode);
#if !TARGET_OS_TV
UISplitViewControllerBackgroundStyle SplitViewPrimaryBackgroundStyleFromHostProp(
react::RNSSplitViewHostPrimaryBackgroundStyle primaryBackgroundStyle);
#endif // !TARGET_OS_TV
UISplitViewControllerDisplayModeButtonVisibility SplitViewDisplayModeButtonVisibilityFromHostProp(
react::RNSSplitViewHostDisplayModeButtonVisibility displayModeButtonVisibility);
std::string UISplitViewControllerDisplayModeToString(UISplitViewControllerDisplayMode displayMode);
RNSOrientation RNSOrientationFromRNSSplitViewHostOrientation(react::RNSSplitViewHostOrientation orientation);
#pragma mark SplitViewScreen props
RNSSplitViewScreenColumnType RNSSplitViewScreenColumnTypeFromScreenProp(react::RNSSplitViewScreenColumnType columnType);
}; // namespace rnscreens::conversion
#if RCT_NEW_ARCH_ENABLED
#import "RNSConversions-Stack.h"
#endif // RCT_NEW_ARCH_ENABLED
#endif

Some files were not shown because too many files have changed in this diff Show More