first commit

This commit is contained in:
2026-03-10 16:18:05 +00:00
commit 11f9c069b5
31635 changed files with 3187747 additions and 0 deletions

View File

@@ -0,0 +1,9 @@
#pragma once
@protocol RNSViewInteractionAware
- (void)rnscreens_disableInteractions;
- (void)rnscreens_enableInteractions;
@end

View File

@@ -0,0 +1,19 @@
#pragma once
@interface RNSViewInteractionManager : NSObject
- (instancetype)init;
/**
* Given a view, traverse its ancestors hierarchy and find a view that supports RNSViewInteractionAware protocol
* and can disable interactions for the time of screen transition. Make sure that at most one view is disabled at any
* time, re-enabling interactions on previously affected views when necessary.
*/
- (void)disableInteractionsForSubtreeWith:(UIView *)view;
/**
* Re-enable interactions on the view that had them previously disabled.
*/
- (void)enableInteractionsForLastSubtree;
@end

View File

@@ -0,0 +1,55 @@
#import "RNSViewInteractionManager.h"
#import "RNSBottomTabsScreenComponentView.h"
#import "RNSViewInteractionAware.h"
@implementation RNSViewInteractionManager {
__weak UIView *lastRootWithInteractionsDisabled;
}
- (instancetype)init
{
if (self = [super init]) {
lastRootWithInteractionsDisabled = nil;
}
return self;
}
- (void)disableInteractionsForSubtreeWith:(UIView *)view
{
UIView *current = view;
while (current && ![current isKindOfClass:UIWindow.class] &&
![current respondsToSelector:@selector(rnscreens_disableInteractions)]) {
current = current.superview;
}
if (current) {
if (lastRootWithInteractionsDisabled && lastRootWithInteractionsDisabled != current) {
// When one view already has interactions disabled, and we request a different view,
// we need to restore the first one
[self enableInteractionsForLastSubtree];
}
if ([current respondsToSelector:@selector(rnscreens_disableInteractions)]) {
[static_cast<id<RNSViewInteractionAware>>(current) rnscreens_disableInteractions];
} else {
current.userInteractionEnabled = NO;
}
lastRootWithInteractionsDisabled = current;
}
}
- (void)enableInteractionsForLastSubtree
{
if (lastRootWithInteractionsDisabled) {
if ([lastRootWithInteractionsDisabled respondsToSelector:@selector(rnscreens_enableInteractions)]) {
[static_cast<id<RNSViewInteractionAware>>(lastRootWithInteractionsDisabled) rnscreens_enableInteractions];
} else {
lastRootWithInteractionsDisabled.userInteractionEnabled = YES;
}
lastRootWithInteractionsDisabled = nil;
}
}
@end

View File

@@ -0,0 +1,28 @@
#pragma once
#import <React/RCTImageLoader.h>
#import <React/RCTImageSource.h>
@interface RNSImageLoadingHelper : NSObject
/**
* Should be called from UI thread only.
* If done so, the method **tries** to load the image synchronously from image source represented in JSON via
* `NSDictionary`. There is no guarantee, because in release mode we rely on `RCTImageLoader` implementation details. No
* matter how the image is loaded, `completionBlock` is executed on main queue.
*/
+ (void)loadImageSyncIfPossibleFromJsonSource:(nonnull NSDictionary *)jsonImageSource
withImageLoader:(nonnull RCTImageLoader *)imageLoader
asTemplate:(BOOL)isTemplate
completionBlock:(void (^_Nonnull)(UIImage *_Nullable image))imageLoadingCompletionBlock;
/**
* Loads image from `RCTImageSource`, relies on `RCTImageLoader` implementation.
* `completionBlock` is executed on main queue.
*/
+ (void)loadImageFromSource:(nonnull RCTImageSource *)imageSource
withImageLoader:(nonnull RCTImageLoader *)imageLoader
asTemplate:(BOOL)isTemplate
completionBlock:(void (^_Nonnull)(UIImage *_Nullable image))imageLoadingCompletionBlock;
@end

View File

