Skip to content

Commit b3f397f

Browse files
lunaleapsfacebook-github-bot
authored andcommitted
VirtualViewExperimental on iOS (#52852)
Summary: Pull Request resolved: #52852 Changelog: [Internal] - Implementation of ScrollView-managed VirtualViews for iOS In previous diffs we've introduced a "VirtualViewExperimental" which is a clone of VirtualView. This diff updates the experimental version to move interection logic (whether something is visible, in prerender-space, etc.) to the ScrollView so there are less listeners. We now use 1 scroll listener vs. N Reviewed By: philIip Differential Revision: D78825701 fbshipit-source-id: d515e3cb2dae53d779b5d3f4c317a2c7a6b25857
1 parent 5517c04 commit b3f397f

8 files changed

Lines changed: 348 additions & 129 deletions

packages/react-native/React/Fabric/Mounting/ComponentViews/ScrollView/RCTScrollViewComponentView.h

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -13,6 +13,8 @@
1313
#import <React/RCTScrollableProtocol.h>
1414
#import <React/RCTViewComponentView.h>
1515

16+
#import "RCTVirtualViewContainerProtocol.h"
17+
1618
NS_ASSUME_NONNULL_BEGIN
1719

1820
/*
@@ -23,7 +25,8 @@ NS_ASSUME_NONNULL_BEGIN
2325
* keyboard-avoiding functionality and so on. All that complexity must be implemented inside those components in order
2426
* to keep the complexity of this component manageable.
2527
*/
26-
@interface RCTScrollViewComponentView : RCTViewComponentView <RCTMountingTransactionObserving>
28+
@interface RCTScrollViewComponentView
29+
: RCTViewComponentView <RCTMountingTransactionObserving, RCTVirtualViewContainerProtocol>
2730

2831
/*
2932
* Finds and returns the closet RCTScrollViewComponentView component to the given view

packages/react-native/React/Fabric/Mounting/ComponentViews/ScrollView/RCTScrollViewComponentView.mm

Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -24,6 +24,7 @@
2424
#import "RCTCustomPullToRefreshViewProtocol.h"
2525
#import "RCTEnhancedScrollView.h"
2626
#import "RCTFabricComponentsPlugins.h"
27+
#import "RCTVirtualViewContainerState.h"
2728

2829
using namespace facebook::react;
2930

@@ -117,6 +118,8 @@ @implementation RCTScrollViewComponentView {
117118
// It is not restored to the default value in prepareForRecycle.
118119
// Once an accessibility API is used, view culling will be disabled for the entire session.
119120
BOOL _isAccessibilityAPIUsed;
121+
122+
RCTVirtualViewContainerState *_virtualViewContainerState;
120123
}
121124

122125
+ (RCTScrollViewComponentView *_Nullable)findScrollViewComponentViewForView:(UIView *)view
@@ -678,6 +681,7 @@ - (void)prepareForRecycle
678681
_contentView = nil;
679682
_prevFirstVisibleFrame = CGRectZero;
680683
_firstVisibleView = nil;
684+
_virtualViewContainerState = nil;
681685
}
682686

683687
#pragma mark - UIScrollViewDelegate
@@ -1090,6 +1094,16 @@ - (void)_adjustForMaintainVisibleContentPosition
10901094
}
10911095
}
10921096

1097+
#pragma mark - RCTVirtualViewContainerProtocol
1098+
1099+
- (RCTVirtualViewContainerState *)virtualViewContainerState
1100+
{
1101+
if (!_virtualViewContainerState) {
1102+
_virtualViewContainerState = [[RCTVirtualViewContainerState alloc] initWithScrollView:self];
1103+
}
1104+
return _virtualViewContainerState;
1105+
}
1106+
10931107
@end
10941108

10951109
Class<RCTComponentViewProtocol> RCTScrollViewCls(void)
Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,14 @@
1+
/*
2+
* Copyright (c) Meta Platforms, Inc. and affiliates.
3+
*
4+
* This source code is licensed under the MIT license found in the
5+
* LICENSE file in the root directory of this source tree.
6+
*/
7+
8+
@class RCTVirtualViewContainerState;
9+
10+
@protocol RCTVirtualViewContainerProtocol
11+
12+
- (RCTVirtualViewContainerState *)virtualViewContainerState;
13+
14+
@end
Lines changed: 25 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,25 @@
1+
/*
2+
* Copyright (c) Meta Platforms, Inc. and affiliates.
3+
*
4+
* This source code is licensed under the MIT license found in the
5+
* LICENSE file in the root directory of this source tree.
6+
*/
7+
8+
#import <Foundation/Foundation.h>
9+
10+
#import "RCTVirtualViewProtocol.h"
11+
12+
@class RCTScrollViewComponentView;
13+
14+
NS_ASSUME_NONNULL_BEGIN
15+
@interface RCTVirtualViewContainerState : NSObject
16+
17+
- (instancetype)init NS_UNAVAILABLE;
18+
- (instancetype)new NS_UNAVAILABLE;
19+
- (instancetype)initWithScrollView:(RCTScrollViewComponentView *)scrollView NS_DESIGNATED_INITIALIZER;
20+
21+
- (void)onChange:(id<RCTVirtualViewProtocol>)virtualView;
22+
- (void)remove:(id<RCTVirtualViewProtocol>)virtualView;
23+
@end
24+
25+
NS_ASSUME_NONNULL_END
Lines changed: 193 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,193 @@
1+
/*
2+
* Copyright (c) Meta Platforms, Inc. and affiliates.
3+
*
4+
* This source code is licensed under the MIT license found in the
5+
* LICENSE file in the root directory of this source tree.
6+
*/
7+
8+
#import <React/RCTLog.h>
9+
#import <React/RCTScrollViewComponentView.h>
10+
#import <React/RCTVirtualViewMode.h>
11+
#import <UIKit/UIKit.h>
12+
#import <react/featureflags/ReactNativeFeatureFlags.h>
13+
14+
#import "RCTVirtualViewContainerState.h"
15+
16+
using namespace facebook;
17+
using namespace facebook::react;
18+
19+
#if RCT_DEBUG
20+
static void debugLog(NSString *msg, ...)
21+
{
22+
auto debugEnabled = ReactNativeFeatureFlags::enableVirtualViewDebugFeatures();
23+
if (!debugEnabled) {
24+
return;
25+
}
26+
27+
va_list args;
28+
va_start(args, msg);
29+
NSString *msgString = [[NSString alloc] initWithFormat:msg arguments:args];
30+
RCTLogInfo(@"%@", msgString);
31+
va_end(args); // Don't forget to call va_end to clean up
32+
}
33+
34+
#endif
35+
36+
/**
37+
* Checks whether one CGRect overlaps with another CGRect.
38+
*
39+
* This is different from CGRectIntersectsRect because a CGRect representing
40+
* a line or a point is considered to overlap with another CGRect if the line
41+
* or point is within the rect bounds. However, two CGRects are not considered
42+
* to overlap if they only share a boundary.
43+
*/
44+
static BOOL CGRectOverlaps(CGRect rect1, CGRect rect2)
45+
{
46+
CGFloat minY1 = CGRectGetMinY(rect1);
47+
CGFloat maxY1 = CGRectGetMaxY(rect1);
48+
CGFloat minY2 = CGRectGetMinY(rect2);
49+
CGFloat maxY2 = CGRectGetMaxY(rect2);
50+
if (minY1 >= maxY2 || minY2 >= maxY1) {
51+
// No overlap on the y-axis.
52+
return NO;
53+
}
54+
CGFloat minX1 = CGRectGetMinX(rect1);
55+
CGFloat maxX1 = CGRectGetMaxX(rect1);
56+
CGFloat minX2 = CGRectGetMinX(rect2);
57+
CGFloat maxX2 = CGRectGetMaxX(rect2);
58+
if (minX1 >= maxX2 || minX2 >= maxX1) {
59+
// No overlap on the x-axis.
60+
return NO;
61+
}
62+
return YES;
63+
}
64+
65+
@interface RCTVirtualViewContainerState () <UIScrollViewDelegate>
66+
@end
67+
68+
@interface RCTVirtualViewContainerState () {
69+
NSMutableSet<id<RCTVirtualViewProtocol>> *_virtualViews;
70+
CGRect _emptyRect;
71+
CGRect _prerenderRect;
72+
__weak RCTScrollViewComponentView *_scrollViewComponentView;
73+
CGFloat _prerenderRatio;
74+
}
75+
@end
76+
77+
@implementation RCTVirtualViewContainerState
78+
79+
- (instancetype)initWithScrollView:(RCTScrollViewComponentView *)scrollView
80+
{
81+
self = [super init];
82+
if (self != nil) {
83+
_virtualViews = [NSMutableSet set];
84+
_emptyRect = CGRectZero;
85+
_prerenderRect = CGRectZero;
86+
_scrollViewComponentView = scrollView;
87+
_prerenderRatio = ReactNativeFeatureFlags::virtualViewPrerenderRatio();
88+
[_scrollViewComponentView addScrollListener:self];
89+
90+
#if RCT_DEBUG
91+
debugLog(@"initWithScrollView");
92+
#endif
93+
}
94+
return self;
95+
}
96+
97+
- (void)dealloc
98+
{
99+
#if RCT_DEBUG
100+
debugLog(@"dealloc");
101+
#endif
102+
if (_scrollViewComponentView != nil) {
103+
[_scrollViewComponentView removeScrollListener:self];
104+
_scrollViewComponentView = nil;
105+
}
106+
[_virtualViews removeAllObjects];
107+
}
108+
109+
#pragma mark - Public API
110+
111+
- (void)onChange:(id<RCTVirtualViewProtocol>)virtualView
112+
{
113+
if (![_virtualViews containsObject:virtualView]) {
114+
[_virtualViews addObject:virtualView];
115+
#if RCT_DEBUG
116+
debugLog(@"Add virtualViewID=%@", virtualView.virtualViewID);
117+
#endif
118+
119+
} else {
120+
#if RCT_DEBUG
121+
debugLog(@"Update virtualViewID=%@", virtualView.virtualViewID);
122+
#endif
123+
}
124+
[self _updateModes:virtualView];
125+
}
126+
127+
- (void)remove:(id<RCTVirtualViewProtocol>)virtualView
128+
{
129+
if (![_virtualViews containsObject:virtualView]) {
130+
RCTLogError(@"Attempting to remove non-existent VirtualView: %@", virtualView.virtualViewID);
131+
}
132+
133+
[_virtualViews removeObject:virtualView];
134+
135+
#if RCT_DEBUG
136+
debugLog(@"Remove virtualViewID=%@", virtualView.virtualViewID);
137+
#endif
138+
}
139+
140+
#pragma mark - Private Helpers
141+
142+
- (void)_updateModes:(id<RCTVirtualViewProtocol>)virtualView
143+
{
144+
auto scrollView = _scrollViewComponentView.scrollView;
145+
CGRect visibleRect = CGRectMake(
146+
scrollView.contentOffset.x,
147+
scrollView.contentOffset.y,
148+
scrollView.frame.size.width,
149+
scrollView.frame.size.height);
150+
151+
_prerenderRect = visibleRect;
152+
_prerenderRect = CGRectInset(
153+
_prerenderRect, -_prerenderRect.size.width * _prerenderRatio, -_prerenderRect.size.height * _prerenderRatio);
154+
155+
NSArray<id<RCTVirtualViewProtocol>> *virtualViewsIt =
156+
(virtualView != nullptr) ? @[ virtualView ] : [_virtualViews allObjects];
157+
158+
for (id<RCTVirtualViewProtocol> vv = nullptr in virtualViewsIt) {
159+
CGRect rect = [vv containerRelativeRect:scrollView];
160+
161+
RCTVirtualViewMode mode = RCTVirtualViewModeHidden;
162+
CGRect thresholdRect = _emptyRect;
163+
164+
if (CGRectIsEmpty(rect)) {
165+
mode = RCTVirtualViewModeHidden;
166+
thresholdRect = _emptyRect;
167+
} else if (CGRectOverlaps(rect, visibleRect)) {
168+
thresholdRect = visibleRect;
169+
mode = RCTVirtualViewModeVisible;
170+
} else if (CGRectOverlaps(rect, _prerenderRect)) {
171+
mode = RCTVirtualViewModePrerender;
172+
thresholdRect = _prerenderRect;
173+
}
174+
175+
#if RCT_DEBUG
176+
debugLog(
177+
@"UpdateModes virtualView=%@ mode=%ld rect=%@ thresholdRect=%@",
178+
vv.virtualViewID,
179+
(long)mode,
180+
NSStringFromCGRect(rect),
181+
NSStringFromCGRect(thresholdRect));
182+
#endif
183+
[vv onModeChange:mode targetRect:rect thresholdRect:thresholdRect];
184+
}
185+
}
186+
187+
#pragma mark - UIScrollViewDelegate
188+
189+
- (void)scrollViewDidScroll:(UIScrollView *)scrollView
190+
{
191+
[self _updateModes:nil];
192+
}
193+
@end
Lines changed: 20 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,20 @@
1+
/*
2+
* Copyright (c) Meta Platforms, Inc. and affiliates.
3+
*
4+
* This source code is licensed under the MIT license found in the
5+
* LICENSE file in the root directory of this source tree.
6+
*/
7+
8+
#import <React/RCTVirtualViewMode.h>
9+
#import <UIKit/UIKit.h>
10+
11+
NS_ASSUME_NONNULL_BEGIN
12+
13+
@protocol RCTVirtualViewProtocol <NSObject>
14+
15+
- (NSString *)virtualViewID;
16+
- (CGRect)containerRelativeRect:(UIView *)view;
17+
- (void)onModeChange:(RCTVirtualViewMode)newMode targetRect:(CGRect)targetRect thresholdRect:(CGRect)thresholdRect;
18+
@end
19+
20+
NS_ASSUME_NONNULL_END

packages/react-native/React/Fabric/Mounting/ComponentViews/VirtualViewExperimental/RCTVirtualViewExperimentalComponentView.h

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -8,10 +8,11 @@
88
#import <UIKit/UIKit.h>
99

1010
#import <React/RCTViewComponentView.h>
11+
#import <React/RCTVirtualViewProtocol.h>
1112

1213
NS_ASSUME_NONNULL_BEGIN
1314

14-
@interface RCTVirtualViewExperimentalComponentView : RCTViewComponentView
15+
@interface RCTVirtualViewExperimentalComponentView : RCTViewComponentView <RCTVirtualViewProtocol>
1516

1617
+ (instancetype)new NS_UNAVAILABLE;
1718
- (instancetype)init NS_UNAVAILABLE;

0 commit comments

Comments
 (0)