Skip to content

Commit 31ceb4a

Browse files
committed
feat(iOS, Stack, Tabs): introduce Container & ContainerItem nesting
Port the container-nesting protocol from Android to iOS so nesting information propagates between containers, and use it to resolve the content scroll view for special effects (scroll-to-top) across a nesting boundary. `RNSStackHostComponentView` and `RNSTabsHostComponentView` adopt the new `RNSContainer` protocol and register with the nearest parent `RNSContainerItem` on attach/detach to the window. `RNSStackScreenComponentView` and `RNSTabsScreenComponentView` adopt `RNSContainerItem`; content-scroll-view resolution now follows cached -> nested container -> descendant-chain heuristic. Shared state/logic lives in two composition holders mirroring the Android side: `RNSContainerItemSupport` (item side) and `RNSParentContainerItemRegistry` (container side). The parent walk prefers the React superview chain, matching the scroll-view marker ancestor lookup. `RNSTabsScreenViewController.resolveContentScrollView` now delegates to the screen's `findContentScrollView`, removing the duplicated edge/heuristic lookup.
1 parent 09f3f5f commit 31ceb4a

13 files changed

Lines changed: 306 additions & 13 deletions

ios/gamma/stack/host/RNSStackHostComponentView.mm

Lines changed: 22 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -7,8 +7,10 @@
77
#import <react/renderer/components/rnscreens/EventEmitters.h>
88
#import <react/renderer/components/rnscreens/Props.h>
99
#import <react/renderer/components/rnscreens/RCTComponentViewHelpers.h>
10+
#import "RNSContainer.h"
1011
#import "RNSDefines.h"
1112
#import "RNSLog.h"
13+
#import "RNSParentContainerItemRegistry.h"
1214

1315
#import "RNSStackNavigationController.h"
1416
#import "RNSStackOperationCoordinator.h"
@@ -17,13 +19,14 @@
1719

1820
namespace react = facebook::react;
1921

20-
@interface RNSStackHostComponentView () <RCTMountingTransactionObserving>
22+
@interface RNSStackHostComponentView () <RCTMountingTransactionObserving, RNSContainer>
2123
@end
2224

2325
@implementation RNSStackHostComponentView {
2426
RNSStackNavigationController *_Nonnull _stackNavigationController;
2527
RNSStackOperationCoordinator *_Nonnull _stackOperationCoordinator;
2628
NSMutableArray<RNSStackScreenComponentView *> *_Nonnull _renderedScreens;
29+
RNSParentContainerItemRegistry *_Nonnull _parentContainerRegistry;
2730
}
2831

2932
- (instancetype)initWithFrame:(CGRect)frame
@@ -39,6 +42,7 @@ - (void)initState
3942
_stackNavigationController = [RNSStackNavigationController new];
4043
_stackOperationCoordinator = [RNSStackOperationCoordinator new];
4144
_renderedScreens = [NSMutableArray new];
45+
_parentContainerRegistry = [RNSParentContainerItemRegistry new];
4246
}
4347

4448
#pragma mark - UIKit Callbacks
@@ -48,6 +52,23 @@ - (void)didMoveToWindow
4852
RNSLog(@"[RNScreens] StackHost [%ld] attached to window", self.tag);
4953
[self reactAddControllerToClosestParent:_stackNavigationController];
5054
[self setupViewConstraintsForController:_stackNavigationController];
55+
56+
if (self.window != nil) {
57+
[_parentContainerRegistry attachContainer:self];
58+
} else {
59+
[_parentContainerRegistry detachContainer:self];
60+
}
61+
}
62+
63+
#pragma mark - RNSContainer
64+
65+
- (nullable UIScrollView *)resolveCurrentContentScrollView
66+
{
67+
UIView *topScreenView = _stackNavigationController.topViewController.view;
68+
if (![topScreenView isKindOfClass:RNSStackScreenComponentView.class]) {
69+
return nil;
70+
}
71+
return [static_cast<RNSStackScreenComponentView *>(topScreenView) findContentScrollView];
5172
}
5273

5374
#pragma mark - Communication with StackScreen

ios/gamma/stack/screen/RNSStackScreenComponentView.h

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,6 @@
11
#pragma once
22

3+
#import "RNSContainerItem.h"
34
#import "RNSReactBaseView.h"
45
#import "RNSStackHeaderData.h"
56
#import "RNSStackScreenComponentEventEmitter.h"
@@ -15,7 +16,7 @@ typedef NS_ENUM(int, RNSStackScreenActivityMode) {
1516
RNSStackScreenActivityModeAttached = 1,
1617
};
1718

18-
@interface RNSStackScreenComponentView : RNSReactBaseView
19+
@interface RNSStackScreenComponentView : RNSReactBaseView <RNSContainerItem>
1920

2021
@property (nonatomic, weak, readwrite, nullable) RNSStackHostComponentView *stackHost;
2122
@property (nonatomic, strong, readonly, nonnull) RNSStackScreenController *controller;

ios/gamma/stack/screen/RNSStackScreenComponentView.mm