@@ -0,0 +1,71 @@
#import "RNSImageLoadingHelper.h"
#import "RCTImageSource+AccessHiddenMembers.h"
@implementation RNSImageLoadingHelper
+ (void)loadImageSyncIfPossibleFromJsonSource:(nonnull NSDictionary *)jsonImageSource
withImageLoader:(nonnull RCTImageLoader *)imageLoader
asTemplate:(BOOL)isTemplate
completionBlock:(void (^_Nonnull)(UIImage *_Nullable image))imageLoadingCompletionBlock
{
RCTAssert(RCTIsMainQueue(), @"[RNScreens] Expected to run on main queue");
RCTImageSource *imageSource = [RCTConvert RCTImageSource:jsonImageSource];
RCTAssert(imageSource != nil, @"[RNScreens] Expected nonnil image source");
#if !defined(NDEBUG) // We're in debug mode here
if (imageSource.packagerAsset) {
// We use `+ [RCTConvert UIImage:]` only in debug mode, because it is deprecated, however
// we haven't found different way to load image synchronously in debug other than
// writing the code manually.
UIImage *image = [RCTConvert UIImage:jsonImageSource];
imageLoadingCompletionBlock([RNSImageLoadingHelper handleRenderingModeForImage:image isTemplate:isTemplate]);
} else
#endif // !defined(NDEBUG)
{
[self loadImageFromSource:imageSource
withImageLoader:imageLoader
asTemplate:isTemplate
completionBlock:imageLoadingCompletionBlock];
}
}
+ (void)loadImageFromSource:(nonnull RCTImageSource *)imageSource
withImageLoader:(nonnull RCTImageLoader *)imageLoader
asTemplate:(BOOL)isTemplate
completionBlock:(void (^_Nonnull)(UIImage *_Nullable 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:RCTResizeModeCenter
progressBlock:^(int64_t progress, int64_t total) {
}
partialLoadBlock:^(UIImage *_Nonnull image) {
}
completionBlock:^(NSError *_Nullable error, UIImage *_Nullable image) {
if (RCTIsMainQueue()) {
imageLoadingCompletionBlock([RNSImageLoadingHelper handleRenderingModeForImage:image isTemplate:isTemplate]);
} else {
dispatch_async(dispatch_get_main_queue(), ^{
imageLoadingCompletionBlock([RNSImageLoadingHelper handleRenderingModeForImage:image
isTemplate:isTemplate]);
});
}
}];
}
+ (nullable UIImage *)handleRenderingModeForImage:(nullable UIImage *)image isTemplate:(BOOL)isTemplate
{
if (isTemplate) {
return [image imageWithRenderingMode:UIImageRenderingModeAlwaysTemplate];
} else {
return [image imageWithRenderingMode:UIImageRenderingModeAlwaysOriginal];
}
}
@end

View File

@@ -0,0 +1,11 @@
#pragma once
@protocol RNSContentScrollViewProviding
/**
* Finds content ScrollView within provider's hierarchy. The content ScrollView serves as a main interaction on the
* given screen. Implementations may use `RNSScrollViewFinder` to continue the search however they see fit.
*/
- (nullable UIScrollView *)findContentScrollView;
@end

View File

@@ -0,0 +1,14 @@
#pragma once
#import "RNSEnums.h"
@protocol RNSScrollEdgeEffectProviding
- (RNSScrollEdgeEffect)bottomScrollEdgeEffect;
- (RNSScrollEdgeEffect)leftScrollEdgeEffect;
- (RNSScrollEdgeEffect)rightScrollEdgeEffect;
- (RNSScrollEdgeEffect)topScrollEdgeEffect;
@end
@interface RNSScrollEdgeEffectApplicator : NSObject
+ (void)applyToScrollView:(UIScrollView *)scrollView withProvider:(id<RNSScrollEdgeEffectProviding>)provider;
@end

View File

