chore: add mobile app

This commit is contained in:
2026-02-07 22:33:02 +01:00
parent db34640acc
commit e89384ad4e
29327 changed files with 3886944 additions and 12 deletions
@@ -0,0 +1,13 @@
#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
@@ -0,0 +1,48 @@
#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)
@end
#endif // !RCT_NEW_ARCH_ENABLED
@@ -0,0 +1,11 @@
#ifdef RCT_NEW_ARCH_ENABLED
#include <React/RCTImageComponentView.h>
@interface RCTImageComponentView (RNSScreenStackHeaderConfig)
- (UIImage *)image;
@end
#endif // RCT_NEW_ARCH_ENABLED
@@ -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
+56
View File
@@ -0,0 +1,56 @@
#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;
+ (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;
+ (NSMutableArray<NSNumber *> *)arrayFromVector:(const std::vector<CGFloat> &)vector;
+ (RNSBlurEffectStyle)RNSBlurEffectStyleFromCppEquivalent:(react::RNSScreenStackHeaderConfigBlurEffect)blurEffect;
#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
+276
View File
@@ -0,0 +1,276 @@
#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;
}
}
+ (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;
}
}
+ (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 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;
}
}
+ (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
}
}
#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
+175
View File
@@ -0,0 +1,175 @@
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, 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_AVAILABLE(tvos(10.0)) API_UNAVAILABLE(ios)
// API_UNAVAILABLE(watchos),
RNSBlurEffectStyleRegular API_AVAILABLE(ios(10.0)) API_UNAVAILABLE(watchos) = UIBlurEffectStyleRegular,
RNSBlurEffectStyleProminent API_AVAILABLE(ios(10.0)) API_UNAVAILABLE(watchos) = UIBlurEffectStyleProminent,
RNSBlurEffectStyleSystemUltraThinMaterial API_AVAILABLE(ios(13.0))
API_UNAVAILABLE(watchos, tvos) = UIBlurEffectStyleSystemUltraThinMaterial,
RNSBlurEffectStyleSystemThinMaterial API_AVAILABLE(ios(13.0))
API_UNAVAILABLE(watchos, tvos) = UIBlurEffectStyleSystemThinMaterial,
RNSBlurEffectStyleSystemMaterial API_AVAILABLE(ios(13.0))
API_UNAVAILABLE(watchos, tvos) = UIBlurEffectStyleSystemMaterial,
RNSBlurEffectStyleSystemThickMaterial API_AVAILABLE(ios(13.0))
API_UNAVAILABLE(watchos, tvos) = UIBlurEffectStyleSystemThickMaterial,
RNSBlurEffectStyleSystemChromeMaterial API_AVAILABLE(ios(13.0))
API_UNAVAILABLE(watchos, tvos) = UIBlurEffectStyleSystemChromeMaterial,
RNSBlurEffectStyleSystemUltraThinMaterialLight API_AVAILABLE(ios(13.0))
API_UNAVAILABLE(watchos, tvos) = UIBlurEffectStyleSystemUltraThinMaterialLight,
RNSBlurEffectStyleSystemThinMaterialLight API_AVAILABLE(ios(13.0))
API_UNAVAILABLE(watchos, tvos) = UIBlurEffectStyleSystemThinMaterialLight,
RNSBlurEffectStyleSystemMaterialLight API_AVAILABLE(ios(13.0))
API_UNAVAILABLE(watchos, tvos) = UIBlurEffectStyleSystemMaterialLight,
RNSBlurEffectStyleSystemThickMaterialLight API_AVAILABLE(ios(13.0))
API_UNAVAILABLE(watchos, tvos) = UIBlurEffectStyleSystemThickMaterialLight,
RNSBlurEffectStyleSystemChromeMaterialLight API_AVAILABLE(ios(13.0))
API_UNAVAILABLE(watchos, tvos) = UIBlurEffectStyleSystemChromeMaterialLight,
RNSBlurEffectStyleSystemUltraThinMaterialDark API_AVAILABLE(ios(13.0))
API_UNAVAILABLE(watchos, tvos) = UIBlurEffectStyleSystemUltraThinMaterialDark,
RNSBlurEffectStyleSystemThinMaterialDark API_AVAILABLE(ios(13.0))
API_UNAVAILABLE(watchos, tvos) = UIBlurEffectStyleSystemThinMaterialDark,
RNSBlurEffectStyleSystemMaterialDark API_AVAILABLE(ios(13.0))
API_UNAVAILABLE(watchos, tvos) = UIBlurEffectStyleSystemMaterialDark,
RNSBlurEffectStyleSystemThickMaterialDark API_AVAILABLE(ios(13.0))
API_UNAVAILABLE(watchos, tvos) = UIBlurEffectStyleSystemThickMaterialDark,
RNSBlurEffectStyleSystemChromeMaterialDark API_AVAILABLE(ios(13.0))
API_UNAVAILABLE(watchos, tvos) = UIBlurEffectStyleSystemChromeMaterialDark
} API_AVAILABLE(ios(8.0)) API_UNAVAILABLE(watchos);
typedef NS_ENUM(NSInteger, RNSBottomTabsIconType) {
RNSBottomTabsIconTypeImage,
RNSBottomTabsIconTypeTemplate,
RNSBottomTabsIconTypeSfSymbol,
};
#if !RCT_NEW_ARCH_ENABLED
typedef NS_ENUM(NSInteger, RNSTabBarMinimizeBehavior) {
RNSTabBarMinimizeBehaviorAutomatic,
RNSTabBarMinimizeBehaviorNever,
RNSTabBarMinimizeBehaviorOnScrollDown,
RNSTabBarMinimizeBehaviorOnScrollUp,
};
#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
};
@@ -0,0 +1,36 @@
#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
@@ -0,0 +1,284 @@
#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;
[self show];
}
- (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)show
{
UIWindow *window = RCTKeyWindow();
[window addSubview:_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];
}
}
#ifdef RCT_NEW_ARCH_ENABLED
#pragma mark - Fabric Specific
// When the component unmounts we remove it from window's children,
// so when the component gets recycled we need to add it back.
- (void)maybeShow
{
UIWindow *window = RCTKeyWindow();
if (![[window subviews] containsObject:self]) {
[window addSubview:_container];
}
}
+ (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
{
// When the component unmounts we remove it from window's children,
// so when the component gets recycled we need to add it back.
// As for now it is called here as we lack of method that is called
// just before component gets restored (from recycle pool).
[self maybeShow];
[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
@@ -0,0 +1,13 @@
#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
@@ -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
@@ -0,0 +1,8 @@
#import "RNSScreen.h"
@interface RNSModalScreen : RNSScreenView
@end
@interface RNSModalScreenManager : RNSScreenManager
@end
+65
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
+19
View File
@@ -0,0 +1,19 @@
#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
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
@@ -0,0 +1,13 @@
#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
@@ -0,0 +1,12 @@
#import <UIKit/UIKit.h>
#import "RNSScreenStackAnimator.h"
NS_ASSUME_NONNULL_BEGIN
@interface RNSPercentDrivenInteractiveTransition : UIPercentDrivenInteractiveTransition
@property (nonatomic, nullable) RNSScreenStackAnimator *animationController;
@end
NS_ASSUME_NONNULL_END
@@ -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
+187
View File
@@ -0,0 +1,187 @@
#import <React/RCTComponent.h>
#import <React/RCTViewManager.h>
#import "RNSEnums.h"
#import "RNSScreenContainer.h"
#import "RNSScreenContentWrapper.h"
#import "RNSScrollViewBehaviorOverriding.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
@end
@class RNSScreenStackHeaderConfig;
@interface RNSScreenView :
#ifdef RCT_NEW_ARCH_ENABLED
RCTViewComponentView
#else
RCTView
#endif
<RNSScreenContentWrapperDelegate, RNSScrollViewBehaviorOverriding>
@property (nonatomic) BOOL fullScreenSwipeEnabled;
@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, 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;
#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;
/**
* Tell `Screen` component that it has been removed from react state and can safely cleanup
* any retained resources.
*
* Note, that on old architecture this method might be called by RN via `RCTInvalidating` protocol.
*/
- (void)invalidate;
/**
* 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
File diff suppressed because it is too large Load Diff
@@ -0,0 +1,45 @@
#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
@@ -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 - RCTViewComponentViewProtocol
- (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
@@ -0,0 +1,50 @@
#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
@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;
- (nullable RNS_REACT_SCROLL_VIEW_COMPONENT *)childRCTScrollViewComponent;
- (BOOL)coerceChildScrollViewComponentSizeToSize:(CGSize)size;
@end
@interface RNSScreenContentWrapperManager : RCTViewManager
@end
NS_ASSUME_NONNULL_END
@@ -0,0 +1,205 @@
#import "RNSScreenContentWrapper.h"
#import "RNSDefines.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);
}
- (nullable RNS_REACT_SCROLL_VIEW_COMPONENT *)childRCTScrollViewComponent
{
for (UIView *subview in self.subviews) {
if ([subview isKindOfClass:RNS_REACT_SCROLL_VIEW_COMPONENT.class]) {
return static_cast<RNS_REACT_SCROLL_VIEW_COMPONENT *>(subview);
}
}
return nil;
}
- (BOOL)coerceChildScrollViewComponentSizeToSize:(CGSize)size
{
RNS_REACT_SCROLL_VIEW_COMPONENT *_Nullable scrollViewComponent = [self childRCTScrollViewComponent];
if (scrollViewComponent == nil) {
return NO;
}
if (self.subviews.count > 2) {
RCTLogWarn(
@"[RNScreens] FormSheet with ScrollView expects at most 2 subviews. Got %ld. 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.",
self.subviews.count);
}
NSUInteger scrollViewComponentIndex = [[self 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 = self.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
+30
View File
@@ -0,0 +1,30 @@
#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
+142
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
@@ -0,0 +1,16 @@
#import <React/RCTViewManager.h>
#import "RNSScreenContainer.h"
#import "RNSScreenStack.h"
@interface RNSContainerNavigationController : RNSNavigationController
@end
@interface RNSScreenNavigationContainerView : RNSScreenContainerView
@end
@interface RNSScreenNavigationContainerManager : RNSScreenContainerManager
@end
@@ -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
+75
View File
@@ -0,0 +1,75 @@
#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
#ifdef RNS_GAMMA_ENABLED
#import "RNSFrameCorrectionProvider.h"
#endif // RNS_GAMMA_ENABLED
NS_ASSUME_NONNULL_BEGIN
@interface RNSNavigationController : UINavigationController <
RNSViewControllerDelegate,
RNSBottomTabsSpecialEffectsSupporting
#if !TARGET_OS_TV
,
RNSOrientationProviding
#endif // !TARGET_OS_TV
#ifdef RNS_GAMMA_ENABLED
,
RNSFrameCorrectionProvider
#endif // RNS_GAMMA_ENABLED
>
@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;
#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
File diff suppressed because it is too large Load Diff
@@ -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
@@ -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.fullScreenSwipeEnabled && 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
@@ -0,0 +1,156 @@
#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;
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
@interface RNSScreenStackHeaderConfigManager : RCTViewManager
@end
#ifdef RCT_NEW_ARCH_ENABLED
#else
/**
* 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
@interface RCTConvert (RNSScreenStackHeader)
+ (UISemanticContentAttribute)UISemanticContentAttribute:(nonnull id)json;
+ (UINavigationItemBackButtonDisplayMode)UINavigationItemBackButtonDisplayMode:(nonnull id)json;
@end
File diff suppressed because it is too large Load Diff
@@ -0,0 +1,64 @@
#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, 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
@end
@interface RNSScreenStackHeaderSubviewManager : RCTViewManager
@property (nonatomic) RNSScreenStackHeaderSubviewType type;
@end
@interface RCTConvert (RNSScreenStackHeaderSubview)
+ (RNSScreenStackHeaderSubviewType)RNSScreenStackHeaderSubviewType:(id)json;
@end
NS_ASSUME_NONNULL_END
@@ -0,0 +1,233 @@
#import "RNSScreenStackHeaderSubview.h"
#import "RNSConvert.h"
#import "RNSDefines.h"
#import "RNSScreenStackHeaderConfig.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/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 {
#ifdef RCT_NEW_ARCH_ENABLED
react::RNSScreenStackHeaderSubviewShadowNode::ConcreteState::Shared _state;
CGRect _lastScheduledFrame;
#endif
}
#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.frame];
}
- (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));
_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)prepareForRecycle
{
[super prepareForRecycle];
}
- (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]];
[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 {
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)) {
[super reactSetFrame:CGRectMake(0, 0, frame.size.width, frame.size.height)];
[self layoutNavigationBar];
}
}
#endif // RCT_NEW_ARCH_ENABLED
@end
@implementation RNSScreenStackHeaderSubviewManager
RCT_EXPORT_MODULE()
RCT_EXPORT_VIEW_PROPERTY(type, RNSScreenStackHeaderSubviewType)
#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
@@ -0,0 +1,26 @@
#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
@@ -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
@@ -0,0 +1,24 @@
#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
@@ -0,0 +1,13 @@
#import <UIKit/UIKit.h>
@interface RNSScrollViewFinder : NSObject
/**
* Finds UIScrollView by traversing down the hierarchy using first subview, similar to UIKit behavior.
* It will fail if:
* - UIScrollView is not a first subview of view or one of its descendants in the hierarchy,
* - if UIScrollView's parent is not yet attached.
*/
+ (nullable UIScrollView *)findScrollViewInFirstDescendantChainFrom:(nullable UIView *)view;
@end
@@ -0,0 +1,22 @@
#import "RNSScrollViewFinder.h"
@implementation RNSScrollViewFinder
+ (UIScrollView *)findScrollViewInFirstDescendantChainFrom:(UIView *)view
{
UIView *currentView = view;
while (currentView != nil) {
if ([currentView isKindOfClass:UIScrollView.class]) {
return static_cast<UIScrollView *>(currentView);
} else if ([currentView.subviews count] > 0) {
currentView = currentView.subviews[0];
} else {
break;
}
}
return nil;
}
@end
@@ -0,0 +1,10 @@
#import <UIKit/UIKit.h>
@interface RNSScrollViewHelper : NSObject
/**
* Finds and overrides contentInsetAdjustmentBehavior for first ScrollView in first descendant chain from view.
*/
+ (void)overrideScrollViewBehaviorInFirstDescendantChainFrom:(nullable UIView *)view;
@end
@@ -0,0 +1,15 @@
#import "RNSScrollViewHelper.h"
#import "RNSScrollViewFinder.h"
@implementation RNSScrollViewHelper
+ (void)overrideScrollViewBehaviorInFirstDescendantChainFrom:(nullable UIView *)view
{
UIScrollView *scrollView = [RNSScrollViewFinder findScrollViewInFirstDescendantChainFrom:view];
if ([scrollView contentInsetAdjustmentBehavior] == UIScrollViewContentInsetAdjustmentNever) {
[scrollView setContentInsetAdjustmentBehavior:UIScrollViewContentInsetAdjustmentAutomatic];
}
}
@end
+45
View File
@@ -0,0 +1,45 @@
#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
+535
View File
@@ -0,0 +1,535 @@
#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;
}
@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;
_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:(BOOL)obscureBackground
{
if (@available(iOS 9.1, *)) {
[_controller setObscuresBackgroundDuringPresentation:obscureBackground];
}
}
- (void)setHideNavigationBar:(BOOL)hideNavigationBar
{
[_controller setHidesNavigationBarDuringPresentation:hideNavigationBar];
}
- (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:newScreenProps.obscureBackground];
}
if (oldScreenProps.hideNavigationBar != newScreenProps.hideNavigationBar) {
[self setHideNavigationBar: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, BOOL)
RCT_EXPORT_VIEW_PROPERTY(hideNavigationBar, BOOL)
RCT_EXPORT_VIEW_PROPERTY(hideWhenScrolling, BOOL)
RCT_EXPORT_VIEW_PROPERTY(autoCapitalize, UITextAutocapitalizationType)
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
@@ -0,0 +1,13 @@
#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
@@ -0,0 +1,15 @@
#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
@@ -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
@@ -0,0 +1,4 @@
//
// Use this file to import your target's public headers that you would like to
// expose to Swift.
//
@@ -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 */;
}
@@ -0,0 +1,14 @@
#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
@@ -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
@@ -0,0 +1,9 @@
#import <UIKit/UIKit.h>
NS_ASSUME_NONNULL_BEGIN
@interface UIViewController (RNScreens)
@end
NS_ASSUME_NONNULL_END
@@ -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
@@ -0,0 +1,9 @@
#import <UIKit/UIKit.h>
NS_ASSUME_NONNULL_BEGIN
@interface UIWindow (RNScreens)
@end
NS_ASSUME_NONNULL_END
@@ -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
@@ -0,0 +1,21 @@
#import <React/RCTConvert.h>
#import <UIKit/UIKit.h>
#if !RCT_NEW_ARCH_ENABLED
#import "RNSEnums.h"
#endif // !RCT_NEW_ARCH_ENABLED
NS_ASSUME_NONNULL_BEGIN
@interface RCTConvert (RNSBottomTabs)
+ (UIOffset)UIOffset:(nonnull id)json;
#if !RCT_NEW_ARCH_ENABLED
+ (RNSBottomTabsIconType)RNSBottomTabsIconType:(nonnull id)json;
+ (RNSOrientation)RNSOrientation:(nonnull id)json;
#endif // !RCT_NEW_ARCH_ENABLED
@end
NS_ASSUME_NONNULL_END
@@ -0,0 +1,71 @@
#import "RCTConvert+RNSBottomTabs.h"
@implementation RCTConvert (RNSBottomTabs)
+ (UIOffset)UIOffset:(id)json;
{
json = [self NSDictionary:json];
return UIOffsetMake([json[@"horizontal"] floatValue], [json[@"vertical"] floatValue]);
}
#if !RCT_NEW_ARCH_ENABLED
RCT_ENUM_CONVERTER(
RNSBottomTabsIconType,
(@{
@"image" : @(RNSBottomTabsIconTypeImage),
@"template" : @(RNSBottomTabsIconTypeTemplate),
@"sfSymbol" : @(RNSBottomTabsIconTypeSfSymbol),
}),
RNSBottomTabsIconTypeSfSymbol,
integerValue)
RCT_ENUM_CONVERTER(
RNSTabBarMinimizeBehavior,
(@{
@"automatic" : @(RNSTabBarMinimizeBehaviorAutomatic),
@"never" : @(RNSTabBarMinimizeBehaviorNever),
@"onScrollDown" : @(RNSTabBarMinimizeBehaviorOnScrollDown),
@"onScrollUp" : @(RNSTabBarMinimizeBehaviorOnScrollUp),
}),
RNSTabBarMinimizeBehaviorAutomatic,
integerValue)
RCT_ENUM_CONVERTER(
RNSOrientation,
(@{
@"inherit" : @(RNSOrientationInherit),
@"all" : @(RNSOrientationAll),
@"allButUpsideDown" : @(RNSOrientationAllButUpsideDown),
@"portrait" : @(RNSOrientationPortrait),
@"portraitUp" : @(RNSOrientationPortraitUp),
@"portraitDown" : @(RNSOrientationPortraitDown),
@"landscape" : @(RNSOrientationLandscape),
@"landscapeLeft" : @(RNSOrientationLandscapeLeft),
@"landscapeRight" : @(RNSOrientationLandscapeRight),
}),
RNSOrientationInherit,
integerValue)
RCT_ENUM_CONVERTER(
RNSBottomTabsScreenSystemItem,
(@{
@"none" : @(RNSBottomTabsScreenSystemItemNone),
@"bookmarks" : @(RNSBottomTabsScreenSystemItemBookmarks),
@"contacts" : @(RNSBottomTabsScreenSystemItemContacts),
@"downloads" : @(RNSBottomTabsScreenSystemItemDownloads),
@"favorites" : @(RNSBottomTabsScreenSystemItemFavorites),
@"featured" : @(RNSBottomTabsScreenSystemItemFeatured),
@"history" : @(RNSBottomTabsScreenSystemItemHistory),
@"more" : @(RNSBottomTabsScreenSystemItemMore),
@"mostRecent" : @(RNSBottomTabsScreenSystemItemMostRecent),
@"mostViewed" : @(RNSBottomTabsScreenSystemItemMostViewed),
@"recents" : @(RNSBottomTabsScreenSystemItemRecents),
@"search" : @(RNSBottomTabsScreenSystemItemSearch),
@"topRated" : @(RNSBottomTabsScreenSystemItemTopRated),
}),
RNSBottomTabsScreenSystemItemNone,
integerValue)
#endif // !RCT_NEW_ARCH_ENABLED
@end
@@ -0,0 +1,87 @@
#import "RNSBottomTabsHostComponentViewManager.h"
#import "RNSBottomTabsHostEventEmitter.h"
#import "RNSDefines.h"
#import "RNSEnums.h"
#import "RNSReactBaseView.h"
#import "RNSScreenContainer.h"
#ifdef RCT_NEW_ARCH_ENABLED
#import "RNSViewControllerInvalidating.h"
#else
#import <React/RCTInvalidating.h>
#endif
NS_ASSUME_NONNULL_BEGIN
@class RNSBottomTabsScreenComponentView;
@class RNSTabBarController;
@class RCTImageLoader;
/**
* Component view. Lifecycle is managed by React Native.
*
* This component serves as:
* 1. host for UITabBarController
* 2. provider of React state & props for the tab bar controller
* 3. two way communication channel with React (commands & events)
*/
@interface RNSBottomTabsHostComponentView : RNSReactBaseView <
RNSScreenContainerDelegate,
#ifdef RCT_NEW_ARCH_ENABLED
RNSViewControllerInvalidating
#else
RCTInvalidating
#endif
>
#if !RCT_NEW_ARCH_ENABLED
- (instancetype)initWithFrame:(CGRect)frame reactImageLoader:(RCTImageLoader *)imageLoader;
#endif // !RCT_NEW_ARCH_ENABLED
@property (nonatomic, nonnull, strong, readonly) RNSTabBarController *controller;
@end
#pragma mark - Props
@interface RNSBottomTabsHostComponentView ()
@property (nonatomic, strong, readonly, nullable) UIColor *tabBarTintColor;
@property (nonatomic, readonly) BOOL experimental_controlNavigationStateInJS;
#if RNS_IPHONE_OS_VERSION_AVAILABLE(26_0)
@property (nonatomic, readonly) UITabBarMinimizeBehavior tabBarMinimizeBehavior API_AVAILABLE(ios(26.0));
#endif // Check for iOS >= 26
@end
#pragma mark - React Events
@interface RNSBottomTabsHostComponentView ()
/**
* Use returned object to emit appropriate React Events to Element Tree.
*/
- (nonnull RNSBottomTabsHostEventEmitter *)reactEventEmitter;
- (BOOL)emitOnNativeFocusChangeRequestSelectedTabScreen:(nonnull RNSBottomTabsScreenComponentView *)tabScreen;
#if !RCT_NEW_ARCH_ENABLED
#pragma mark - LEGACY Event blocks
@property (nonatomic, copy) RCTDirectEventBlock onNativeFocusChange;
#endif
@end
#pragma mark - React Image Loader
@interface RNSBottomTabsHostComponentView ()
- (nullable RCTImageLoader *)reactImageLoader;
@end
NS_ASSUME_NONNULL_END
@@ -0,0 +1,458 @@
#import "RNSBottomTabsHostComponentView.h"
#if RCT_NEW_ARCH_ENABLED
#import <React/RCTConversions.h>
#import <React/RCTImageLoader.h>
#import <React/RCTMountingTransactionObserving.h>
#import <react/renderer/components/rnscreens/ComponentDescriptors.h>
#import <react/renderer/components/rnscreens/EventEmitters.h>
#import <react/renderer/components/rnscreens/Props.h>
#import <react/renderer/components/rnscreens/RCTComponentViewHelpers.h>
#import <rnscreens/RNSBottomTabsComponentDescriptor.h>
#import "RNSBottomTabsHostComponentView+RNSImageLoader.h"
#import "RNSInvalidatedComponentsRegistry.h"
#import "RNSViewControllerInvalidator.h"
#endif // RCT_NEW_ARCH_ENABLED
#import "RNSBottomTabsScreenComponentView.h"
#import "RNSConversions.h"
#import "RNSConvert.h"
#import "RNSDefines.h"
#import "RNSLog.h"
#import "RNSTabBarController.h"
#import "RNSTabBarControllerDelegate.h"
namespace react = facebook::react;
#pragma mark - View implementation
@interface RNSBottomTabsHostComponentView ()
#if RCT_NEW_ARCH_ENABLED
<RCTMountingTransactionObserving>
#endif // RCT_NEW_ARCH_ENABLED
@end
@implementation RNSBottomTabsHostComponentView {
RNSTabBarController *_Nonnull _controller;
RNSTabBarControllerDelegate *_controllerDelegate;
RNSBottomTabsHostEventEmitter *_Nonnull _reactEventEmitter;
RCTImageLoader *_Nullable _imageLoader;
#if RCT_NEW_ARCH_ENABLED
RNSInvalidatedComponentsRegistry *_Nonnull _invalidatedComponentsRegistry;
#endif // RCT_NEW_ARCH_ENABLED
// RCTViewComponentView does not expose this field, therefore we maintain
// it on our side.
NSMutableArray<RNSBottomTabsScreenComponentView *> *_reactSubviews;
BOOL _hasModifiedReactSubviewsInCurrentTransaction;
BOOL _needsTabBarAppearanceUpdate;
}
- (instancetype)initWithFrame:(CGRect)frame
{
if (self = [super initWithFrame:frame]) {
[self initState];
}
return self;
}
#if !RCT_NEW_ARCH_ENABLED
- (instancetype)initWithFrame:(CGRect)frame reactImageLoader:(RCTImageLoader *)imageLoader
{
if (self = [self initWithFrame:frame]) {
_imageLoader = imageLoader;
}
return self;
}
#endif // !RCT_NEW_ARCH_ENABLED
- (nonnull RNSTabBarController *)controller
{
RCTAssert(_controller != nil, @"[RNScreens] Controller must not be nil");
return _controller;
}
- (void)initState
{
[self resetProps];
_controller = [[RNSTabBarController alloc] initWithTabsHostComponentView:self];
_controllerDelegate = [RNSTabBarControllerDelegate new];
_controller.delegate = _controllerDelegate;
_reactSubviews = [NSMutableArray new];
_reactEventEmitter = [RNSBottomTabsHostEventEmitter new];
#if RCT_NEW_ARCH_ENABLED
_invalidatedComponentsRegistry = [RNSInvalidatedComponentsRegistry new];
#endif // RCT_NEW_ARCH_ENABLED
_hasModifiedReactSubviewsInCurrentTransaction = NO;
_needsTabBarAppearanceUpdate = NO;
}
- (void)resetProps
{
#if RCT_NEW_ARCH_ENABLED
static const auto defaultProps = std::make_shared<const react::RNSBottomTabsProps>();
_props = defaultProps;
#endif
_tabBarTintColor = nil;
}
#pragma mark - UIView methods
- (void)willMoveToWindow:(UIWindow *)newWindow
{
#if RCT_NEW_ARCH_ENABLED
if (newWindow == nil) {
[_invalidatedComponentsRegistry flushInvalidViews];
}
#endif // RCT_NEW_ARCH_ENABLED
}
- (void)didMoveToWindow
{
if ([self window] != nil) {
[self reactAddControllerToClosestParent:_controller];
#if !RCT_NEW_ARCH_ENABLED
// This is required on legacy architecture to prevent a bug with doubled size of UIViewControllerWrapperView.
_controller.view.frame = self.bounds;
#endif // !RCT_NEW_ARCH_ENABLED
}
}
- (void)reactAddControllerToClosestParent:(UIViewController *)controller
{
if (!controller.parentViewController) {
UIView *parentView = (UIView *)self.reactSuperview;
while (parentView) {
if (parentView.reactViewController) {
[parentView.reactViewController addChildViewController:controller];
[self addSubview:controller.view];
[controller didMoveToParentViewController:parentView.reactViewController];
break;
}
parentView = (UIView *)parentView.reactSuperview;
}
return;
}
}
#pragma mark - RNSScreenContainerDelegate
- (void)updateContainer
{
NSMutableArray<RNSTabsScreenViewController *> *tabControllers =
[[NSMutableArray alloc] initWithCapacity:_reactSubviews.count];
for (RNSBottomTabsScreenComponentView *childView in _reactSubviews) {
[tabControllers addObject:childView.controller];
}
RNSLog(@"updateContainer: tabControllers: %@", tabControllers);
[_controller childViewControllersHaveChangedTo:tabControllers];
}
- (void)markChildUpdated
{
[self updateContainer];
}
#if RCT_NEW_ARCH_ENABLED
#pragma mark - RNSViewControllerInvalidating
- (void)invalidateController
{
_controller = nil;
}
- (BOOL)shouldInvalidateOnMutation:(const facebook::react::ShadowViewMutation &)mutation
{
return (mutation.oldChildShadowView.tag == self.tag && mutation.type == facebook::react::ShadowViewMutation::Delete);
}
#else
#pragma mark - RCTInvalidating
- (void)invalidate
{
// We assume that bottom tabs host is removed from view hierarchy **only** when
// whole component is destroyed & therefore we do the necessary cleanup here.
// If at some point that statement does not hold anymore, this cleanup
// should be moved to a different place.
for (RNSBottomTabsScreenComponentView *subview in _reactSubviews) {
[subview invalidate];
}
_controller = nil;
}
#endif
#pragma mark - React events
- (nonnull RNSBottomTabsHostEventEmitter *)reactEventEmitter
{
RCTAssert(_reactEventEmitter != nil, @"[RNScreens] Attempt to access uninitialized _reactEventEmitter");
return _reactEventEmitter;
}
- (BOOL)emitOnNativeFocusChangeRequestSelectedTabScreen:(RNSBottomTabsScreenComponentView *)tabScreen
{
return [_reactEventEmitter emitOnNativeFocusChange:OnNativeFocusChangePayload{.tabKey = tabScreen.tabKey}];
}
#pragma mark - RCTViewComponentViewProtocol
#if RCT_NEW_ARCH_ENABLED
- (void)mountChildComponentView:(UIView<RCTComponentViewProtocol> *)childComponentView index:(NSInteger)index
{
RCTAssert(
[childComponentView isKindOfClass:RNSBottomTabsScreenComponentView.class],
@"BottomTabsView only accepts children of type BottomTabScreen. Attempted to mount %@",
childComponentView);
auto *childScreen = static_cast<RNSBottomTabsScreenComponentView *>(childComponentView);
childScreen.reactSuperview = self;
[_reactSubviews insertObject:childScreen atIndex:index];
_hasModifiedReactSubviewsInCurrentTransaction = YES;
}
- (void)unmountChildComponentView:(UIView<RCTComponentViewProtocol> *)childComponentView index:(NSInteger)index
{
RCTAssert(
[childComponentView isKindOfClass:RNSBottomTabsScreenComponentView.class],
@"BottomTabsView only accepts children of type BottomTabScreen. Attempted to unmount %@",
childComponentView);
auto *childScreen = static_cast<RNSBottomTabsScreenComponentView *>(childComponentView);
childScreen.reactSuperview = nil;
[_reactSubviews removeObject:childScreen];
_hasModifiedReactSubviewsInCurrentTransaction = YES;
}
- (void)updateProps:(const facebook::react::Props::Shared &)props
oldProps:(const facebook::react::Props::Shared &)oldProps
{
const auto &oldComponentProps = *std::static_pointer_cast<const react::RNSBottomTabsProps>(_props);
const auto &newComponentProps = *std::static_pointer_cast<const react::RNSBottomTabsProps>(props);
if (newComponentProps.controlNavigationStateInJS != oldComponentProps.controlNavigationStateInJS) {
_experimental_controlNavigationStateInJS = newComponentProps.controlNavigationStateInJS;
}
if (newComponentProps.tabBarTintColor != oldComponentProps.tabBarTintColor) {
_needsTabBarAppearanceUpdate = YES;
_tabBarTintColor = RCTUIColorFromSharedColor(newComponentProps.tabBarTintColor);
}
if (newComponentProps.tabBarMinimizeBehavior != oldComponentProps.tabBarMinimizeBehavior) {
#if RNS_IPHONE_OS_VERSION_AVAILABLE(26_0)
if (@available(iOS 26.0, *)) {
_tabBarMinimizeBehavior = rnscreens::conversion::UITabBarMinimizeBehaviorFromRNSBottomTabsTabBarMinimizeBehavior(
newComponentProps.tabBarMinimizeBehavior);
_controller.tabBarMinimizeBehavior = _tabBarMinimizeBehavior;
} else
#endif // Check for iOS >= 26
if (newComponentProps.tabBarMinimizeBehavior != react::RNSBottomTabsTabBarMinimizeBehavior::Automatic) {
RCTLogWarn(@"[RNScreens] tabBarMinimizeBehavior is supported for iOS >= 26");
}
}
// Super call updates _props pointer. We should NOT update it before calling super.
[super updateProps:props oldProps:oldProps];
}
- (void)updateState:(const facebook::react::State::Shared &)state
oldState:(const facebook::react::State::Shared &)oldState
{
react::RNSBottomTabsShadowNode::ConcreteState::Shared receivedState =
std::static_pointer_cast<const react::RNSBottomTabsShadowNode::ConcreteState>(state);
_imageLoader = [self retrieveImageLoaderFromState:receivedState];
}
- (void)updateEventEmitter:(const facebook::react::EventEmitter::Shared &)eventEmitter
{
[super updateEventEmitter:eventEmitter];
const auto &castedEventEmitter = std::static_pointer_cast<const react::RNSBottomTabsEventEmitter>(eventEmitter);
[_reactEventEmitter updateEventEmitter:castedEventEmitter];
}
- (void)finalizeUpdates:(RNComponentViewUpdateMask)updateMask
{
if (_needsTabBarAppearanceUpdate) {
_needsTabBarAppearanceUpdate = NO;
[_controller setNeedsUpdateOfTabBarAppearance:true];
}
[super finalizeUpdates:updateMask];
}
+ (react::ComponentDescriptorProvider)componentDescriptorProvider
{
return react::concreteComponentDescriptorProvider<react::RNSBottomTabsComponentDescriptor>();
}
+ (BOOL)shouldBeRecycled
{
// There won't be tens of instances of this component usually & it's easier for now.
// We could consider enabling it someday though.
return NO;
}
#pragma mark - RCTMountingTransactionObserving
- (void)mountingTransactionWillMount:(const facebook::react::MountingTransaction &)transaction
withSurfaceTelemetry:(const facebook::react::SurfaceTelemetry &)surfaceTelemetry
{
_hasModifiedReactSubviewsInCurrentTransaction = NO;
[_controller reactMountingTransactionWillMount];
#if RCT_NEW_ARCH_ENABLED
for (const auto &mutation : transaction.getMutations()) {
if ([self shouldInvalidateOnMutation:mutation]) {
for (RNSBottomTabsScreenComponentView *childView in _reactSubviews) {
[RNSViewControllerInvalidator invalidateViewIfDetached:childView forRegistry:_invalidatedComponentsRegistry];
}
[RNSViewControllerInvalidator invalidateViewIfDetached:self forRegistry:_invalidatedComponentsRegistry];
}
}
#endif // RCT_NEW_ARCH_ENABLED
}
- (void)mountingTransactionDidMount:(const facebook::react::MountingTransaction &)transaction
withSurfaceTelemetry:(const facebook::react::SurfaceTelemetry &)surfaceTelemetry
{
if (_hasModifiedReactSubviewsInCurrentTransaction) {
[self updateContainer];
}
[_controller reactMountingTransactionDidMount];
}
#else
#pragma mark - LEGACY architecture implementation
#pragma mark - LEGACY RCTComponent protocol
RNS_IGNORE_SUPER_CALL_BEGIN
- (void)insertReactSubview:(UIView *)subview atIndex:(NSInteger)index
{
RCTAssert(
[subview isKindOfClass:RNSBottomTabsScreenComponentView.class],
@"BottomTabsView only accepts children of type BottomTabScreen. Attempted to mount %@",
subview);
auto *childScreen = static_cast<RNSBottomTabsScreenComponentView *>(subview);
childScreen.reactSuperview = self;
[_reactSubviews insertObject:childScreen atIndex:index];
}
- (void)removeReactSubview:(UIView *)subview
{
RCTAssert(
[subview isKindOfClass:RNSBottomTabsScreenComponentView.class],
@"BottomTabsView only accepts children of type BottomTabScreen. Attempted to unmount %@",
subview);
auto *childScreen = static_cast<RNSBottomTabsScreenComponentView *>(subview);
childScreen.reactSuperview = nil;
[_reactSubviews removeObject:childScreen];
}
RNS_IGNORE_SUPER_CALL_END
- (void)didUpdateReactSubviews
{
[super didUpdateReactSubviews];
_hasModifiedReactSubviewsInCurrentTransaction = YES;
[self invalidateFlagsOnControllerIfNeeded];
}
- (void)didSetProps:(NSArray<NSString *> *)changedProps
{
[super didSetProps:changedProps];
_needsTabBarAppearanceUpdate = YES;
[self invalidateFlagsOnControllerIfNeeded];
}
#pragma mark - LEGACY update methods
- (void)invalidateFlagsOnControllerIfNeeded
{
if (_needsTabBarAppearanceUpdate) {
_needsTabBarAppearanceUpdate = NO;
[_controller setNeedsUpdateOfTabBarAppearance:true];
}
if (_hasModifiedReactSubviewsInCurrentTransaction) {
_hasModifiedReactSubviewsInCurrentTransaction = NO;
[self updateContainer];
}
}
- (void)invalidateTabBarAppearance
{
_needsTabBarAppearanceUpdate = YES;
[self invalidateFlagsOnControllerIfNeeded];
}
#pragma mark - LEGACY prop setters
// Paper will call property setters
- (void)setTabBarTintColor:(UIColor *_Nullable)tabBarTintColor
{
_tabBarTintColor = tabBarTintColor;
[self invalidateTabBarAppearance];
}
// This is a Paper-only setter method that will be called by the mounting code.
// It allows us to store UITabBarMinimizeBehavior in the component while accepting a custom enum as input from JS.
- (void)setTabBarMinimizeBehaviorFromRNSTabBarMinimizeBehavior:(RNSTabBarMinimizeBehavior)tabBarMinimizeBehavior
{
#if RNS_IPHONE_OS_VERSION_AVAILABLE(26_0)
if (@available(iOS 26.0, *)) {
_tabBarMinimizeBehavior =
rnscreens::conversion::UITabBarMinimizeBehaviorFromRNSTabBarMinimizeBehavior(tabBarMinimizeBehavior);
_controller.tabBarMinimizeBehavior = _tabBarMinimizeBehavior;
} else
#endif // Check for iOS >= 26
if (tabBarMinimizeBehavior != RNSTabBarMinimizeBehaviorAutomatic) {
RCTLogWarn(@"[RNScreens] tabBarMinimizeBehavior is supported for iOS >= 26");
}
}
- (void)setOnNativeFocusChange:(RCTDirectEventBlock)onNativeFocusChange
{
[self.reactEventEmitter setOnNativeFocusChange:onNativeFocusChange];
}
#endif // RCT_NEW_ARCH_ENABLED
#pragma mark - React Image Loader
- (nullable RCTImageLoader *)reactImageLoader
{
return _imageLoader;
}
@end
#if RCT_NEW_ARCH_ENABLED
#pragma mark - View class exposure
Class<RCTComponentViewProtocol> RNSBottomTabsCls(void)
{
return RNSBottomTabsHostComponentView.class;
}
#endif // RCT_NEW_ARCH_ENABLED
@@ -0,0 +1,11 @@
#pragma once
#import <React/RCTViewManager.h>
NS_ASSUME_NONNULL_BEGIN
@interface RNSBottomTabsHostComponentViewManager : RCTViewManager
@end
NS_ASSUME_NONNULL_END
@@ -0,0 +1,42 @@
#import "RNSBottomTabsHostComponentViewManager.h"
#if !RCT_NEW_ARCH_ENABLED
#import <React/RCTImageLoader.h>
#import "RNSBottomTabsHostComponentView.h"
#endif
@implementation RNSBottomTabsHostComponentViewManager
// TODO: This seems to be legacy arch only - test & remove when no longer needed
RCT_EXPORT_MODULE(RNSBottomTabsManager)
#if !RCT_NEW_ARCH_ENABLED
- (UIView *)view
{
// For Paper, we need to initialize TabsHost with RCTImageLoader from bridge
return [[RNSBottomTabsHostComponentView alloc] initWithFrame:CGRectZero
reactImageLoader:[self.bridge moduleForClass:[RCTImageLoader class]]];
}
#pragma mark - LEGACY Props
RCT_EXPORT_VIEW_PROPERTY(tabBarTintColor, UIColor);
// This remapping allows us to store UITabBarMinimizeBehavior in the component while accepting a custom enum as input
// from JS.
RCT_REMAP_VIEW_PROPERTY(
tabBarMinimizeBehavior,
tabBarMinimizeBehaviorFromRNSTabBarMinimizeBehavior,
RNSTabBarMinimizeBehavior);
// TODO: Missing prop
//@property (nonatomic, readonly) BOOL experimental_controlNavigationStateInJS;
#pragma mark - LEGACY Events
RCT_EXPORT_VIEW_PROPERTY(onNativeFocusChange, RCTDirectEventBlock);
#endif
@end
@@ -0,0 +1,53 @@
#import <Foundation/Foundation.h>
// Hide C++ symbols from C compiler used when building Swift module
#if defined(__cplusplus) && RCT_NEW_ARCH_ENABLED
#import <react/renderer/components/rnscreens/EventEmitters.h>
namespace react = facebook::react;
#endif // __cplusplus
#if !RCT_NEW_ARCH_ENABLED
#import <React/RCTComponent.h>
#endif // !RCT_NEW_ARCH_ENABLED
NS_ASSUME_NONNULL_BEGIN
#if defined(__cplusplus)
struct OnNativeFocusChangePayload {
NSString *_Nonnull tabKey;
};
#else
typedef struct {
NSString *_Nonnull tabKey;
} OnNativeFocusChangePayload;
#endif
@interface RNSBottomTabsHostEventEmitter : NSObject
- (BOOL)emitOnNativeFocusChange:(OnNativeFocusChangePayload)payload;
@end
#pragma mark - Hidden from Swift
#if defined(__cplusplus)
@interface RNSBottomTabsHostEventEmitter ()
#if RCT_NEW_ARCH_ENABLED
- (void)updateEventEmitter:(const std::shared_ptr<const react::RNSBottomTabsEventEmitter> &)emitter;
#else
#pragma mark - LEGACY Event emitter blocks
@property (nonatomic, copy) RCTDirectEventBlock onNativeFocusChange;
#endif // RCT_NEW_ARCH_ENABLED
@end
#endif // __cplusplus
NS_ASSUME_NONNULL_END
@@ -0,0 +1,57 @@
#import "RNSBottomTabsHostEventEmitter.h"
#import <React/RCTLog.h>
#if RCT_NEW_ARCH_ENABLED
#import <React/RCTConversions.h>
#import <react/renderer/components/rnscreens/EventEmitters.h>
#endif // RCT_NEW_ARCH_ENABLED
#if RCT_NEW_ARCH_ENABLED
namespace react = facebook::react;
#endif // RCT_NEW_ARCH_ENABLED
@implementation RNSBottomTabsHostEventEmitter {
#if RCT_NEW_ARCH_ENABLED
std::shared_ptr<const react::RNSBottomTabsEventEmitter> _reactEventEmitter;
#endif // RCT_NEW_ARCH_ENABLED
}
- (instancetype)init
{
if (self = [super init]) {
#if RCT_NEW_ARCH_ENABLED
_reactEventEmitter = nullptr;
#endif // RCT_NEW_ARCH_ENABLED
}
return self;
}
#if RCT_NEW_ARCH_ENABLED
- (void)updateEventEmitter:(const std::shared_ptr<const react::RNSBottomTabsEventEmitter> &)emitter
{
_reactEventEmitter = emitter;
}
#endif // RCT_NEW_ARCH_ENABLED
- (BOOL)emitOnNativeFocusChange:(OnNativeFocusChangePayload)payload
{
#if RCT_NEW_ARCH_ENABLED
if (_reactEventEmitter != nullptr) {
_reactEventEmitter->onNativeFocusChange({.tabKey = RCTStringFromNSString(payload.tabKey)});
return YES;
} else {
RCTLogWarn(@"[RNScreens] Skipped OnNativeFocusChange event emission due to nullish emitter");
return NO;
}
#else
if (self.onNativeFocusChange) {
self.onNativeFocusChange(@{@"tabKey" : payload.tabKey});
return YES;
} else {
RCTLogWarn(@"[RNScreens] Skipped OnNativeFocusChange event emission due to nullish emitter");
return NO;
}
#endif // RCT_NEW_ARCH_ENABLED
}
@end
@@ -0,0 +1,98 @@
#import <React/RCTImageSource.h>
#import "RNSBottomTabsScreenEventEmitter.h"
#import "RNSEnums.h"
#import "RNSReactBaseView.h"
#import "RNSScrollViewBehaviorOverriding.h"
#ifdef RCT_NEW_ARCH_ENABLED
#import "RNSViewControllerInvalidating.h"
#else
#import <React/RCTInvalidating.h>
#endif
NS_ASSUME_NONNULL_BEGIN
@class RNSBottomTabsHostComponentView;
@class RNSTabsScreenViewController;
/**
* Component view with react managed lifecycle. This view serves as root view in hierarchy
* of a particular tab.
*/
@interface RNSBottomTabsScreenComponentView : RNSReactBaseView <
#ifdef RCT_NEW_ARCH_ENABLED
RNSViewControllerInvalidating
#else
RCTInvalidating
#endif
>
/**
* View controller responsible for managing tab represented by this component view.
*/
@property (nonatomic, strong, readonly, nullable) RNSTabsScreenViewController *controller;
/**
* If not null, the bottom tabs host view that this tab component view belongs to.
*/
@property (nonatomic, weak, nullable) RNSBottomTabsHostComponentView *reactSuperview;
@end
#pragma mark - Props
/**
* Properties set on component in JavaScript.
*/
@interface RNSBottomTabsScreenComponentView () <RNSScrollViewBehaviorOverriding>
// TODO: All of these properties should be `readonly`. Do this when support for legacy
// architecture is dropped.
@property (nonatomic) BOOL isSelectedScreen;
@property (nonatomic, nullable) NSString *tabKey;
@property (nonatomic, nullable) NSString *badgeValue;
@property (nonatomic, readonly) RNSBottomTabsIconType iconType;
@property (nonatomic, strong, readonly, nullable) RCTImageSource *iconImageSource;
@property (nonatomic, strong, readonly, nullable) NSString *iconSfSymbolName;
@property (nonatomic, strong, readonly, nullable) RCTImageSource *selectedIconImageSource;
@property (nonatomic, strong, readonly, nullable) NSString *selectedIconSfSymbolName;
@property (nonatomic, strong, readonly, nullable) UITabBarAppearance *standardAppearance;
@property (nonatomic, strong, readonly, nullable) UITabBarAppearance *scrollEdgeAppearance;
@property (nonatomic, nullable) NSString *title;
@property (nonatomic, readonly) RNSOrientation orientation;
@property (nonatomic) BOOL shouldUseRepeatedTabSelectionPopToRootSpecialEffect;
@property (nonatomic) BOOL shouldUseRepeatedTabSelectionScrollToTopSpecialEffect;
@property (nonatomic, readonly) BOOL overrideScrollViewContentInsetAdjustmentBehavior;
@property (nonatomic) RNSBottomTabsScreenSystemItem systemItem;
@end
#pragma mark - Events
@interface RNSBottomTabsScreenComponentView ()
/**
* Use returned object to emit appropriate React Events to Element Tree.
*/
- (nonnull RNSBottomTabsScreenEventEmitter *)reactEventEmitter;
#if !RCT_NEW_ARCH_ENABLED
#pragma mark - LEGACY Event emitting blocks
@property (nonatomic, copy, nullable) RCTDirectEventBlock onWillAppear;
@property (nonatomic, copy, nullable) RCTDirectEventBlock onDidAppear;
@property (nonatomic, copy, nullable) RCTDirectEventBlock onWillDisappear;
@property (nonatomic, copy, nullable) RCTDirectEventBlock onDidDisappear;
#endif // !RCT_NEW_ARCH_ENABLED
@end
NS_ASSUME_NONNULL_END
@@ -0,0 +1,549 @@
#import "RNSBottomTabsScreenComponentView.h"
#import "NSString+RNSUtility.h"
#import "RNSConversions.h"
#import "RNSDefines.h"
#import "RNSLog.h"
#import "RNSScrollViewHelper.h"
#import "RNSTabBarAppearanceCoordinator.h"
#import "RNSTabBarController.h"
#if RCT_NEW_ARCH_ENABLED
#import <React/RCTConversions.h>
#import <React/RCTImageSource.h>
#import <react/renderer/components/rnscreens/ComponentDescriptors.h>
#import <react/renderer/components/rnscreens/EventEmitters.h>
#import <react/renderer/components/rnscreens/Props.h>
#import <react/renderer/components/rnscreens/RCTComponentViewHelpers.h>
#endif // RCT_NEW_ARCH_ENABLED
#if RCT_NEW_ARCH_ENABLED
namespace react = facebook::react;
#endif // RCT_NEW_ARCH_ENABLED
#pragma mark - View implementation
@implementation RNSBottomTabsScreenComponentView {
RNSTabsScreenViewController *_controller;
RNSBottomTabsHostComponentView *__weak _Nullable _reactSuperview;
RNSBottomTabsScreenEventEmitter *_Nonnull _reactEventEmitter;
// We need this information to warn users about dynamic changes to behavior being currently unsupported.
BOOL _isOverrideScrollViewContentInsetAdjustmentBehaviorSet;
#if !RCT_NEW_ARCH_ENABLED
BOOL _tabItemNeedsAppearanceUpdate;
BOOL _tabScreenOrientationNeedsUpdate;
BOOL _tabBarItemNeedsUpdate;
#endif // !RCT_NEW_ARCH_ENABLED
}
- (instancetype)initWithFrame:(CGRect)frame
{
if (self = [super initWithFrame:frame]) {
[self initState];
}
return self;
}
- (void)initState
{
#if RCT_NEW_ARCH_ENABLED
static const auto defaultProps = std::make_shared<const react::RNSBottomTabsScreenProps>();
_props = defaultProps;
#endif // RCT_NEW_ARCH_ENABLED
_controller = [RNSTabsScreenViewController new];
_controller.view = self;
_reactSuperview = nil;
_reactEventEmitter = [RNSBottomTabsScreenEventEmitter new];
#if !RCT_NEW_ARCH_ENABLED
_tabItemNeedsAppearanceUpdate = NO;
_tabScreenOrientationNeedsUpdate = NO;
_tabBarItemNeedsUpdate = NO;
#endif
// This is a temporary workaround to avoid UIScrollEdgeEffect glitch
// when changing tabs when ScrollView is present.
// TODO: don't hardcode color here
self.backgroundColor = [UIColor whiteColor];
[self resetProps];
}
- (void)resetProps
{
_isSelectedScreen = NO;
_badgeValue = nil;
_title = nil;
_orientation = RNSOrientationInherit;
_standardAppearance = [UITabBarAppearance new];
_scrollEdgeAppearance = nil;
_shouldUseRepeatedTabSelectionPopToRootSpecialEffect = YES;
_shouldUseRepeatedTabSelectionScrollToTopSpecialEffect = YES;
_overrideScrollViewContentInsetAdjustmentBehavior = YES;
_isOverrideScrollViewContentInsetAdjustmentBehaviorSet = NO;
_iconType = RNSBottomTabsIconTypeSfSymbol;
_iconImageSource = nil;
_iconSfSymbolName = nil;
_selectedIconImageSource = nil;
_selectedIconSfSymbolName = nil;
_systemItem = RNSBottomTabsScreenSystemItemNone;
}
RNS_IGNORE_SUPER_CALL_BEGIN
- (nullable RNSBottomTabsHostComponentView *)reactSuperview
{
return _reactSuperview;
}
RNS_IGNORE_SUPER_CALL_END
#ifdef RCT_NEW_ARCH_ENABLED
#pragma mark - RNSViewControllerInvalidating
- (void)invalidateController
{
_controller = nil;
}
#else
#pragma mark - RCTInvalidating
- (void)invalidate
{
_controller = nil;
}
#endif
#pragma mark - Events
- (nonnull RNSBottomTabsScreenEventEmitter *)reactEventEmitter
{
RCTAssert(_reactEventEmitter != nil, @"[RNScreens] Attempt to access uninitialized _reactEventEmitter");
return _reactEventEmitter;
}
- (nullable RNSTabBarController *)findTabBarController
{
return static_cast<RNSTabBarController *>(_controller.tabBarController);
}
#pragma mark - RNSScrollViewBehaviorOverriding
- (BOOL)shouldOverrideScrollViewContentInsetAdjustmentBehavior
{
return self.overrideScrollViewContentInsetAdjustmentBehavior;
}
- (void)overrideScrollViewBehaviorInFirstDescendantChainIfNeeded
{
if ([self shouldOverrideScrollViewContentInsetAdjustmentBehavior]) {
[RNSScrollViewHelper overrideScrollViewBehaviorInFirstDescendantChainFrom:self];
}
}
#pragma mark - Prop update utils
- (void)updateTabBarItem
{
UITabBarItem *tabBarItem = nil;
if (_systemItem != RNSBottomTabsScreenSystemItemNone) {
UITabBarSystemItem systemItem =
rnscreens::conversion::RNSBottomTabsScreenSystemItemToUITabBarSystemItem(_systemItem);
tabBarItem = [[UITabBarItem alloc] initWithTabBarSystemItem:systemItem tag:0];
} else {
tabBarItem = [[UITabBarItem alloc] init];
tabBarItem.title = _title;
}
tabBarItem.badgeValue = _badgeValue;
_controller.tabBarItem = tabBarItem;
}
#if RCT_NEW_ARCH_ENABLED
#pragma mark - RCTViewComponentViewProtocol
- (void)updateProps:(const facebook::react::Props::Shared &)props
oldProps:(const facebook::react::Props::Shared &)oldProps
{
const auto &oldComponentProps = *std::static_pointer_cast<const react::RNSBottomTabsScreenProps>(_props);
const auto &newComponentProps = *std::static_pointer_cast<const react::RNSBottomTabsScreenProps>(props);
bool tabItemNeedsAppearanceUpdate{false};
bool tabScreenOrientationNeedsUpdate{false};
bool tabBarItemNeedsUpdate{false};
if (newComponentProps.title != oldComponentProps.title) {
_title = RCTNSStringFromStringNilIfEmpty(newComponentProps.title);
_controller.title = _title;
}
if (newComponentProps.orientation != oldComponentProps.orientation) {
_orientation =
rnscreens::conversion::RNSOrientationFromRNSBottomTabsScreenOrientation(newComponentProps.orientation);
tabScreenOrientationNeedsUpdate = YES;
}
if (newComponentProps.tabKey != oldComponentProps.tabKey) {
RCTAssert(!newComponentProps.tabKey.empty(), @"[RNScreens] tabKey must not be empty!");
_tabKey = RCTNSStringFromString(newComponentProps.tabKey);
}
if (newComponentProps.isFocused != oldComponentProps.isFocused) {
_isSelectedScreen = newComponentProps.isFocused;
[_controller tabScreenFocusHasChanged];
}
if (newComponentProps.badgeValue != oldComponentProps.badgeValue) {
_badgeValue = RCTNSStringFromStringNilIfEmpty(newComponentProps.badgeValue);
tabBarItemNeedsUpdate = YES;
}
if (newComponentProps.standardAppearance != oldComponentProps.standardAppearance) {
_standardAppearance = [UITabBarAppearance new];
[RNSTabBarAppearanceCoordinator configureTabBarAppearance:_standardAppearance
fromAppearanceProps:rnscreens::conversion::RNSConvertFollyDynamicToId(
newComponentProps.standardAppearance)];
tabItemNeedsAppearanceUpdate = YES;
}
if (newComponentProps.scrollEdgeAppearance != oldComponentProps.scrollEdgeAppearance) {
if (newComponentProps.scrollEdgeAppearance.type() == folly::dynamic::OBJECT) {
_scrollEdgeAppearance = [UITabBarAppearance new];
[RNSTabBarAppearanceCoordinator configureTabBarAppearance:_scrollEdgeAppearance
fromAppearanceProps:rnscreens::conversion::RNSConvertFollyDynamicToId(
newComponentProps.scrollEdgeAppearance)];
} else {
_scrollEdgeAppearance = nil;
}
tabItemNeedsAppearanceUpdate = YES;
}
if (newComponentProps.iconType != oldComponentProps.iconType) {
_iconType = rnscreens::conversion::RNSBottomTabsIconTypeFromIcon(newComponentProps.iconType);
tabItemNeedsAppearanceUpdate = YES;
tabBarItemNeedsUpdate = YES;
}
if (newComponentProps.iconImageSource != oldComponentProps.iconImageSource) {
_iconImageSource =
rnscreens::conversion::RCTImageSourceFromImageSourceAndIconType(&newComponentProps.iconImageSource, _iconType);
tabItemNeedsAppearanceUpdate = YES;
tabBarItemNeedsUpdate = YES;
}
if (newComponentProps.iconSfSymbolName != oldComponentProps.iconSfSymbolName) {
_iconSfSymbolName = RCTNSStringFromStringNilIfEmpty(newComponentProps.iconSfSymbolName);
tabItemNeedsAppearanceUpdate = YES;
tabBarItemNeedsUpdate = YES;
}
if (newComponentProps.selectedIconImageSource != oldComponentProps.selectedIconImageSource) {
_selectedIconImageSource = rnscreens::conversion::RCTImageSourceFromImageSourceAndIconType(
&newComponentProps.selectedIconImageSource, _iconType);
tabItemNeedsAppearanceUpdate = YES;
tabBarItemNeedsUpdate = YES;
}
if (newComponentProps.selectedIconSfSymbolName != oldComponentProps.selectedIconSfSymbolName) {
_selectedIconSfSymbolName = RCTNSStringFromStringNilIfEmpty(newComponentProps.selectedIconSfSymbolName);
tabItemNeedsAppearanceUpdate = YES;
tabBarItemNeedsUpdate = YES;
}
if (newComponentProps.specialEffects.repeatedTabSelection.popToRoot !=
oldComponentProps.specialEffects.repeatedTabSelection.popToRoot) {
_shouldUseRepeatedTabSelectionPopToRootSpecialEffect =
newComponentProps.specialEffects.repeatedTabSelection.popToRoot;
}
if (newComponentProps.specialEffects.repeatedTabSelection.scrollToTop !=
oldComponentProps.specialEffects.repeatedTabSelection.scrollToTop) {
_shouldUseRepeatedTabSelectionScrollToTopSpecialEffect =
newComponentProps.specialEffects.repeatedTabSelection.scrollToTop;
}
if (newComponentProps.overrideScrollViewContentInsetAdjustmentBehavior !=
oldComponentProps.overrideScrollViewContentInsetAdjustmentBehavior) {
_overrideScrollViewContentInsetAdjustmentBehavior =
newComponentProps.overrideScrollViewContentInsetAdjustmentBehavior;
if (_isOverrideScrollViewContentInsetAdjustmentBehaviorSet) {
RCTLogWarn(
@"[RNScreens] changing overrideScrollViewContentInsetAdjustmentBehavior dynamically is currently unsupported");
}
}
// This flag is set to YES when overrideScrollViewContentInsetAdjustmentBehavior prop
// is assigned for the first time. This allows us to identify any subsequent changes to this prop,
// enabling us to warn users that dynamic changes are not supported.
_isOverrideScrollViewContentInsetAdjustmentBehaviorSet = YES;
if (newComponentProps.systemItem != oldComponentProps.systemItem) {
_systemItem = rnscreens::conversion::RNSBottomTabsScreenSystemItemFromReactRNSBottomTabsScreenSystemItem(
newComponentProps.systemItem);
tabBarItemNeedsUpdate = YES;
}
if (tabBarItemNeedsUpdate) {
[self updateTabBarItem];
// Force appearance update to make sure correct image for tab bar item is used
tabItemNeedsAppearanceUpdate = YES;
}
if (tabItemNeedsAppearanceUpdate) {
[_controller tabItemAppearanceHasChanged];
}
if (tabScreenOrientationNeedsUpdate) {
[_controller tabScreenOrientationHasChanged];
}
[super updateProps:props oldProps:oldProps];
}
- (void)updateLayoutMetrics:(const facebook::react::LayoutMetrics &)layoutMetrics
oldLayoutMetrics:(const facebook::react::LayoutMetrics &)oldLayoutMetrics
{
RNSLog(
@"TabScreen [%ld] updateLayoutMetrics: %@", self.tag, NSStringFromCGRect(RCTCGRectFromRect(layoutMetrics.frame)));
[super updateLayoutMetrics:layoutMetrics oldLayoutMetrics:oldLayoutMetrics];
}
- (void)updateEventEmitter:(const facebook::react::EventEmitter::Shared &)eventEmitter
{
[super updateEventEmitter:eventEmitter];
[_reactEventEmitter
updateEventEmitter:std::static_pointer_cast<const react::RNSBottomTabsScreenEventEmitter>(eventEmitter)];
}
- (void)mountChildComponentView:(UIView<RCTComponentViewProtocol> *)childComponentView index:(NSInteger)index
{
RNSLog(@"TabScreen [%ld] mount [%ld] at %ld", self.tag, childComponentView.tag, index);
[super mountChildComponentView:childComponentView index:index];
}
- (void)unmountChildComponentView:(UIView<RCTComponentViewProtocol> *)childComponentView index:(NSInteger)index
{
RNSLog(@"TabScreen [%ld] unmount [%ld] from %ld", self.tag, childComponentView.tag, index);
[super unmountChildComponentView:childComponentView index:index];
}
+ (react::ComponentDescriptorProvider)componentDescriptorProvider
{
return react::concreteComponentDescriptorProvider<react::RNSBottomTabsScreenComponentDescriptor>();
}
+ (BOOL)shouldBeRecycled
{
// There won't be tens of instances of this component usually & it's easier for now.
// We could consider enabling it someday though.
return NO;
}
#else
#pragma mark - LEGACY RCTComponent protocol
- (void)didSetProps:(NSArray<NSString *> *)changedProps
{
[super didSetProps:changedProps];
// This flag is set to YES when overrideScrollViewContentInsetAdjustmentBehavior prop
// is assigned for the first time. This allows us to identify any subsequent changes to this prop,
// enabling us to warn users that dynamic changes are not supported.
// On Paper, setter for the prop may not be called (when it is undefined in JS).
// Therefore we set the flag in didSetProps to make sure to handle this case as well.
// didSetProps will always be called because tabKey prop is required.
_isOverrideScrollViewContentInsetAdjustmentBehaviorSet = YES;
if (_tabBarItemNeedsUpdate) {
[self updateTabBarItem];
_tabBarItemNeedsUpdate = NO;
// Force appearance update to make sure correct image for tab bar item is used
_tabItemNeedsAppearanceUpdate = YES;
}
if (_tabItemNeedsAppearanceUpdate) {
[_controller tabItemAppearanceHasChanged];
_tabItemNeedsAppearanceUpdate = NO;
}
if (_tabScreenOrientationNeedsUpdate) {
[_controller tabScreenOrientationHasChanged];
_tabScreenOrientationNeedsUpdate = NO;
}
}
#pragma mark - LEGACY prop setters
- (void)setIsSelectedScreen:(BOOL)isSelectedScreen
{
if (_isSelectedScreen != isSelectedScreen) {
_isSelectedScreen = isSelectedScreen;
[_controller tabScreenFocusHasChanged];
}
}
- (void)setTabKey:(NSString *)tabKey
{
RCTAssert([NSString rnscreens_isBlankOrNull:tabKey] == NO, @"[RNScreens] tabKey must not be empty");
_tabKey = tabKey;
}
- (void)setTitle:(NSString *)title
{
_title = title;
_controller.title = title;
}
- (void)setBadgeValue:(NSString *)badgeValue
{
_badgeValue = [NSString rnscreens_stringOrNilIfBlank:badgeValue];
_tabBarItemNeedsUpdate = YES;
}
- (void)setIconType:(RNSBottomTabsIconType)iconType
{
_iconType = iconType;
_tabItemNeedsAppearanceUpdate = YES;
_tabBarItemNeedsUpdate = YES;
}
- (void)setIconImageSource:(RCTImageSource *)iconImageSource
{
_iconImageSource = iconImageSource;
_tabItemNeedsAppearanceUpdate = YES;
_tabBarItemNeedsUpdate = YES;
}
- (void)setIconSfSymbolName:(NSString *)iconSfSymbolName
{
_iconSfSymbolName = [NSString rnscreens_stringOrNilIfEmpty:iconSfSymbolName];
_tabItemNeedsAppearanceUpdate = YES;
_tabBarItemNeedsUpdate = YES;
}
- (void)setSelectedIconImageSource:(RCTImageSource *)selectedIconImageSource
{
_selectedIconImageSource = selectedIconImageSource;
_tabItemNeedsAppearanceUpdate = YES;
_tabBarItemNeedsUpdate = YES;
}
- (void)setSelectedIconSfSymbolName:(NSString *)selectedIconSfSymbolName
{
_selectedIconSfSymbolName = [NSString rnscreens_stringOrNilIfEmpty:selectedIconSfSymbolName];
_tabItemNeedsAppearanceUpdate = YES;
_tabBarItemNeedsUpdate = YES;
}
- (void)setOverrideScrollViewContentInsetAdjustmentBehavior:(BOOL)overrideScrollViewContentInsetAdjustmentBehavior
{
_overrideScrollViewContentInsetAdjustmentBehavior = overrideScrollViewContentInsetAdjustmentBehavior;
if (_isOverrideScrollViewContentInsetAdjustmentBehaviorSet) {
RCTLogWarn(
@"[RNScreens] changing overrideScrollViewContentInsetAdjustmentBehavior dynamically is currently unsupported");
}
// _isOverrideScrollViewContentInsetAdjustmentBehaviorSet flag is set in didSetProps to handle a case
// when the prop is undefined in JS and default value is used instead of calling this setter.
}
- (void)setStandardAppearance:(NSDictionary *)standardAppearanceProps
{
_standardAppearance = [UITabBarAppearance new];
if (standardAppearanceProps != nil) {
[RNSTabBarAppearanceCoordinator configureTabBarAppearance:_standardAppearance
fromAppearanceProps:standardAppearanceProps];
}
_tabItemNeedsAppearanceUpdate = YES;
}
- (void)setScrollEdgeAppearance:(NSDictionary *)scrollEdgeAppearanceProps
{
if (scrollEdgeAppearanceProps != nil) {
_scrollEdgeAppearance = [UITabBarAppearance new];
[RNSTabBarAppearanceCoordinator configureTabBarAppearance:_scrollEdgeAppearance
fromAppearanceProps:scrollEdgeAppearanceProps];
} else {
_scrollEdgeAppearance = nil;
}
_tabItemNeedsAppearanceUpdate = YES;
}
// This is a Paper-only setter method that will be called by the mounting code.
// It allows us to store UITabBarMinimizeBehavior in the component while accepting a custom enum as input from JS.
- (void)setSystemItem:(RNSBottomTabsScreenSystemItem)systemItem
{
_systemItem = systemItem;
_tabBarItemNeedsUpdate = YES;
}
- (void)setOrientation:(RNSOrientation)orientation
{
_orientation = orientation;
_tabScreenOrientationNeedsUpdate = YES;
}
- (void)setOnWillAppear:(RCTDirectEventBlock)onWillAppear
{
[self.reactEventEmitter setOnWillAppear:onWillAppear];
}
- (void)setOnWillDisappear:(RCTDirectEventBlock)onWillDisappear
{
[self.reactEventEmitter setOnWillDisappear:onWillDisappear];
}
- (void)setOnDidAppear:(RCTDirectEventBlock)onDidAppear
{
[self.reactEventEmitter setOnDidAppear:onDidAppear];
}
- (void)setOnDidDisappear:(RCTDirectEventBlock)onDidDisappear
{
[self.reactEventEmitter setOnDidDisappear:onDidDisappear];
}
#define RNS_FAILING_EVENT_GETTER(eventName) \
-(RCTDirectEventBlock)eventName \
{ \
RCTAssert(NO, @"[RNScreens] Events should be emitted through reactEventEmitter"); \
return nil; \
}
RNS_FAILING_EVENT_GETTER(onWillAppear);
RNS_FAILING_EVENT_GETTER(onDidAppear);
RNS_FAILING_EVENT_GETTER(onWillDisappear);
RNS_FAILING_EVENT_GETTER(onDidDisappear);
#undef RNS_FAILING_EVENT_GETTER
#endif // RCT_NEW_ARCH_ENABLED
@end
#if RCT_NEW_ARCH_ENABLED
#pragma mark - View class exposure
Class<RCTComponentViewProtocol> RNSBottomTabsScreen(void)
{
return RNSBottomTabsScreenComponentView.class;
}
#endif // RCT_NEW_ARCH_ENABLED
@@ -0,0 +1,9 @@
#import <React/RCTViewManager.h>
NS_ASSUME_NONNULL_BEGIN
@interface RNSBottomTabsScreenComponentViewManager : RCTViewManager
@end
NS_ASSUME_NONNULL_END
@@ -0,0 +1,50 @@
#import "RNSBottomTabsScreenComponentViewManager.h"
#if !RCT_NEW_ARCH_ENABLED
#import "RNSBottomTabsScreenComponentView.h"
#endif // !RCT_NEW_ARCH_ENABLED
@implementation RNSBottomTabsScreenComponentViewManager
// TODO: This seems to be legacy arch only - remove when no longer needed
RCT_EXPORT_MODULE(RNSBottomTabsScreenManager)
#if !RCT_NEW_ARCH_ENABLED
- (UIView *)view
{
// This uses main initializer for Fabric implementation.
return [[RNSBottomTabsScreenComponentView alloc] initWithFrame:CGRectZero];
}
RCT_EXPORT_VIEW_PROPERTY(tabKey, NSString);
RCT_REMAP_VIEW_PROPERTY(isFocused, isSelectedScreen, BOOL);
RCT_EXPORT_VIEW_PROPERTY(title, NSString);
RCT_EXPORT_VIEW_PROPERTY(orientation, RNSOrientation);
RCT_EXPORT_VIEW_PROPERTY(badgeValue, NSString);
RCT_EXPORT_VIEW_PROPERTY(standardAppearance, NSDictionary);
RCT_EXPORT_VIEW_PROPERTY(scrollEdgeAppearance, NSDictionary);
RCT_EXPORT_VIEW_PROPERTY(iconType, RNSBottomTabsIconType);
RCT_EXPORT_VIEW_PROPERTY(iconImageSource, RCTImageSource);
RCT_EXPORT_VIEW_PROPERTY(iconSfSymbolName, NSString);
RCT_EXPORT_VIEW_PROPERTY(selectedIconImageSource, RCTImageSource);
RCT_EXPORT_VIEW_PROPERTY(selectedIconSfSymbolName, NSString);
RCT_EXPORT_VIEW_PROPERTY(shouldUseRepeatedTabSelectionPopToRootSpecialEffect, BOOL);
RCT_EXPORT_VIEW_PROPERTY(shouldUseRepeatedTabSelectionScrollToTopSpecialEffect, BOOL);
RCT_EXPORT_VIEW_PROPERTY(overrideScrollViewContentInsetAdjustmentBehavior, BOOL);
RCT_EXPORT_VIEW_PROPERTY(systemItem, RNSBottomTabsScreenSystemItem);
RCT_EXPORT_VIEW_PROPERTY(onWillAppear, RCTDirectEventBlock);
RCT_EXPORT_VIEW_PROPERTY(onWillDisappear, RCTDirectEventBlock);
RCT_EXPORT_VIEW_PROPERTY(onDidAppear, RCTDirectEventBlock);
RCT_EXPORT_VIEW_PROPERTY(onDidDisappear, RCTDirectEventBlock);
#endif // !RCT_NEW_ARCH_ENABLED
@end
@@ -0,0 +1,53 @@
#import <Foundation/Foundation.h>
// Hide C++ symbols from C compiler used when building Swift module
#if defined(__cplusplus) && RCT_NEW_ARCH_ENABLED
#import <react/renderer/components/rnscreens/EventEmitters.h>
namespace react = facebook::react;
#endif // __cplusplus
#if !RCT_NEW_ARCH_ENABLED
#import <React/RCTComponent.h>
#endif // !RCT_NEW_ARCH_ENABLED
NS_ASSUME_NONNULL_BEGIN
/**
* These methods can be called to send an appropriate event to ElementTree.
* Returned value denotes whether the event has been successfully dispatched to React event pipeline.
* The returned value of `true` does not mean, that the event has been successfully delivered.
*/
@interface RNSBottomTabsScreenEventEmitter : NSObject
- (BOOL)emitOnWillAppear;
- (BOOL)emitOnDidAppear;
- (BOOL)emitOnWillDisappear;
- (BOOL)emitOnDidDisappear;
@end
#pragma mark - Hidden from Swift
#if defined(__cplusplus)
@interface RNSBottomTabsScreenEventEmitter ()
#if RCT_NEW_ARCH_ENABLED
- (void)updateEventEmitter:(const std::shared_ptr<const react::RNSBottomTabsScreenEventEmitter> &)emitter;
#else
#pragma mark - LEGACY Event emitting blocks
@property (nonatomic, copy, nullable) RCTDirectEventBlock onWillAppear;
@property (nonatomic, copy, nullable) RCTDirectEventBlock onDidAppear;
@property (nonatomic, copy, nullable) RCTDirectEventBlock onWillDisappear;
@property (nonatomic, copy, nullable) RCTDirectEventBlock onDidDisappear;
#endif // RCT_NEW_ARCH_ENABLED
@end
#endif // __cplusplus
NS_ASSUME_NONNULL_END
@@ -0,0 +1,106 @@
#import "RNSBottomTabsScreenEventEmitter.h"
#import <React/RCTLog.h>
#if RCT_NEW_ARCH_ENABLED
#import <React/RCTConversions.h>
#import <react/renderer/components/rnscreens/EventEmitters.h>
#endif // RCT_NEW_ARCH_ENABLED
@implementation RNSBottomTabsScreenEventEmitter {
#if RCT_NEW_ARCH_ENABLED
std::shared_ptr<const react::RNSBottomTabsScreenEventEmitter> _reactEventEmitter;
#endif // RCT_NEW_ARCH_ENABLED
}
- (BOOL)emitOnWillAppear
{
#if RCT_NEW_ARCH_ENABLED
if (_reactEventEmitter != nullptr) {
_reactEventEmitter->onWillAppear({});
return YES;
} else {
RCTLogWarn(@"[RNScreens] Skipped OnWillAppear event emission due to nullish emitter");
return NO;
}
#else
if (self.onWillAppear) {
self.onWillAppear(nil);
return YES;
} else {
RCTLogWarn(@"[RNScreens] Skipped OnWillAppear event emission due to nullish emitter");
return NO;
}
#endif
}
- (BOOL)emitOnDidAppear
{
#if RCT_NEW_ARCH_ENABLED
if (_reactEventEmitter != nullptr) {
_reactEventEmitter->onDidAppear({});
return YES;
} else {
RCTLogWarn(@"[RNScreens] Skipped OnDidAppear event emission due to nullish emitter");
return NO;
}
#else
if (self.onDidAppear) {
self.onDidAppear(nil);
return YES;
} else {
RCTLogWarn(@"[RNScreens] Skipped OnDidAppear event emission due to nullish emitter");
return NO;
}
#endif
}
- (BOOL)emitOnWillDisappear
{
#if RCT_NEW_ARCH_ENABLED
if (_reactEventEmitter != nullptr) {
_reactEventEmitter->onWillDisappear({});
return YES;
} else {
RCTLogWarn(@"[RNScreens] Skipped OnWillDisappear event emission due to nullish emitter");
return NO;
}
#else
if (self.onWillDisappear) {
self.onWillDisappear(nil);
return YES;
} else {
RCTLogWarn(@"[RNScreens] Skipped OnWillDisappear event emission due to nullish emitter");
return NO;
}
#endif
}
- (BOOL)emitOnDidDisappear
{
#if RCT_NEW_ARCH_ENABLED
if (_reactEventEmitter != nullptr) {
_reactEventEmitter->onDidDisappear({});
return YES;
} else {
RCTLogWarn(@"[RNScreens] Skipped OnDidDisappear event emission due to nullish emitter");
return NO;
}
#else
if (self.onDidDisappear) {
self.onDidDisappear(nil);
return YES;
} else {
RCTLogWarn(@"[RNScreens] Skipped OnDidDisappear event emission due to nullish emitter");
return NO;
}
#endif
}
#if RCT_NEW_ARCH_ENABLED
- (void)updateEventEmitter:(const std::shared_ptr<const react::RNSBottomTabsScreenEventEmitter> &)emitter
{
_reactEventEmitter = emitter;
}
#endif
@end
@@ -0,0 +1,17 @@
#import <UIKit/UIKit.h>
NS_ASSUME_NONNULL_BEGIN
@class RNSTabsScreenViewController;
@protocol RNSBottomTabsSpecialEffectsSupporting
/**
* Handle repeated tab selection (e.g. in order to pop UINavigationController to root).
* Returns boolean indicating whether the action has been handled.
*/
- (bool)onRepeatedTabSelectionOfTabScreenController:(nonnull RNSTabsScreenViewController *)tabScreenController;
@end
NS_ASSUME_NONNULL_END
@@ -0,0 +1,49 @@
#import <Foundation/Foundation.h>
#import "RNSBottomTabsHostComponentView.h"
#import "RNSTabsScreenViewController.h"
NS_ASSUME_NONNULL_BEGIN
@class RCTImageLoader;
/**
* Responsible for creating & applying appearance to the tab bar.
*
* It does take into account all properties from host component view & tab screen controllers related to tab bar
* appearance and applies them accordingly in correct order.
*/
@interface RNSTabBarAppearanceCoordinator : NSObject
/**
* Applies the tab bar appearance props to the tab bar and respective tab bar items, basing on information contained in
* provided params.
*
* TODO: Do not take references to component view & controllers here. Put the tab bar appearance properites in single
* type & only take it here.
*/
- (void)updateAppearanceOfTabBar:(nullable UITabBar *)tabBar
withHostComponentView:(nullable RNSBottomTabsHostComponentView *)hostComponentView
tabScreenControllers:(nullable NSArray<RNSTabsScreenViewController *> *)tabScreenCtrls
imageLoader:(nullable RCTImageLoader *)imageLoader;
/**
* Configures UITabBarAppearance object using appearance props provided in the param.
*
* `appearanceProps` should be an NSDictionary with hierarchical structure that corresponds to UIKit's
* UITabBarAppearance object:
* - `appearanceProps` can contain:
* - `stacked`, `inline` and `compactInline` keys that map to dictionaries corresponding to UIKit's
* UITabBarItemAppearance objects (`itemAppearanceProps`),
* - entries that correspond to other props from UITabBarItemAppearance (UIBarAppearance) object, e.g.
* `backgroundColor`,
* - `itemAppearanceProps` can contain `normal`, `selected`, `disabled`, `focused` keys that map to dictionaries
* corresponding to UIKit's UITabBarItemStateAppearance objects (`itemStateAppearanceProps`),
* - `itemStateAppearanceProps` can contain entries that correspond to props from UITabBarItemStateAppearance object,
* e.g. `tabBarItemIconColor`.
*/
+ (void)configureTabBarAppearance:(nonnull UITabBarAppearance *)tabBarAppearance
fromAppearanceProps:(nonnull NSDictionary *)appearanceProps;
@end
NS_ASSUME_NONNULL_END
@@ -0,0 +1,226 @@
#import "RNSTabBarAppearanceCoordinator.h"
#import <React/RCTFont.h>
#import <React/RCTImageLoader.h>
#import "RCTConvert+RNSBottomTabs.h"
#import "RNSConversions.h"
#import "RNSTabsScreenViewController.h"
@implementation RNSTabBarAppearanceCoordinator
- (void)updateAppearanceOfTabBar:(nullable UITabBar *)tabBar
withHostComponentView:(nullable RNSBottomTabsHostComponentView *)hostComponentView
tabScreenControllers:(nullable NSArray<RNSTabsScreenViewController *> *)tabScreenCtrls
imageLoader:(nullable RCTImageLoader *)imageLoader
{
if (tabBar == nil) {
return;
}
// Step 1 - configure host-specific appearance
tabBar.tintColor = hostComponentView.tabBarTintColor;
if (tabScreenCtrls == nil) {
return;
}
// Step 2 - configure screen-specific appearance
for (RNSTabsScreenViewController *tabScreenCtrl in tabScreenCtrls) {
if (tabScreenCtrl == nil) {
// It should not be null here, something went wrong.
RCTLogWarn(@"[RNScreens] Nullish controller of TabScreen while tab bar appearance update!");
continue;
}
[self configureTabBarItemForTabScreenController:tabScreenCtrl imageLoader:imageLoader];
}
}
- (void)configureTabBarItemForTabScreenController:(nonnull RNSTabsScreenViewController *)tabScreenCtrl
imageLoader:(nullable RCTImageLoader *)imageLoader
{
UITabBarItem *tabBarItem = tabScreenCtrl.tabBarItem;
tabBarItem.standardAppearance = tabScreenCtrl.tabScreenComponentView.standardAppearance;
tabBarItem.scrollEdgeAppearance = tabScreenCtrl.tabScreenComponentView.scrollEdgeAppearance;
[self setIconsForTabBarItem:tabBarItem
fromScreenView:tabScreenCtrl.tabScreenComponentView
withImageLoader:imageLoader];
}
- (void)setIconsForTabBarItem:(UITabBarItem *)tabBarItem
fromScreenView:(RNSBottomTabsScreenComponentView *)screenView
withImageLoader:(RCTImageLoader *_Nullable)imageLoader
{
if (screenView.iconType == RNSBottomTabsIconTypeSfSymbol) {
if (screenView.iconSfSymbolName != nil) {
tabBarItem.image = [UIImage systemImageNamed:screenView.iconSfSymbolName];
}
if (screenView.selectedIconSfSymbolName != nil) {
tabBarItem.selectedImage = [UIImage systemImageNamed:screenView.selectedIconSfSymbolName];
}
} else if (imageLoader != nil) {
bool isTemplate = screenView.iconType == RNSBottomTabsIconTypeTemplate;
// Normal icon
if (screenView.iconImageSource != nil) {
[self loadImageFrom:screenView.iconImageSource
withImageLoader:imageLoader
asTemplate:isTemplate
completionBlock:^(UIImage *image) {
tabBarItem.image = image;
}];
} else {
tabBarItem.image = nil;
}
// Selected icon
if (screenView.selectedIconImageSource != nil) {
[self loadImageFrom:screenView.selectedIconImageSource
withImageLoader:imageLoader
asTemplate:isTemplate
completionBlock:^(UIImage *image) {
tabBarItem.selectedImage = image;
}];
} else {
tabBarItem.selectedImage = nil;
}
} else {
RCTLogWarn(@"[RNScreens] unable to load tab bar item icons: imageLoader should not be nil");
}
}
- (void)loadImageFrom:(nonnull RCTImageSource *)imageSource
withImageLoader:(nonnull RCTImageLoader *)imageLoader
asTemplate:(bool)isTemplate
completionBlock:(void (^)(UIImage *image))imageLoadingCompletionBlock
{
RCTAssert(imageSource != nil, @"[RNScreens] imageSource must not be nil");
RCTAssert(imageLoader != nil, @"[RNScreens] imageLoader must not be nil");
[imageLoader loadImageWithURLRequest:imageSource.request
size:imageSource.size
scale:imageSource.scale
clipped:true
resizeMode:RCTResizeModeContain
progressBlock:^(int64_t progress, int64_t total) {
}
partialLoadBlock:^(UIImage *_Nonnull image) {
}
completionBlock:^(NSError *_Nullable error, UIImage *_Nullable image) {
dispatch_async(dispatch_get_main_queue(), ^{
if (isTemplate) {
imageLoadingCompletionBlock([image imageWithRenderingMode:UIImageRenderingModeAlwaysTemplate]);
} else {
imageLoadingCompletionBlock([image imageWithRenderingMode:UIImageRenderingModeAlwaysOriginal]);
}
});
}];
}
+ (void)configureTabBarAppearance:(nonnull UITabBarAppearance *)tabBarAppearance
fromAppearanceProps:(nonnull NSDictionary *)appearanceProps
{
if (appearanceProps[@"tabBarBackgroundColor"] != nil) {
tabBarAppearance.backgroundColor = [RCTConvert UIColor:appearanceProps[@"tabBarBackgroundColor"]];
}
if (appearanceProps[@"tabBarBlurEffect"] != nil) {
NSString *blurEffectString = [appearanceProps[@"tabBarBlurEffect"] isKindOfClass:[NSString class]]
? appearanceProps[@"tabBarBlurEffect"]
: @"none";
if (![blurEffectString isEqualToString:@"systemDefault"]) {
tabBarAppearance.backgroundEffect = rnscreens::conversion::RNSUIBlurEffectFromString(blurEffectString);
}
}
if (appearanceProps[@"tabBarShadowColor"] != nil) {
tabBarAppearance.shadowColor = [RCTConvert UIColor:appearanceProps[@"tabBarShadowColor"]];
}
if ([appearanceProps[@"stacked"] isKindOfClass:[NSDictionary class]]) {
[self configureTabBarItemAppearance:tabBarAppearance.stackedLayoutAppearance
fromItemAppearanceProps:appearanceProps[@"stacked"]];
}
if ([appearanceProps[@"inline"] isKindOfClass:[NSDictionary class]]) {
[self configureTabBarItemAppearance:tabBarAppearance.inlineLayoutAppearance
fromItemAppearanceProps:appearanceProps[@"inline"]];
}
if ([appearanceProps[@"compactInline"] isKindOfClass:[NSDictionary class]]) {
[self configureTabBarItemAppearance:tabBarAppearance.compactInlineLayoutAppearance
fromItemAppearanceProps:appearanceProps[@"compactInline"]];
}
}
+ (void)configureTabBarItemAppearance:(nonnull UITabBarItemAppearance *)tabBarItemAppearance
fromItemAppearanceProps:(nonnull NSDictionary *)itemAppearanceProps
{
if ([itemAppearanceProps[@"normal"] isKindOfClass:[NSDictionary class]]) {
[self configureTabBarItemStateAppearance:tabBarItemAppearance.normal
fromItemStateAppearanceProps:itemAppearanceProps[@"normal"]];
}
if ([itemAppearanceProps[@"selected"] isKindOfClass:[NSDictionary class]]) {
[self configureTabBarItemStateAppearance:tabBarItemAppearance.selected
fromItemStateAppearanceProps:itemAppearanceProps[@"selected"]];
}
if ([itemAppearanceProps[@"focused"] isKindOfClass:[NSDictionary class]]) {
[self configureTabBarItemStateAppearance:tabBarItemAppearance.focused
fromItemStateAppearanceProps:itemAppearanceProps[@"focused"]];
}
if ([itemAppearanceProps[@"disabled"] isKindOfClass:[NSDictionary class]]) {
[self configureTabBarItemStateAppearance:tabBarItemAppearance.disabled
fromItemStateAppearanceProps:itemAppearanceProps[@"disabled"]];
}
}
+ (void)configureTabBarItemStateAppearance:(nonnull UITabBarItemStateAppearance *)tabBarItemStateAppearance
fromItemStateAppearanceProps:(nonnull NSDictionary *)itemStateAppearanceProps
{
NSMutableDictionary *titleTextAttributes = [[NSMutableDictionary alloc] init];
if (itemStateAppearanceProps[@"tabBarItemTitleFontFamily"] != nil ||
itemStateAppearanceProps[@"tabBarItemTitleFontSize"] != nil ||
itemStateAppearanceProps[@"tabBarItemTitleFontWeight"] != nil ||
itemStateAppearanceProps[@"tabBarItemTitleFontStyle"] != nil) {
titleTextAttributes[NSFontAttributeName] =
[RCTFont updateFont:nil
withFamily:itemStateAppearanceProps[@"tabBarItemTitleFontFamily"]
size:itemStateAppearanceProps[@"tabBarItemTitleFontSize"]
weight:itemStateAppearanceProps[@"tabBarItemTitleFontWeight"]
style:itemStateAppearanceProps[@"tabBarItemTitleFontStyle"]
variant:nil
scaleMultiplier:1.0];
}
if (itemStateAppearanceProps[@"tabBarItemTitleFontColor"] != nil) {
titleTextAttributes[NSForegroundColorAttributeName] =
[RCTConvert UIColor:itemStateAppearanceProps[@"tabBarItemTitleFontColor"]];
}
if ([titleTextAttributes count] > 0) {
tabBarItemStateAppearance.titleTextAttributes = titleTextAttributes;
}
if (itemStateAppearanceProps[@"tabBarItemBadgeBackgroundColor"] != nil) {
tabBarItemStateAppearance.badgeBackgroundColor =
[RCTConvert UIColor:itemStateAppearanceProps[@"tabBarItemBadgeBackgroundColor"]];
}
if (itemStateAppearanceProps[@"tabBarItemIconColor"] != nil) {
tabBarItemStateAppearance.iconColor = [RCTConvert UIColor:itemStateAppearanceProps[@"tabBarItemIconColor"]];
}
if (itemStateAppearanceProps[@"tabBarItemTitlePositionAdjustment"] != nil) {
tabBarItemStateAppearance.titlePositionAdjustment =
[RCTConvert UIOffset:itemStateAppearanceProps[@"tabBarItemTitlePositionAdjustment"]];
}
}
@end
@@ -0,0 +1,164 @@
#import <UIKit/UIKit.h>
#import "RNSTabBarAppearanceCoordinator.h"
#import "RNSTabsScreenViewController.h"
#if !TARGET_OS_TV
#import "RNSOrientationProviding.h"
#endif // !TARGET_OS_TV
NS_ASSUME_NONNULL_BEGIN
@protocol RNSReactTransactionObserving
- (void)reactMountingTransactionWillMount;
- (void)reactMountingTransactionDidMount;
@end
/**
* This controller is responsible for tab management & all other responsibilities coming from the fact of inheritance
* from `UITabBarController`. It is limited only to the child view controllers of type `RNSTabsScreenViewController`,
* however.
*
* Updates made by this controller are synchronized by `RNSReactTransactionObserving` protocol,
* i.e. if you made changes through one of signals method, unless you flush them immediately (not needed atm), they will
* be executed only after react finishes the transaction (from within transaction execution block).
*/
@interface RNSTabBarController : UITabBarController <
RNSReactTransactionObserving
#if !TARGET_OS_TV
,
RNSOrientationProviding
#endif // !TARGET_OS_TV
>
- (instancetype)initWithTabsHostComponentView:(nullable RNSBottomTabsHostComponentView *)tabsHostComponentView;
/**
* Get reference to the host component view that owns this tab bar controller.
*
* Might return null in cases where the controller view hierararchy is not attached to parent.
*/
@property (nonatomic, readonly, nullable) RNSBottomTabsHostComponentView *tabsHostComponentView;
/**
* Tab bar appearance coordinator. If you need to update tab bar appearance avoid using this one directly. Send the
* controller a signal, invalidate the tab bar appearance & either wait for the update flush or flush it manually.
*/
@property (nonatomic, readonly, strong, nonnull) RNSTabBarAppearanceCoordinator *tabBarAppearanceCoordinator;
/**
* Update tab controller state with previously provided children.
*
* This method does nothing if the children have not been changed / update has not been requested before.
* The requested update is performed immediately. If you do not need this, consider just raising an appropriate
* invalidation signal & let the controller decide when to flush the updates.
*/
- (void)updateReactChildrenControllersIfNeeded;
/**
* Force update of the tab controller state with previously provided children.
*
* The requested update is performed immediately. If you do not need this, consider just raising an appropriate
* invalidation signal & let the controller decide when to flush the updates.
*/
- (void)updateReactChildrenControllers;
/**
* Find out which tab bar controller is currently focused & select it.
*
* This method does nothing if the update has not been previously requested.
* If needed, the requested update is performed immediately. If you do not need this, consider just raising an
* appropriate invalidation signal & let the controller decide when to flush the updates.
*/
- (void)updateSelectedViewControllerIfNeeded;
/**
* Find out which tab bar controller is currently focused & select it.
*
* The requested update is performed immediately. If you do not need this, consider just raising an appropriate
* invalidation signal & let the controller decide when to flush the updates.
*/
- (void)updateSelectedViewController;
/**
* Updates the tab bar appearance basing on configuration sources (host view, tab screens).
*
* This method does nothing if the update has not been previously requested.
* If needed, the requested update is performed immediately. If you do not need this, consider just raising an
* appropriate invalidation signal & let the controller decide when to flush the updates.
*/
- (void)updateTabBarAppearanceIfNeeded;
/**
* Updates the tab bar appearance basing on configuration sources (host view, tab screens).
*
* The requested update is performed immediately. If you do not need this, consider just raising an appropriate
* invalidation signal & let the controller decide when to flush the updates.
*/
- (void)updateTabBarAppearance;
/**
* Updates the interface orientation based on selected tab screen and its children.
*
* This method does nothing if the update has not been previously requested.
* If needed, the requested update is performed immediately. If you do not need this, consider just raising an
* appropriate invalidation signal & let the controller decide when to flush the updates.
*/
- (void)updateOrientationIfNeeded;
/**
* Updates the interface orientation based on selected tab screen and its children.
*
* The requested update is performed immediately. If you do not need this, consider just raising an appropriate
* invalidation signal & let the controller decide when to flush the updates.
*/
- (void)updateOrientation;
@end
#pragma mark - Signals
/**
* This extension defines various invalidation signals that you can send to the controller, to notify it that it needs
* to take some action.
*/
@interface RNSTabBarController ()
/**
* Tell the controller that react provided tabs have changed (count / instances) & the child view controllers need to be
* updated.
*
* This also automatically raises `needsReactChildrenUpdate` flag, no need to call it manually.
*/
- (void)childViewControllersHaveChangedTo:(nonnull NSArray<RNSTabsScreenViewController *> *)childViewControllers;
/**
* Tell the controller that react provided tabs have changed (count / instances) & the child view controllers need to be
* updated.
*
* Do not raise this signal only when focused state of the tab has changed - use `needsSelectedTabUpdate` instead.
*/
@property (nonatomic, readwrite) bool needsUpdateOfReactChildrenControllers;
/**
* Tell the controller that react provided tabs have changed (count / instances) & the child view controllers need to be
* updated.
*/
@property (nonatomic, readwrite) bool needsUpdateOfSelectedTab;
/**
* Tell the controller that some configuration regarding the tab bar apperance has changed & the appearance requires
* update.
*/
@property (nonatomic, readwrite) bool needsUpdateOfTabBarAppearance;
/**
* Tell the controller that some configuration regarding interface orientation has changed & it requires update.
*/
@property (nonatomic, readwrite) bool needsOrientationUpdate;
@end
NS_ASSUME_NONNULL_END
@@ -0,0 +1,250 @@
#import "RNSTabBarController.h"
#import <React/RCTAssert.h>
#import <React/RCTLog.h>
#import "RNSLog.h"
#import "RNSScreenWindowTraits.h"
@implementation RNSTabBarController {
NSArray<RNSTabsScreenViewController *> *_Nullable _tabScreenControllers;
#if !RCT_NEW_ARCH_ENABLED
BOOL _isControllerFlushBlockScheduled;
#endif // !RCT_NEW_ARCH_ENABLED
}
- (instancetype)init
{
if (self = [super init]) {
_tabScreenControllers = nil;
_tabBarAppearanceCoordinator = [RNSTabBarAppearanceCoordinator new];
_tabsHostComponentView = nil;
#if !RCT_NEW_ARCH_ENABLED
_isControllerFlushBlockScheduled = NO;
#endif // !RCT_NEW_ARCH_ENABLED
}
return self;
}
- (instancetype)initWithTabsHostComponentView:(nullable RNSBottomTabsHostComponentView *)tabsHostComponentView
{
if (self = [self init]) {
_tabsHostComponentView = tabsHostComponentView;
}
return self;
}
- (void)tabBar:(UITabBar *)tabBar didSelectItem:(UITabBarItem *)item
{
RNSLog(@"TabBar: %@ didSelectItem: %@", tabBar, item);
}
#pragma mark-- Signals
- (void)childViewControllersHaveChangedTo:(NSArray<RNSTabsScreenViewController *> *)reactChildControllers
{
_tabScreenControllers = reactChildControllers;
self.needsUpdateOfReactChildrenControllers = true;
}
- (void)setNeedsUpdateOfReactChildrenControllers:(bool)needsReactChildrenUpdate
{
_needsUpdateOfReactChildrenControllers = true;
self.needsUpdateOfSelectedTab = true;
#if !RCT_NEW_ARCH_ENABLED
[self scheduleControllerUpdateIfNeeded];
#endif // !RCT_NEW_ARCH_ENABLED
}
- (void)setNeedsUpdateOfSelectedTab:(bool)needsSelectedTabUpdate
{
_needsUpdateOfSelectedTab = needsSelectedTabUpdate;
if (needsSelectedTabUpdate) {
_needsOrientationUpdate = true;
}
#if !RCT_NEW_ARCH_ENABLED
[self scheduleControllerUpdateIfNeeded];
#endif // !RCT_NEW_ARCH_ENABLED
}
- (void)setNeedsUpdateOfTabBarAppearance:(bool)needsUpdateOfTabBarAppearance
{
_needsUpdateOfTabBarAppearance = needsUpdateOfTabBarAppearance;
#if !RCT_NEW_ARCH_ENABLED
[self scheduleControllerUpdateIfNeeded];
#endif // !RCT_NEW_ARCH_ENABLED
}
- (void)setNeedsOrientationUpdate:(bool)needsOrientationUpdate
{
_needsOrientationUpdate = needsOrientationUpdate;
#if !RCT_NEW_ARCH_ENABLED
[self scheduleControllerUpdateIfNeeded];
#endif // !RCT_NEW_ARCH_ENABLED
}
#pragma mark-- RNSReactTransactionObserving
- (void)reactMountingTransactionWillMount
{
RNSLog(@"TabBarCtrl mountintTransactionWillMount");
}
- (void)reactMountingTransactionDidMount
{
RNSLog(@"TabBarCtrl mountintTransactionDidMount running updates");
[self updateReactChildrenControllersIfNeeded];
[self updateSelectedViewControllerIfNeeded];
[self updateTabBarAppearanceIfNeeded];
[self updateOrientationIfNeeded];
}
#pragma mark-- Signals related
- (void)updateReactChildrenControllersIfNeeded
{
if (_needsUpdateOfReactChildrenControllers) {
[self updateReactChildrenControllers];
}
}
- (void)updateReactChildrenControllers
{
RNSLog(@"TabBarCtrl updateReactChildrenControllers");
_needsUpdateOfReactChildrenControllers = false;
if (_tabScreenControllers == nil) {
RCTLogWarn(@"[RNScreens] Attempt to update react children while the _updatedChildren array is nil!");
return;
}
[self setViewControllers:_tabScreenControllers animated:[[self viewControllers] count] != 0];
}
- (void)updateSelectedViewControllerIfNeeded
{
if (_needsUpdateOfSelectedTab) {
[self updateSelectedViewController];
}
}
- (void)updateSelectedViewController
{
RNSLog(@"TabBarCtrl updateSelectedViewController");
_needsUpdateOfSelectedTab = false;
#if !defined(NDEBUG)
[self assertExactlyOneFocusedTab];
#endif
RNSTabsScreenViewController *_Nullable selectedViewController = nil;
for (RNSTabsScreenViewController *tabViewController in self.viewControllers) {
RNSLog(
@"Update Selected View Controller [%ld] isFocused %d",
tabViewController.tabScreenComponentView.tag,
tabViewController.tabScreenComponentView.isSelectedScreen);
if (tabViewController.tabScreenComponentView.isSelectedScreen == true) {
selectedViewController = tabViewController;
break;
}
}
RCTAssert(selectedViewController != nil, @"[RNScreens] No selected view controller!");
RNSLog(@"Change selected view controller to: %@", selectedViewController);
[selectedViewController.tabScreenComponentView overrideScrollViewBehaviorInFirstDescendantChainIfNeeded];
[self setSelectedViewController:selectedViewController];
}
- (void)updateTabBarAppearanceIfNeeded
{
if (_needsUpdateOfTabBarAppearance) {
[self updateTabBarAppearance];
}
}
- (void)updateTabBarAppearance
{
RNSLog(@"TabBarCtrl updateTabBarAppearance");
_needsUpdateOfTabBarAppearance = false;
[_tabBarAppearanceCoordinator updateAppearanceOfTabBar:[self tabBar]
withHostComponentView:self.tabsHostComponentView
tabScreenControllers:_tabScreenControllers
imageLoader:[self.tabsHostComponentView reactImageLoader]];
}
#if !defined(NDEBUG)
- (void)assertExactlyOneFocusedTab
{
int selectedCount = 0;
for (RNSTabsScreenViewController *tabViewController in _tabScreenControllers) {
if (tabViewController.tabScreenComponentView.isSelectedScreen) {
++selectedCount;
}
}
RCTAssert(
selectedCount == 1, @"[RNScreens] Invariant violation. Expected exactly 1 focused tab, got: %d", selectedCount);
}
#endif
#if !RCT_NEW_ARCH_ENABLED
#pragma mark - LEGACY Paper scheduling methods
// TODO: These could be moved to separate scheduler class
- (void)scheduleControllerUpdateIfNeeded
{
if (_isControllerFlushBlockScheduled) {
return;
}
_isControllerFlushBlockScheduled = YES;
auto *__weak weakSelf = self;
dispatch_async(dispatch_get_main_queue(), ^{
auto *strongSelf = weakSelf;
if (strongSelf == nil) {
return;
}
strongSelf->_isControllerFlushBlockScheduled = NO;
[strongSelf reactMountingTransactionWillMount];
[strongSelf reactMountingTransactionDidMount];
});
}
#endif // !RCT_NEW_ARCH_ENABLED
- (void)updateOrientationIfNeeded
{
if (_needsOrientationUpdate) {
[self updateOrientation];
}
}
- (void)updateOrientation
{
_needsOrientationUpdate = false;
[RNSScreenWindowTraits enforceDesiredDeviceOrientation];
}
#pragma mark - RNSOrientationProviding
#if !TARGET_OS_TV
- (RNSOrientation)evaluateOrientation
{
if ([self.selectedViewController respondsToSelector:@selector(evaluateOrientation)]) {
id<RNSOrientationProviding> selected = static_cast<id<RNSOrientationProviding>>(self.selectedViewController);
return [selected evaluateOrientation];
}
return RNSOrientationInherit;
}
#endif // !TARGET_OS_TV
@end
@@ -0,0 +1,9 @@
#import <UIKit/UIKit.h>
NS_ASSUME_NONNULL_BEGIN
@interface RNSTabBarControllerDelegate : NSObject <UITabBarControllerDelegate>
@end
NS_ASSUME_NONNULL_END
@@ -0,0 +1,93 @@
#import "RNSTabBarControllerDelegate.h"
#import <React/RCTAssert.h>
#import "RNSTabBarController.h"
#import "RNSTabsScreenViewController.h"
@implementation RNSTabBarControllerDelegate
- (BOOL)tabBarController:(UITabBarController *)tabBarController
shouldSelectViewController:(UIViewController *)viewController
{
RCTAssert(
[tabBarController isKindOfClass:RNSTabBarController.class],
@"[RNScreens] Unexpected type of controller: %@",
tabBarController.class);
// Can be UINavigationController in case of MoreNavigationController
RCTAssert(
[viewController isKindOfClass:RNSTabsScreenViewController.class] ||
[viewController isKindOfClass:UINavigationController.class],
@"[RNScreens] Unexpected type of controller: %@",
viewController.class);
RNSTabBarController *tabBarCtrl = static_cast<RNSTabBarController *>(tabBarController);
RNSTabsScreenViewController *tabScreenCtrl = static_cast<RNSTabsScreenViewController *>(viewController);
#if !TARGET_OS_TV
// When the moreNavigationController is selected, we want to show it
// TODO: this solution only works for uncontrolled mode. Add support for controlled mode as well.
if ([self shouldAllowMoreControllerSelection:tabBarCtrl] &&
viewController == tabBarController.moreNavigationController) {
return YES;
}
#endif // !TARGET_OS_TV
bool repeatedSelectionHandledNatively = false;
// Detect repeated selection and inform tabScreenController
if ([tabBarCtrl selectedViewController] == tabScreenCtrl) {
repeatedSelectionHandledNatively = [tabScreenCtrl tabScreenSelectedRepeatedly];
}
// TODO: send an event with information about event being handled natively
if (!repeatedSelectionHandledNatively) {
[tabBarCtrl.tabsHostComponentView
emitOnNativeFocusChangeRequestSelectedTabScreen:tabScreenCtrl.tabScreenComponentView];
// TODO: handle overrideScrollViewBehaviorInFirstDescendantChainIfNeeded for natively-driven tabs
return ![self shouldPreventNativeTabChangeWithinTabBarController:tabBarCtrl];
}
// TODO: handle enforcing orientation with natively-driven tabs
// As we're selecting the same controller, returning both true and false works here.
return true;
}
- (void)tabBarController:(UITabBarController *)tabBarController
didSelectViewController:(UIViewController *)viewController
{
RCTAssert(
[tabBarController isKindOfClass:RNSTabBarController.class],
@"[RNScreens] Unexpected type of controller: %@",
tabBarController.class);
// Can be UINavigationController in case of MoreNavigationController
RCTAssert(
[viewController isKindOfClass:RNSTabsScreenViewController.class] ||
[viewController isKindOfClass:UINavigationController.class],
@"[RNScreens] Unexpected type of controller: %@",
viewController.class);
#if !TARGET_OS_TV
// When the moreNavigationController is selected, we want to show it
if ([self shouldAllowMoreControllerSelection:static_cast<RNSTabBarController *>(tabBarController)] &&
viewController == tabBarController.moreNavigationController) {
// Hide the navigation bar for the more controller
[tabBarController.moreNavigationController setNavigationBarHidden:YES animated:NO];
}
#endif // !TARGET_OS_TV
}
- (BOOL)shouldPreventNativeTabChangeWithinTabBarController:(nonnull RNSTabBarController *)tabBarCtrl
{
// This handles the tabsHostComponentView nullability
return [tabBarCtrl.tabsHostComponentView experimental_controlNavigationStateInJS] ?: NO;
}
- (BOOL)shouldAllowMoreControllerSelection:(nonnull RNSTabBarController *)tabBarCtrl
{
return ![tabBarCtrl.tabsHostComponentView experimental_controlNavigationStateInJS] ?: YES;
}
@end
@@ -0,0 +1,54 @@
#import <UIKit/UIKit.h>
#import "RNSBottomTabsScreenComponentView.h"
#import "RNSBottomTabsSpecialEffectsSupporting.h"
#if !TARGET_OS_TV
#import "RNSOrientationProviding.h"
#endif // !TARGET_OS_TV
NS_ASSUME_NONNULL_BEGIN
@interface RNSTabsScreenViewController : UIViewController
#if !TARGET_OS_TV
<RNSOrientationProviding>
#endif // !TARGET_OS_TV
@property (nonatomic, strong, readonly, nullable) RNSBottomTabsScreenComponentView *tabScreenComponentView;
@property (nonatomic, weak, readonly, nullable) id<RNSBottomTabsSpecialEffectsSupporting> tabsSpecialEffectsDelegate;
/**
* Tell the controller that the tab screen it owns has got its react-props-focus changed.
*/
- (void)tabScreenFocusHasChanged;
/**
* Tell the controller that the tab screen it owns has got its react-props related to appearance changed.
*/
- (void)tabItemAppearanceHasChanged;
/**
* Tell the controller that the tab screen it owns has got its react-props-orientation changed.
*/
- (void)tabScreenOrientationHasChanged;
/**
* Tell the controller that the tab item related to this controller has been selected again after being presented.
* Returns boolean indicating whether the action has been handled.
*/
- (bool)tabScreenSelectedRepeatedly;
/**
* Set new special effects delegate.
*/
- (void)setTabsSpecialEffectsDelegate:(nonnull id<RNSBottomTabsSpecialEffectsSupporting>)delegate;
/**
* Inform the controller about resignation from being special effects delegate. If resignation comes from current
* tabsSpecialEffectsDelegate, this method sets tabsSpecialEffectsDelegate to nil. If tabsSpecialEffectsDelegate has
* already changed (to other delegate or nil), this method does nothing.
*/
- (void)clearTabsSpecialEffectsDelegateIfNeeded:(nonnull id<RNSBottomTabsSpecialEffectsSupporting>)delegate;
@end
NS_ASSUME_NONNULL_END
@@ -0,0 +1,135 @@
#import "RNSTabsScreenViewController.h"
#import "RNSLog.h"
#import "RNSScrollViewFinder.h"
#import "RNSTabBarController.h"
#import "UIScrollView+RNScreens.h"
@implementation RNSTabsScreenViewController
- (nullable RNSTabBarController *)findTabBarController
{
return static_cast<RNSTabBarController *_Nullable>(self.tabBarController);
}
- (nullable RNSBottomTabsScreenComponentView *)tabScreenComponentView
{
return static_cast<RNSBottomTabsScreenComponentView *>(self.view);
}
- (void)tabScreenFocusHasChanged
{
RNSLog(
@"TabScreen [%ld] changed focus: %d",
self.tabScreenComponentView.tag,
self.tabScreenComponentView.isSelectedScreen);
// The focus of owned tab has been updated from react. We tell the parent controller that it should update the
// container.
[[self findTabBarController] setNeedsUpdateOfSelectedTab:true];
}
- (void)tabItemAppearanceHasChanged
{
[[self findTabBarController] setNeedsUpdateOfTabBarAppearance:true];
}
- (void)tabScreenOrientationHasChanged
{
[[self findTabBarController] setNeedsOrientationUpdate:true];
}
- (void)viewWillAppear:(BOOL)animated
{
[self.tabScreenComponentView.reactEventEmitter emitOnWillAppear];
}
- (void)viewDidAppear:(BOOL)animated
{
[self.tabScreenComponentView.reactEventEmitter emitOnDidAppear];
}
- (void)viewWillDisappear:(BOOL)animated
{
[self.tabScreenComponentView.reactEventEmitter emitOnWillDisappear];
}
- (void)viewDidDisappear:(BOOL)animated
{
[self.tabScreenComponentView.reactEventEmitter emitOnDidDisappear];
}
- (void)didMoveToParentViewController:(UIViewController *)parent
{
RNSLog(@"TabScreen [%ld] ctrl moved to parent: %@", self.tabScreenComponentView.tag, parent);
if (parent == nil) {
return;
}
// Can be UINavigationController in case of MoreNavigationController
RCTAssert(
[parent isKindOfClass:RNSTabBarController.class] || [parent isKindOfClass:UINavigationController.class],
@"[RNScreens] TabScreenViewController added to parent of unexpected type: %@",
parent.class);
if ([parent isKindOfClass:UINavigationController.class]) {
// Hide the navigation bar for the more controller
[(UINavigationController *)parent setNavigationBarHidden:YES animated:YES];
}
RNSTabBarController *tabBarCtrl = [self findTabBarController];
RCTAssert(
tabBarCtrl != nil, @"[RNScreens] nullish tabBarCtrl after TabScreenViewController has been added to parent");
[tabBarCtrl setNeedsUpdateOfTabBarAppearance:true];
[tabBarCtrl updateTabBarAppearanceIfNeeded];
}
- (void)setTabsSpecialEffectsDelegate:(id<RNSBottomTabsSpecialEffectsSupporting>)delegate
{
RCTAssert(
delegate != nil,
@"[RNScreens] can't set special effects delegate to nil. Use clearTabsSpecialEffectsDelegateIfNeeded instead.");
_tabsSpecialEffectsDelegate = delegate;
}
- (void)clearTabsSpecialEffectsDelegateIfNeeded:(id<RNSBottomTabsSpecialEffectsSupporting>)delegate
{
if (_tabsSpecialEffectsDelegate == delegate) {
_tabsSpecialEffectsDelegate = nil;
}
}
- (bool)tabScreenSelectedRepeatedly
{
if ([self tabsSpecialEffectsDelegate] != nil) {
return [[self tabsSpecialEffectsDelegate] onRepeatedTabSelectionOfTabScreenController:self];
} else if (self.tabScreenComponentView.shouldUseRepeatedTabSelectionScrollToTopSpecialEffect) {
UIScrollView *scrollView =
[RNSScrollViewFinder findScrollViewInFirstDescendantChainFrom:[self tabScreenComponentView]];
return [scrollView rnscreens_scrollToTop];
}
return false;
}
#if !TARGET_OS_TV
- (RNSOrientation)evaluateOrientation
{
if ([self.childViewControllers.lastObject respondsToSelector:@selector(evaluateOrientation)]) {
id<RNSOrientationProviding> child = static_cast<id<RNSOrientationProviding>>(self.childViewControllers.lastObject);
RNSOrientation childOrientation = [child evaluateOrientation];
if (childOrientation != RNSOrientationInherit) {
return childOrientation;
}
}
return self.tabScreenComponentView.orientation;
}
#endif // !TARGET_OS_TV
@end
@@ -0,0 +1,22 @@
#import "RNSBottomTabsHostComponentView.h"
#if defined(__cplusplus) && RCT_NEW_ARCH_ENABLED
#import "RNSBottomTabsShadowNode.h"
#endif // defined(__cplusplus) && RCT_NEW_ARCH_ENABLED
NS_ASSUME_NONNULL_BEGIN
#if defined(__cplusplus) && RCT_NEW_ARCH_ENABLED
@class RCTImageLoader;
@interface RNSBottomTabsHostComponentView (RNSImageLoader)
- (nullable RCTImageLoader *)retrieveImageLoaderFromState:
(facebook::react::RNSBottomTabsShadowNode::ConcreteState::Shared)state;
@end
#endif // defined(__cplusplus) && RCT_NEW_ARCH_ENABLED
NS_ASSUME_NONNULL_END
@@ -0,0 +1,23 @@
#import "RNSBottomTabsHostComponentView+RNSImageLoader.h"
#if RCT_NEW_ARCH_ENABLED
#import <React/RCTImageLoader.h>
#import <react/utils/ManagedObjectWrapper.h>
@implementation RNSBottomTabsHostComponentView (RNSImageLoader)
- (nullable RCTImageLoader *)retrieveImageLoaderFromState:
(facebook::react::RNSBottomTabsShadowNode::ConcreteState::Shared)receivedState
{
if (auto imgLoaderPtr = receivedState.get()->getData().getImageLoader().lock()) {
return react::unwrapManagedObject(imgLoaderPtr);
}
RCTLogWarn(@"[RNScreens] unable to retrieve RCTImageLoader");
return nil;
}
@end
#endif // RCT_NEW_ARCH_ENABLED
@@ -0,0 +1,31 @@
// 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
@@ -0,0 +1,5 @@
#import "RNSReactBaseView.h"
@implementation RNSReactBaseView
@end
@@ -0,0 +1,7 @@
#pragma once
#if __has_include(<RNScreens/RNScreens-Swift.h>)
#import <RNScreens/RNScreens-Swift.h>
#else
#import "RNScreens-Swift.h"
#endif
@@ -0,0 +1,322 @@
#import <React/RCTConversions.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
RNSBottomTabsIconType RNSBottomTabsIconTypeFromIcon(react::RNSBottomTabsScreenIconType iconType)
{
using enum facebook::react::RNSBottomTabsScreenIconType;
switch (iconType) {
case Image:
return RNSBottomTabsIconTypeImage;
case Template:
return RNSBottomTabsIconTypeTemplate;
case SfSymbol:
return RNSBottomTabsIconTypeSfSymbol;
}
}
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;
}
}; // namespace rnscreens::conversion
@@ -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
@@ -0,0 +1,145 @@
#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;
}
}
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
@@ -0,0 +1,88 @@
#pragma once
#if defined(__cplusplus)
#import <React/RCTImageSource.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
RNSBottomTabsIconType RNSBottomTabsIconTypeFromIcon(react::RNSBottomTabsScreenIconType iconType);
RNSBottomTabsScreenSystemItem RNSBottomTabsScreenSystemItemFromReactRNSBottomTabsScreenSystemItem(
react::RNSBottomTabsScreenSystemItem systemItem);
UITabBarSystemItem RNSBottomTabsScreenSystemItemToUITabBarSystemItem(RNSBottomTabsScreenSystemItem systemItem);
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);
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
#endif
@@ -0,0 +1,61 @@
#import "RNSConversions.h"
#import <React/RCTLog.h>
namespace rnscreens::conversion {
#if !TARGET_OS_TV
UIInterfaceOrientationMask UIInterfaceOrientationMaskFromRNSOrientation(RNSOrientation orientation)
{
switch (orientation) {
case RNSOrientationAll:
return UIInterfaceOrientationMaskAll;
case RNSOrientationAllButUpsideDown:
return UIInterfaceOrientationMaskAllButUpsideDown;
case RNSOrientationPortrait:
return UIInterfaceOrientationMaskPortrait | UIInterfaceOrientationMaskPortraitUpsideDown;
case RNSOrientationPortraitUp:
return UIInterfaceOrientationMaskPortrait;
case RNSOrientationPortraitDown:
return UIInterfaceOrientationMaskPortraitUpsideDown;
case RNSOrientationLandscape:
return UIInterfaceOrientationMaskLandscape;
case RNSOrientationLandscapeLeft:
return UIInterfaceOrientationMaskLandscapeLeft;
case RNSOrientationLandscapeRight:
return UIInterfaceOrientationMaskLandscapeRight;
case RNSOrientationInherit:
RCTLogError(@"[RNScreens] RNSOrientationInherit does not map directly to any UIInterfaceOrientationMask");
return 0;
default:
RCTLogError(@"[RNScreens] Unsupported orientation");
return 0;
}
}
RNSOrientation RNSOrientationFromUIInterfaceOrientationMask(UIInterfaceOrientationMask orientationMask)
{
switch (orientationMask) {
case UIInterfaceOrientationMaskAll:
return RNSOrientationAll;
case UIInterfaceOrientationMaskAllButUpsideDown:
return RNSOrientationAllButUpsideDown;
case UIInterfaceOrientationMaskLandscape:
return RNSOrientationLandscape;
case UIInterfaceOrientationMaskLandscapeLeft:
return RNSOrientationLandscapeLeft;
case UIInterfaceOrientationLandscapeRight:
return RNSOrientationLandscapeRight;
case UIInterfaceOrientationMaskPortraitUpsideDown:
return RNSOrientationPortraitDown;
case UIInterfaceOrientationMaskPortrait:
return RNSOrientationPortraitUp;
case UIInterfaceOrientationMaskPortrait | UIInterfaceOrientationMaskPortraitUpsideDown:
return RNSOrientationPortrait;
default:
RCTLogError(@"[RNScreens] Unsupported orientation mask");
return RNSOrientationInherit;
}
}
#endif // !TARGET_OS_TV
}; // namespace rnscreens::conversion
@@ -0,0 +1,7 @@
#import <React/RCTEventDispatcherProtocol.h>
@interface RNSHeaderHeightChangeEvent : NSObject <RCTEvent>
- (instancetype)initWithEventName:(NSString *)eventName reactTag:(NSNumber *)reactTag headerHeight:(double)headerHeight;
@end
@@ -0,0 +1,59 @@
#import "RNSHeaderHeightChangeEvent.h"
#import <React/RCTAssert.h>
@implementation RNSHeaderHeightChangeEvent {
double _headerHeight;
}
@synthesize viewTag = _viewTag;
@synthesize eventName = _eventName;
- (instancetype)initWithEventName:(NSString *)eventName reactTag:(NSNumber *)reactTag headerHeight:(double)headerHeight
{
RCTAssertParam(reactTag);
if ((self = [super init])) {
_eventName = [eventName copy];
_viewTag = reactTag;
_headerHeight = headerHeight;
}
return self;
}
RCT_NOT_IMPLEMENTED(-(instancetype)init)
- (NSDictionary *)body
{
NSDictionary *body = @{
@"headerHeight" : @(_headerHeight),
};
return body;
}
- (BOOL)canCoalesce
{
return YES;
}
- (uint16_t)coalescingKey
{
return _headerHeight;
}
- (id<RCTEvent>)coalesceWithEvent:(id<RCTEvent>)newEvent
{
return newEvent;
}
+ (NSString *)moduleDotMethod
{
return @"RCTEventEmitter.receiveEvent";
}
- (NSArray *)arguments
{
return @[ self.viewTag, RCTNormalizeInputEventName(self.eventName), [self body] ];
}
@end
@@ -0,0 +1,11 @@
#import <React/RCTEventDispatcherProtocol.h>
@interface RNSScreenViewEvent : NSObject <RCTEvent>
- (instancetype)initWithEventName:(NSString *)eventName
reactTag:(NSNumber *)reactTag
progress:(double)progress
closing:(int)closing
goingForward:(int)goingForward;
@end
@@ -0,0 +1,59 @@
#import "RNSScreenViewEvent.h"
#import <React/RCTAssert.h>
@implementation RNSScreenViewEvent {
double _progress;
int _closing;
int _goingForward;
}
@synthesize viewTag = _viewTag;
@synthesize eventName = _eventName;
- (instancetype)initWithEventName:(NSString *)eventName
reactTag:(NSNumber *)reactTag
progress:(double)progress
closing:(int)closing
goingForward:(int)goingForward
{
RCTAssertParam(reactTag);
if ((self = [super init])) {
_eventName = [eventName copy];
_viewTag = reactTag;
_progress = progress;
_closing = closing;
_goingForward = goingForward;
}
return self;
}
RCT_NOT_IMPLEMENTED(-(instancetype)init)
- (NSDictionary *)body
{
NSDictionary *body = @{
@"progress" : @(_progress),
@"closing" : @(_closing ? 1 : 0),
@"goingForward" : @(_goingForward ? 1 : 0),
};
return body;
}
- (BOOL)canCoalesce
{
return NO;
}
+ (NSString *)moduleDotMethod
{
return @"RCTEventEmitter.receiveEvent";
}
- (NSArray *)arguments
{
return @[ self.viewTag, RCTNormalizeInputEventName(self.eventName), [self body] ];
}
@end
@@ -0,0 +1,7 @@
import Foundation
@objc
public protocol ReactMountingTransactionObserving {
func reactMountingTransactionWillMount()
func reactMountingTransactionDidMount()
}
@@ -0,0 +1,6 @@
// TODO: investigate objc - swift interop and deduplicate this code
// This class needs to follow the RNSOrientationProviding from objc.
@objc
public protocol RNSOrientationProvidingSwift {
func evaluateOrientation() -> RNSOrientationSwift
}
@@ -0,0 +1,14 @@
// TODO: investigate objc - swift interop and deduplicate this code
// This enum needs to follow the RNSOrientation from objc.
@objc
public enum RNSOrientationSwift: Int {
case inherit
case all
case allButUpsideDown
case portrait
case portraitUp
case portraitDown
case landscape
case landscapeLeft
case landscapeRight
}
@@ -0,0 +1,191 @@
///
/// @brief - Class responsible for applying all upcoming updates to SplitView.
///
/// This class is synchronizing UISplitViewController configuration props which are affecting the SplitView appearance with props passed to RNSSplitViewHostComponentView from the ElementTree.
///
class RNSSplitViewAppearanceApplicator {
///
/// @brief Function responsible for applying all updates to SplitView in correct order
///
/// It requests calling proper callbacks with batched SplitView updates on the AppearanceCoordinator object
///
/// @param splitView The view representing JS component which is sending updates.
/// @param splitViewController The controller associated with the SplitView component which receives updates and manages the native layer.
/// @param appearanceCoordinator The coordinator which is checking whether the update needs to be applied and if so, it executes the callback passed by this class.
///
public func updateAppearanceIfNeeded(
_ splitView: RNSSplitViewHostComponentView,
_ splitViewController: RNSSplitViewHostController,
_ appearanceCoordinator: RNSSplitViewAppearanceCoordinator
) {
appearanceCoordinator.updateIfNeeded(.generalUpdate) { [weak self] in
guard let self = self
else {
return
}
self.updateSplitViewConfiguration(for: splitView, withController: splitViewController)
}
appearanceCoordinator.updateIfNeeded(.secondaryScreenNavBarUpdate) { [weak self] in
guard let self = self
else {
return
}
splitViewController.refreshSecondaryNavBar()
}
appearanceCoordinator.updateIfNeeded(.displayModeUpdate) { [weak self] in
guard let self = self
else {
return
}
self.updateSplitViewDisplayMode(for: splitView, withController: splitViewController)
}
appearanceCoordinator.updateIfNeeded(.orientationUpdate) { [] in
RNSScreenWindowTraits.enforceDesiredDeviceOrientation()
}
}
///
/// @brief Function that applies all basic updates.
///
/// It calls all setters on RNSSplitViewHostController that doesn't require any custom logic and conditions to be met.
///
/// @param splitView The view representing JS component which is sending updates.
/// @param splitViewController The controller associated with the SplitView component which receives updates and manages the native layer.
///
private func updateSplitViewConfiguration(
for splitView: RNSSplitViewHostComponentView,
withController splitViewController: RNSSplitViewHostController
) {
// Step 1 - general settings
splitViewController.displayModeButtonVisibility = splitView.displayModeButtonVisibility
splitViewController.preferredSplitBehavior = splitView.preferredSplitBehavior
splitViewController.presentsWithGesture = splitView.presentsWithGesture
splitViewController.primaryEdge = splitView.primaryEdge
splitViewController.showsSecondaryOnlyButton = splitView.showSecondaryToggleButton
// Step 2.1 - validating column constraints
validateColumnConstraints(
minWidth: splitView.minimumPrimaryColumnWidth,
maxWidth: splitView.maximumPrimaryColumnWidth)
validateColumnConstraints(
minWidth: splitView.minimumSupplementaryColumnWidth,
maxWidth: splitView.maximumSupplementaryColumnWidth)
#if compiler(>=6.2)
if #available(iOS 26.0, *) {
validateColumnConstraints(
minWidth: splitView.minimumInspectorColumnWidth,
maxWidth: splitView.maximumInspectorColumnWidth)
}
#endif
// Step 2.2 - applying updates to columns
if splitView.minimumPrimaryColumnWidth >= 0 {
splitViewController.minimumPrimaryColumnWidth = splitView.minimumPrimaryColumnWidth
}
if splitView.maximumPrimaryColumnWidth >= 0 {
splitViewController.maximumPrimaryColumnWidth = splitView.maximumPrimaryColumnWidth
}
if splitView.preferredPrimaryColumnWidthOrFraction >= 0
&& splitView.preferredPrimaryColumnWidthOrFraction < 1
{
splitViewController.preferredPrimaryColumnWidthFraction =
splitView.preferredPrimaryColumnWidthOrFraction
} else if splitView.preferredPrimaryColumnWidthOrFraction >= 1 {
splitViewController.preferredPrimaryColumnWidth =
splitView.preferredPrimaryColumnWidthOrFraction
}
if splitView.minimumSupplementaryColumnWidth >= 0 {
splitViewController.minimumSupplementaryColumnWidth =
splitView.minimumSupplementaryColumnWidth
}
if splitView.maximumSupplementaryColumnWidth >= 0 {
splitViewController.maximumSupplementaryColumnWidth =
splitView.maximumSupplementaryColumnWidth
}
if splitView.preferredSupplementaryColumnWidthOrFraction >= 0
&& splitView.preferredSupplementaryColumnWidthOrFraction < 1
{
splitViewController.preferredSupplementaryColumnWidthFraction =
splitView.preferredSupplementaryColumnWidthOrFraction
} else if splitView.preferredSupplementaryColumnWidthOrFraction >= 1 {
splitViewController.preferredSupplementaryColumnWidth =
splitView.preferredSupplementaryColumnWidthOrFraction
}
#if compiler(>=6.2)
if #available(iOS 26.0, *) {
if splitView.minimumSecondaryColumnWidth >= 0 {
splitViewController.minimumSecondaryColumnWidth = splitView.minimumSecondaryColumnWidth
}
if splitView.preferredSecondaryColumnWidthOrFraction >= 0
&& splitView.preferredSecondaryColumnWidthOrFraction < 1
{
splitViewController.preferredSecondaryColumnWidthFraction =
splitView.preferredSecondaryColumnWidthOrFraction
} else if splitView.preferredSecondaryColumnWidthOrFraction >= 1 {
splitViewController.preferredSecondaryColumnWidth =
splitView.preferredSecondaryColumnWidthOrFraction
}
if splitView.minimumInspectorColumnWidth >= 0 {
splitViewController.minimumInspectorColumnWidth = splitView.minimumInspectorColumnWidth
}
if splitView.maximumInspectorColumnWidth >= 0 {
splitViewController.maximumInspectorColumnWidth = splitView.maximumInspectorColumnWidth
}
if splitView.preferredInspectorColumnWidthOrFraction >= 0
&& splitView.preferredInspectorColumnWidthOrFraction < 1
{
splitViewController.preferredInspectorColumnWidthFraction =
splitView.preferredInspectorColumnWidthOrFraction
} else if splitView.preferredInspectorColumnWidthOrFraction >= 1 {
splitViewController.preferredInspectorColumnWidth =
splitView.preferredInspectorColumnWidthOrFraction
}
}
#endif
// Step 2.3 - manipulating with inspector column
splitViewController.toggleSplitViewInspector(splitView.showInspector)
}
///
/// @brief Function that updates `preferredDisplayMode` property on SplitView.
///
/// `preferredDisplayMode` needs to have a dedicated flag to prevent updates from the JS, when other props updates the appearance.
/// It is crucial in the case, when `preferredDisplayMode` has changed due to some transition that was executed natively, e. g. after showing/hiding a column by a swipe.
/// In that case, any prop update incoming, would reset `preferredDisplayMode` to the state from JS, what doesn't look good.
///
/// @param splitView The view representing JS component which is sending updates.
/// @param splitViewController The controller associated with the SplitView component which receives updates and manages the native layer.
///
func updateSplitViewDisplayMode(
for splitView: RNSSplitViewHostComponentView,
withController splitViewController: RNSSplitViewHostController
) {
splitViewController.preferredDisplayMode = splitView.preferredDisplayMode
}
func validateColumnConstraints(minWidth: CGFloat, maxWidth: CGFloat) {
assert(
minWidth <= maxWidth,
"[RNScreens] SplitView column constraints are invalid: minWidth \(minWidth) cannot be greater than maxWidth \(maxWidth)"
)
}
}
@@ -0,0 +1,26 @@
///
/// @brief - A class that is responsible for coordinating SplitViewHost appearance updates.
///
/// It collects flags for SplitView appearance update actions and invalidates them.
/// It's also responsible for executing callbacks when the action is requested.
///
final class RNSSplitViewAppearanceCoordinator {
var updateFlags: RNSSplitViewAppearanceUpdateFlags = []
public func needs(_ updateFlag: RNSSplitViewAppearanceUpdateFlags) {
updateFlags.insert(updateFlag)
}
public func updateIfNeeded(
_ updateFlag: RNSSplitViewAppearanceUpdateFlags, _ updateCallback: () -> Void
) {
if isNeeded(updateFlag) {
updateFlags.remove(updateFlag)
updateCallback()
}
}
func isNeeded(_ updateFlag: RNSSplitViewAppearanceUpdateFlags) -> Bool {
updateFlags.contains(updateFlag)
}
}
@@ -0,0 +1,11 @@
///
/// @brief - A collection of flags, which can be invalidated on RNSSplitViewHostController to apply proper updates to SplitView
///
struct RNSSplitViewAppearanceUpdateFlags: OptionSet {
let rawValue: UInt8
static let generalUpdate = RNSSplitViewAppearanceUpdateFlags(rawValue: 1 << 0)
static let secondaryScreenNavBarUpdate = RNSSplitViewAppearanceUpdateFlags(rawValue: 1 << 1)
static let displayModeUpdate = RNSSplitViewAppearanceUpdateFlags(rawValue: 1 << 2)
static let orientationUpdate = RNSSplitViewAppearanceUpdateFlags(rawValue: 1 << 3)
}
@@ -0,0 +1,68 @@
#import <Foundation/Foundation.h>
// Hide C++ symbols from C compiler used when building Swift module
#if defined(__cplusplus)
#import <react/renderer/components/rnscreens/EventEmitters.h>
namespace react = facebook::react;
#endif // __cplusplus
NS_ASSUME_NONNULL_BEGIN
/**
* @class RNSSplitViewHostComponentEventEmitter
* @brief Responsible for emitting events from the native SplitView to the React Element Tree.
*/
@interface RNSSplitViewHostComponentEventEmitter : NSObject
/**
* @brief Emits the onCollapse event to notify associated component instance that the SplitView has collapsed.
*
* Call this method when the SplitView collapses to a single column e.g. due to
* size constraints or user interaction to notify React realm of this event.
*
* @return true if the event was successfully emitted, false otherwise.
*/
- (BOOL)emitOnCollapse;
- (BOOL)emitOnDisplayModeWillChangeFrom:(UISplitViewControllerDisplayMode)currentDisplayMode
to:(UISplitViewControllerDisplayMode)nextDisplayMode;
/**
* @brief Emits the onExpand event to notify associated component instance that the SplitView has expanded.
*
* Call this method when a SplitView is transitioning from the collapsed state to the multi-column display
* to notify React realm of this event.
*
* @return true if the event was successfully emitted, false otherwise.
*/
- (BOOL)emitOnExpand;
/**
* @brief Emits the onHideInspector event to notify that the inspector column is being hidden.
*
* Call this method when a user dismisses an inspector modal or hides it programmatically
* to notify React for synchronize the state based on this event.
*
* @return true if the event was emitted successfully, false otherwise.
*/
- (BOOL)emitOnHideInspector;
@end
#pragma mark - Hidden from Swift
#if defined(__cplusplus)
@interface RNSSplitViewHostComponentEventEmitter ()
/**
* @brief Sets the underlying C++ event emitter used for dispatching events to React.
*
* @param emitter A shared pointer to a RNSSplitViewHostEventEmitter instance.
*/
- (void)updateEventEmitter:(const std::shared_ptr<const react::RNSSplitViewHostEventEmitter> &)emitter;
@end
#endif // __cplusplus
NS_ASSUME_NONNULL_END

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