Lines changed: 34 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -7,7 +7,9 @@
77
#import <react/renderer/components/rnscreens/RCTComponentViewHelpers.h>
88
#import <rnscreens/RNSStackScreenComponentDescriptor.h>
99

10+
#import "RNSContainerItemSupport.h"
1011
#import "RNSConversions-Stack.h"
12+
#import "RNSScrollViewSeeking.h"
1113
#import "RNSStackHeaderConfigComponentView.h"
1214
#import "RNSStackHostComponentView.h"
1315
#import "RNSStackNavigationController.h"
@@ -18,14 +20,15 @@
1820

1921
namespace react = facebook::react;
2022

21-
@interface RNSStackScreenComponentView () <RCTMountingTransactionObserving>
23+
@interface RNSStackScreenComponentView () <RCTMountingTransactionObserving, RNSScrollViewSeeking>
2224
@end
2325

2426
#pragma mark - View implementation
2527

2628
@implementation RNSStackScreenComponentView {
2729
RNSStackScreenController *_Nonnull _controller;
2830
RNSStackScreenComponentEventEmitter *_Nonnull _reactEventEmitter;
31+
RNSContainerItemSupport *_Nonnull _containerItemSupport;
2932

3033
// Flags
3134
BOOL _hasUpdatedActivityMode;
@@ -46,6 +49,7 @@ - (void)initState
4649
[self setupController];
4750

4851
_reactEventEmitter = [RNSStackScreenComponentEventEmitter new];
52+
_containerItemSupport = [RNSContainerItemSupport new];
4953

5054
_hasUpdatedActivityMode = NO;
5155
_isNativelyDismissed = NO;
@@ -87,6 +91,35 @@ - (nonnull RNSStackScreenComponentEventEmitter *)reactEventEmitter
8791
return _reactEventEmitter;
8892
}
8993

94+
#pragma mark - RNSScrollViewSeeking
95+
96+
- (void)registerDescendantScrollView:(UIScrollView *)scrollView fromMarker:(RNSScrollViewMarkerComponentView *)marker
97+
{
98+
[_containerItemSupport registerScrollView:scrollView];
99+
}
100+
101+
#pragma mark - RNSContainerItem
102+
103+
- (void)registerNestedContainer:(id<RNSContainer>)container
104+
{
105+
[_containerItemSupport registerNestedContainer:container];
106+
}
107+
108+
- (void)unregisterNestedContainer:(id<RNSContainer>)container
109+
{
110+
[_containerItemSupport unregisterNestedContainer:container];
111+
}
112+
113+
- (nullable id<RNSContainer>)resolveNestedContainer
114+
{
115+
return [_containerItemSupport resolveNestedContainer];
116+
}
117+
118+
- (nullable UIScrollView *)findContentScrollView
119+
{
120+
return [_containerItemSupport findContentScrollViewForOwner:self];
121+
}
122+
90123
#pragma mark - RCTComponentViewProtocol
91124