@@ -0,0 +1,52 @@
#import "RNSScrollEdgeEffectApplicator.h"
#import <React/RCTLog.h>
#import "RNSDefines.h"
#import "RNSEnums.h"
@implementation RNSScrollEdgeEffectApplicator
+ (void)applyToScrollView:(UIScrollView *)scrollView withProvider:(id<RNSScrollEdgeEffectProviding>)provider
{
#if RNS_IPHONE_OS_VERSION_AVAILABLE(26_0)
if (@available(iOS 26, *)) {
[RNSScrollEdgeEffectApplicator configureEffect:scrollView.bottomEdgeEffect
withEnum:[provider bottomScrollEdgeEffect]];
[RNSScrollEdgeEffectApplicator configureEffect:scrollView.leftEdgeEffect withEnum:[provider leftScrollEdgeEffect]];
[RNSScrollEdgeEffectApplicator configureEffect:scrollView.rightEdgeEffect
withEnum:[provider rightScrollEdgeEffect]];
[RNSScrollEdgeEffectApplicator configureEffect:scrollView.topEdgeEffect withEnum:[provider topScrollEdgeEffect]];
}
#endif
}
#if RNS_IPHONE_OS_VERSION_AVAILABLE(26_0)
+ (void)configureEffect:(UIScrollEdgeEffect *)edgeEffect
withEnum:(RNSScrollEdgeEffect)effectEnum API_AVAILABLE(ios(26.0))
{
if (@available(iOS 26, *)) {
switch (effectEnum) {
case RNSScrollEdgeEffectAutomatic:
edgeEffect.hidden = false;
edgeEffect.style = UIScrollEdgeEffectStyle.automaticStyle;
break;
case RNSScrollEdgeEffectHard:
edgeEffect.hidden = false;
edgeEffect.style = UIScrollEdgeEffectStyle.hardStyle;
break;
case RNSScrollEdgeEffectSoft:
edgeEffect.hidden = false;
edgeEffect.style = UIScrollEdgeEffectStyle.softStyle;
break;
case RNSScrollEdgeEffectHidden:
edgeEffect.hidden = true;
edgeEffect.style = UIScrollEdgeEffectStyle.automaticStyle;
break;
default:
RCTLogError(@"[RNScreens] unsupported edge effect");
break;
}
}
}
#endif
@end

View File

@@ -0,0 +1,28 @@
#pragma once
#import <UIKit/UIKit.h>
#import "RNSContentScrollViewProviding.h"
@interface RNSScrollViewFinder : NSObject
/**
* Searches for content ScrollView 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.
*
* When `view == nil`, it should also return `nil`.
*/
+ (nullable UIScrollView *)findScrollViewInFirstDescendantChainFrom:(nullable UIView *)view;
/**
* Looks for UIScrollView in a similar way to `findScrollViewInFirstDescendantChainFrom`, until it finds
* `RNSContentScrollViewProviding`. Then, it delegates the task to the provider, and returns the results. This can
* overcome the problems of subviews' children not being mounted yet, or ScrollView being mounted at index different
* than 0.
*
* When `view == nil`, it should also return `nil`.
*/
+ (nullable UIScrollView *)findContentScrollViewWithDelegatingToProvider:(nullable UIView *)view;
@end

View File

@@ -0,0 +1,44 @@
#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;
}
+ (nullable UIScrollView *)findContentScrollViewWithDelegatingToProvider:(nullable UIView *)view
{
UIView *currentView = view;
while (currentView != nil) {
if ([currentView isKindOfClass:UIScrollView.class]) {
return static_cast<UIScrollView *>(currentView);
} else if ([currentView respondsToSelector:@selector(findContentScrollView)]) {
// When traversing the hierarchy, we don't check for conformance to protocol,
// but whether the view responds to `RNSContentScrollViewProviding.findContentScrollView`.
// This doesn't place locks and is faster.
return [static_cast<id<RNSContentScrollViewProviding>>(currentView) findContentScrollView];
} else if ([currentView.subviews count] > 0) {
currentView = currentView.subviews[0];
} else {
break;
}
}
return nil;
}
@end

View File

@@ -0,0 +1,12 @@
#pragma once
#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

View File

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