diff --git a/ios/RNSScreen.mm b/ios/RNSScreen.mm index c0801a3605..4f2863576a 100644 --- a/ios/RNSScreen.mm +++ b/ios/RNSScreen.mm @@ -1242,8 +1242,9 @@ - (void)overrideScrollViewBehaviorInFirstDescendantChainIfNeeded - (void)updateContentScrollViewEdgeEffectsIfExists { - [RNSScrollEdgeEffectApplicator applyToScrollView:[RNSScrollViewFinder findScrollViewInFirstDescendantChainFrom:self] - withProvider:self]; + [RNSScrollEdgeEffectApplicator + applyToScrollView:[RNSScrollViewFinder findContentScrollViewWithDelegatingToProvider:self] + withProvider:self]; } #pragma mark - RNSSafeAreaProviding diff --git a/ios/RNSScrollViewFinder.h b/ios/RNSScrollViewFinder.h deleted file mode 100644 index 0a716402b5..0000000000 --- a/ios/RNSScrollViewFinder.h +++ /dev/null @@ -1,13 +0,0 @@ -#import - -@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 diff --git a/ios/RNSScrollViewFinder.mm b/ios/RNSScrollViewFinder.mm deleted file mode 100644 index e44604a9b0..0000000000 --- a/ios/RNSScrollViewFinder.mm +++ /dev/null @@ -1,22 +0,0 @@ -#import "RNSScrollViewFinder.h" - -@implementation RNSScrollViewFinder - -+ (UIScrollView *)findScrollViewInFirstDescendantChainFrom:(UIView *)view -{ - UIView *currentView = view; - - while (currentView != nil) { - if ([currentView isKindOfClass:UIScrollView.class]) { - return static_cast(currentView); - } else if ([currentView.subviews count] > 0) { - currentView = currentView.subviews[0]; - } else { - break; - } - } - - return nil; -} - -@end diff --git a/ios/bottom-tabs/RNSBottomTabsHostComponentView.h b/ios/bottom-tabs/RNSBottomTabsHostComponentView.h index 11059f8bec..77d5ce8f37 100644 --- a/ios/bottom-tabs/RNSBottomTabsHostComponentView.h +++ b/ios/bottom-tabs/RNSBottomTabsHostComponentView.h @@ -4,6 +4,7 @@ #import "RNSEnums.h" #import "RNSReactBaseView.h" #import "RNSScreenContainer.h" +#import "RNSScrollViewFinder.h" #ifdef RCT_NEW_ARCH_ENABLED #import "RNSViewControllerInvalidating.h" @@ -27,6 +28,7 @@ NS_ASSUME_NONNULL_BEGIN */ @interface RNSBottomTabsHostComponentView : RNSReactBaseView < RNSScreenContainerDelegate, + RNSContentScrollViewProviding, #ifdef RCT_NEW_ARCH_ENABLED RNSViewControllerInvalidating #else diff --git a/ios/bottom-tabs/RNSBottomTabsHostComponentView.mm b/ios/bottom-tabs/RNSBottomTabsHostComponentView.mm index fe7d959117..0f3b8de38f 100644 --- a/ios/bottom-tabs/RNSBottomTabsHostComponentView.mm +++ b/ios/bottom-tabs/RNSBottomTabsHostComponentView.mm @@ -194,6 +194,25 @@ - (void)invalidate #endif +#pragma mark - RNSContentScrollViewProviding + +- (UIScrollView *)findContentScrollView +{ +#if !TARGET_OS_TV && !TARGET_OS_VISION + if (_controller.selectedViewController == _controller.moreNavigationController) { + // Logic for More Controller; user is shown the native list of tabs. + // This we want to keep as-is, we're not styling the ScrollView here, + // so let's pretend there isn't one + return nil; + } +#endif // check for build target != tvOS, visionOS + + // User selected regular tab with our BottomTabScreenComponentView. We start directly from it. + // This has the advantage of being able to continue searching even if the TabScreen + // hasn't been mounted yet (see mountChildComponentView(), _reactSubviews). + return [RNSScrollViewFinder findContentScrollViewWithDelegatingToProvider:_controller.selectedViewController.view]; +} + #pragma mark - React events - (nonnull RNSBottomTabsHostEventEmitter *)reactEventEmitter diff --git a/ios/helpers/scroll-view/RNSContentScrollViewProviding.h b/ios/helpers/scroll-view/RNSContentScrollViewProviding.h new file mode 100644 index 0000000000..d314d9a352 --- /dev/null +++ b/ios/helpers/scroll-view/RNSContentScrollViewProviding.h @@ -0,0 +1,9 @@ +@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 diff --git a/ios/helpers/scroll-view/RNSScrollViewFinder.h b/ios/helpers/scroll-view/RNSScrollViewFinder.h new file mode 100644 index 0000000000..64f5a9e79c --- /dev/null +++ b/ios/helpers/scroll-view/RNSScrollViewFinder.h @@ -0,0 +1,26 @@ +#import +#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 diff --git a/ios/helpers/scroll-view/RNSScrollViewFinder.mm b/ios/helpers/scroll-view/RNSScrollViewFinder.mm new file mode 100644 index 0000000000..2434c61295 --- /dev/null +++ b/ios/helpers/scroll-view/RNSScrollViewFinder.mm @@ -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(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(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>(currentView) findContentScrollView]; + } else if ([currentView.subviews count] > 0) { + currentView = currentView.subviews[0]; + } else { + break; + } + } + + return nil; +} + +@end diff --git a/ios/RNSScrollViewHelper.h b/ios/helpers/scroll-view/RNSScrollViewHelper.h similarity index 100% rename from ios/RNSScrollViewHelper.h rename to ios/helpers/scroll-view/RNSScrollViewHelper.h diff --git a/ios/RNSScrollViewHelper.mm b/ios/helpers/scroll-view/RNSScrollViewHelper.mm similarity index 100% rename from ios/RNSScrollViewHelper.mm rename to ios/helpers/scroll-view/RNSScrollViewHelper.mm