92125
- (void)updateProps:(const facebook::react::Props::Shared &)props
Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,14 @@
1+
#pragma once
2+
3+
#import <UIKit/UIKit.h>
4+
5+
@protocol RNSContainer <NSObject>
6+
7+
/**
8+
* A container can host multiple items, and each item can have its own content scroll view. It is
9+
* down to the implementer to decide which scroll view to return here (if any) for the item that is
10+
* currently presented.
11+
*/
12+
- (nullable UIScrollView *)resolveCurrentContentScrollView;
13+
14+
@end
Lines changed: 28 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,28 @@
1+
#pragma once
2+
3+
#import <UIKit/UIKit.h>
4+
5+
#import "RNSContentScrollViewProviding.h"
6+
7+
@protocol RNSContainer;
8+
9+
@protocol RNSContainerItem <RNSContentScrollViewProviding>
10+
11+
#pragma mark - Nested Container handling
12+
13+
/**
14+
* A `RNSContainerItem` supports at most a single nested `RNSContainer`. Registering a second
15+
* container while one is already registered overwrites the previous one. This is an intentional
16+
* design invariant: a single item is expected to host at most one nested container.
17+
*/
18+
- (void)registerNestedContainer:(nonnull id<RNSContainer>)container;
19+
20+
- (void)unregisterNestedContainer:(nonnull id<RNSContainer>)container;
21+
22+
- (nullable id<RNSContainer>)resolveNestedContainer;
23+
24+
#pragma mark - Content Scroll View support
25+
26+
// `findContentScrollView` is inherited from `RNSContentScrollViewProviding`.
27+
28+
@end
Lines changed: 36 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,36 @@
1+
#pragma once
2+
3+
#import <UIKit/UIKit.h>
4+
5+
#import "RNSContainer.h"
6+
7+
NS_ASSUME_NONNULL_BEGIN
8+
9+
/**
10+
* Shared state & logic backing a `RNSContainerItem`. Holds the (optional) nested `RNSContainer`
11+
* and the cached content scroll view, and resolves the content scroll view on behalf of the
12+
* owning item.
13+
*
14+
* A `RNSContainerItem` supports at most a single nested `RNSContainer`; see
15+
* `RNSContainerItem.registerNestedContainer:`.
16+
*/
17+
@interface RNSContainerItemSupport : NSObject
18+
19+
- (void)registerScrollView:(UIScrollView *)scrollView;
20+
21+
- (void)registerNestedContainer:(id<RNSContainer>)container;
22+
23+
- (void)unregisterNestedContainer:(id<RNSContainer>)container;
24+
25+
- (nullable id<RNSContainer>)resolveNestedContainer;
26+
27+
/**
28+
* Resolves the content scroll view for the owning item: the cached one if present, otherwise the
29+
* one provided by the nested container, otherwise a heuristic search through `owner`'s first
30+
* descendant chain.
31+
*/
32+
- (nullable UIScrollView *)findContentScrollViewForOwner:(UIView *)owner;
33+
34+
@end
35+
36+
NS_ASSUME_NONNULL_END
Lines changed: 50 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,50 @@
1+
#import "RNSContainerItemSupport.h"
2+
3+
#import "RNSScrollViewFinder.h"
4+
5+
@implementation RNSContainerItemSupport {
6+
__weak id<RNSContainer> _nestedContainer;
7+
__weak UIScrollView *_contentScrollView;
8+
}
9+
10+
- (void)registerScrollView:(UIScrollView *)scrollView
11+
{
12+
_contentScrollView = scrollView;
13+
}
14+
15+
- (void)registerNestedContainer:(id<RNSContainer>)container
16+
{
17+
_nestedContainer = container;
18+
}
19+
20+
- (void)unregisterNestedContainer:(id<RNSContainer>)container
21+
{
22+
if (_nestedContainer == container) {
23+
_nestedContainer = nil;
24+
}
25+
}
26+
27+
- (nullable id<RNSContainer>)resolveNestedContainer
28+
{
29+
return _nestedContainer;
30+
}
31+
32+
- (nullable UIScrollView *)findContentScrollViewForOwner:(UIView *)owner
33+
{
34+
// Cached one
35+
UIScrollView *cached = _contentScrollView;
36+
if (cached != nil) {
37+
return cached;
38+
}
39+
40+
// Provided by nested container
41+
UIScrollView *fromNestedContainer = [[self resolveNestedContainer] resolveCurrentContentScrollView];
42+
if (fromNestedContainer != nil) {
43+
return fromNestedContainer;
44+
}
45+
46+
// Heuristic
47+
return [RNSScrollViewFinder findScrollViewInFirstDescendantChainFrom:owner];
48+
}
49+
50+
@end
Lines changed: 22 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,22 @@
1+
#pragma once
2+
3+
#import <UIKit/UIKit.h>
4+
5+
#import "RNSContainer.h"
6+
7+
NS_ASSUME_NONNULL_BEGIN
8+
9+
/**
10+
* Shared state & logic backing a `RNSContainer`'s registration with the nearest parent
11+
* `RNSContainerItem` in the view hierarchy. Call `attachContainer:` when the container attaches to
12+
* the window and `detachContainer:` when it detaches.
13+
*/
14+
@interface RNSParentContainerItemRegistry : NSObject
15+
16+
- (void)attachContainer:(UIView<RNSContainer> *)container;
17+
18+
- (void)detachContainer:(id<RNSContainer>)container;
19+
20+
@end
21+
22+
NS_ASSUME_NONNULL_END
Lines changed: 47 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,47 @@
1+
#import "RNSParentContainerItemRegistry.h"
2+
3+
#import <React/UIView+React.h>
4+
5+
#import "RNSContainerItem.h"
6+
7+
@implementation RNSParentContainerItemRegistry {
8+
__weak id<RNSContainerItem> _parentContainerItem;
9+
}
10+
11+
- (void)attachContainer:(UIView<RNSContainer> *)container
12+
{
13+
id<RNSContainerItem> parentContainerItem = [self findParentContainerItemFrom:container];
14+
if (parentContainerItem != nil) {
15+
[parentContainerItem registerNestedContainer:container];
16+
}
17+
_parentContainerItem = parentContainerItem;
18+
}
19+
20+
- (void)detachContainer:(id<RNSContainer>)container
21+
{
22+
[_parentContainerItem unregisterNestedContainer:container];
23+
_parentContainerItem = nil;
24+
}
25+
26+
/**
27+
* Walks up the view hierarchy starting above `view`, returning the nearest ancestor conforming to
28+
* `RNSContainerItem`. Prefers the React superview chain, falling back to the UIKit one. Mirrors the
29+
* ancestor walk in `RNSScrollViewMarkerComponentView.findFirstSeekingAncestor`.
30+
*/
31+
- (nullable id<RNSContainerItem>)findParentContainerItemFrom:(UIView *)view
32+
{
33+
UIView *superview = view.superview;
34+
while (superview != nil) {
35+
if ([superview respondsToSelector:@selector(registerNestedContainer:)]) {
36+
return static_cast<id<RNSContainerItem>>(superview);
37+
}
38+
if ([superview respondsToSelector:@selector(reactSuperview)]) {
39+
superview = [superview reactSuperview];
40+
} else {
41+
superview = superview.superview;
42+
}
43+
}
44+
return nil;
45+
}
46+
47+
@end

0 commit comments

Comments
 (0)