Skip to content

Commit 14e5b15

Browse files
lunaleapsfacebook-github-bot
authored andcommitted
Move VirtualView on iOS to react-native-github
Summary: Changelog: [Internal] - Move experimental VirtualView iOS native component to react-native-github Differential Revision: D76473664
1 parent 5894fe9 commit 14e5b15

File tree

2 files changed

+314
-0
lines changed

2 files changed

+314
-0
lines changed
Lines changed: 24 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,24 @@
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 <UIKit/UIKit.h>
9+
10+
#import <React/RCTViewComponentView.h>
11+
12+
NS_ASSUME_NONNULL_BEGIN
13+
14+
@interface RCTVirtualViewComponentView : RCTViewComponentView
15+
16+
+ (instancetype)new NS_UNAVAILABLE;
17+
- (instancetype)init NS_UNAVAILABLE;
18+
- (instancetype)initWithCoder:(NSCoder *)coder NS_UNAVAILABLE;
19+
20+
- (instancetype)initWithFrame:(CGRect)frame NS_DESIGNATED_INITIALIZER;
21+
22+
@end
23+
24+
NS_ASSUME_NONNULL_END
Lines changed: 290 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,290 @@
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 "RCTVirtualViewComponentView.h"
9+
10+
#import <React/RCTAssert.h>
11+
#import <React/RCTConversions.h>
12+
#import <React/RCTScrollViewComponentView.h>
13+
#import <React/RCTScrollableProtocol.h>
14+
#import <React/UIView+React.h>
15+
#import <jsi/jsi.h>
16+
#import <react/featureflags/ReactNativeFeatureFlags.h>
17+
#import <react/renderer/components/virtualview/VirtualViewComponentDescriptor.h>
18+
19+
#import "Plugins.h"
20+
21+
using namespace facebook;
22+
using namespace facebook::react;
23+
24+
typedef NS_ENUM(NSInteger, RCTVirtualViewMode) {
25+
RCTVirtualViewModeVisible = 0,
26+
RCTVirtualViewModePrerender = 1,
27+
RCTVirtualViewModeHidden = 2,
28+
};
29+
30+
/**
31+
* Checks whether one CGRect overlaps with another CGRect.
32+
*
33+
* This is different from CGRectIntersectsRect because a CGRect representing
34+
* a line or a point is considered to overlap with another CGRect if the line
35+
* or point is within the rect bounds. However, two CGRects are not considered
36+
* to overlap if they only share a boundary.
37+
*/
38+
static BOOL CGRectOverlaps(CGRect rect1, CGRect rect2)
39+
{
40+
CGFloat minY1 = CGRectGetMinY(rect1);
41+
CGFloat maxY1 = CGRectGetMaxY(rect1);
42+
CGFloat minY2 = CGRectGetMinY(rect2);
43+
CGFloat maxY2 = CGRectGetMaxY(rect2);
44+
if (minY1 >= maxY2 || minY2 >= maxY1) {
45+
// No overlap on the y-axis.
46+
return NO;
47+
}
48+
CGFloat minX1 = CGRectGetMinX(rect1);
49+
CGFloat maxX1 = CGRectGetMaxX(rect1);
50+
CGFloat minX2 = CGRectGetMinX(rect2);
51+
CGFloat maxX2 = CGRectGetMaxX(rect2);
52+
if (minX1 >= maxX2 || minX2 >= maxX1) {
53+
// No overlap on the x-axis.
54+
return NO;
55+
}
56+
return YES;
57+
}
58+
59+
@interface RCTVirtualViewComponentView () <UIScrollViewDelegate>
60+
@end
61+
62+
@implementation RCTVirtualViewComponentView {
63+
RCTScrollViewComponentView *_lastParentScrollViewComponentView;
64+
std::optional<enum RCTVirtualViewMode> _mode;
65+
std::optional<CGRect> _targetRect;
66+
}
67+
68+
- (instancetype)initWithFrame:(CGRect)frame
69+
{
70+
if (self = [super initWithFrame:frame]) {
71+
_props = VirtualViewShadowNode::defaultSharedProps();
72+
}
73+
74+
return self;
75+
}
76+
77+
- (void)updateProps:(const Props::Shared &)props oldProps:(const Props::Shared &)oldProps
78+
{
79+
if (!_mode.has_value()) {
80+
const auto &newViewProps = static_cast<const VirtualViewProps &>(*props);
81+
_mode = newViewProps.initialHidden ? RCTVirtualViewModeHidden : RCTVirtualViewModeVisible;
82+
}
83+
84+
[super updateProps:props oldProps:oldProps];
85+
}
86+
87+
- (RCTScrollViewComponentView *)getParentScrollViewComponentView
88+
{
89+
UIView *view = self.superview;
90+
while (view != nil) {
91+
if ([view isKindOfClass:[RCTScrollViewComponentView class]]) {
92+
return (RCTScrollViewComponentView *)view;
93+
}
94+
view = view.superview;
95+
}
96+
return nil;
97+
}
98+
99+
- (void)prepareForRecycle
100+
{
101+
[super prepareForRecycle];
102+
103+
// No need to remove the scroll listener here since the view is always removed from window before being recycled and
104+
// we do that in didMoveToWindow, which gets called when the view is removed from window.
105+
RCTAssert(
106+
_lastParentScrollViewComponentView == nil,
107+
@"_lastParentScrollViewComponentView should already have been cleared in didMoveToWindow.");
108+
109+
_mode.reset();
110+
_targetRect.reset();
111+
}
112+
113+
// Handles case when sibling changes size.
114+
// TODO(T202601695): This doesn't yet handle the case of elements in the ScrollView outside a VirtualColumn changing
115+
// size.
116+
- (void)updateLayoutMetrics:(const LayoutMetrics &)layoutMetrics
117+
oldLayoutMetrics:(const LayoutMetrics &)oldLayoutMetrics
118+
{
119+
[super updateLayoutMetrics:layoutMetrics oldLayoutMetrics:_layoutMetrics];
120+
121+
[self dispatchOnModeChangeIfNeeded:YES];
122+
}
123+
124+
- (void)didMoveToWindow
125+
{
126+
[super didMoveToWindow];
127+
128+
if (_lastParentScrollViewComponentView) {
129+
[_lastParentScrollViewComponentView removeScrollListener:self];
130+
_lastParentScrollViewComponentView = nil;
131+
}
132+
133+
if (RCTScrollViewComponentView *parentScrollViewComponentView = [self getParentScrollViewComponentView]) {
134+
if (self.window) {
135+
// TODO(T202601695): We also want the ScrollView to emit layout changes from didLayoutSubviews so that any event
136+
// that may affect visibily of this view notifies the listeners.
137+
[parentScrollViewComponentView addScrollListener:self];
138+
_lastParentScrollViewComponentView = parentScrollViewComponentView;
139+
140+
// We want to dispatch the event immediately when the view is added to the window before any scrolling occurs.
141+
[self dispatchOnModeChangeIfNeeded:NO];
142+
}
143+
}
144+
}
145+
146+
- (void)scrollViewDidScroll:(UIScrollView *)scrollView
147+
{
148+
[self dispatchOnModeChangeIfNeeded:NO];
149+
}
150+
151+
- (void)dispatchOnModeChangeIfNeeded:(BOOL)checkForTargetRectChange
152+
{
153+
if (!_lastParentScrollViewComponentView) {
154+
return;
155+
}
156+
157+
UIScrollView *scrollView = _lastParentScrollViewComponentView.scrollView;
158+
CGRect targetRect = [self convertRect:self.bounds toView:scrollView];
159+
160+
// While scrolling, the `targetRect` does not change, so we don't check for changed `targetRect` in that case.
161+
if (checkForTargetRectChange) {
162+
if (_targetRect.has_value() && CGRectEqualToRect(targetRect, _targetRect.value())) {
163+
return;
164+
}
165+
_targetRect = targetRect;
166+
}
167+
168+
enum RCTVirtualViewMode newMode;
169+
CGRect thresholdRect = CGRectMake(
170+
scrollView.contentOffset.x,
171+
scrollView.contentOffset.y,
172+
scrollView.frame.size.width,
173+
scrollView.frame.size.height);
174+
if (CGRectOverlaps(targetRect, thresholdRect)) {
175+
newMode = RCTVirtualViewModeVisible;
176+
} else {
177+
auto prerender = false;
178+
const CGFloat prerenderRatio = ReactNativeFeatureFlags::virtualViewPrerenderRatio();
179+
if (prerenderRatio > 0) {
180+
thresholdRect = CGRectInset(
181+
thresholdRect, -thresholdRect.size.width * prerenderRatio, -thresholdRect.size.height * prerenderRatio);
182+
prerender = CGRectOverlaps(targetRect, thresholdRect);
183+
}
184+
if (prerender) {
185+
newMode = RCTVirtualViewModePrerender;
186+
} else {
187+
newMode = RCTVirtualViewModeHidden;
188+
thresholdRect = CGRectZero;
189+
}
190+
}
191+
192+
if (_mode.has_value() && newMode == _mode.value()) {
193+
return;
194+
}
195+
196+
// NOTE: Make sure to keep these props in sync with dispatchSyncModeChange below where we have to explicitly copy all
197+
// props.
198+
VirtualViewEventEmitter::OnModeChange event = {
199+
.mode = (int)newMode,
200+
.targetRect =
201+
{.x = targetRect.origin.x,
202+
.y = targetRect.origin.y,
203+
.width = targetRect.size.width,
204+
.height = targetRect.size.height},
205+
.thresholdRect =
206+
{.x = thresholdRect.origin.x,
207+
.y = thresholdRect.origin.y,
208+
.width = thresholdRect.size.width,
209+
.height = thresholdRect.size.height},
210+
};
211+
212+
const std::optional<enum RCTVirtualViewMode> oldMode = _mode;
213+
_mode = newMode;
214+
215+
switch (newMode) {
216+
case RCTVirtualViewModeVisible:
217+
[self dispatchSyncModeChange:event];
218+
break;
219+
case RCTVirtualViewModePrerender:
220+
if (!oldMode.has_value() || oldMode != RCTVirtualViewModeVisible) {
221+
[self dispatchAsyncModeChange:event];
222+
}
223+
break;
224+
case RCTVirtualViewModeHidden:
225+
[self dispatchAsyncModeChange:event];
226+
break;
227+
}
228+
}
229+
230+
- (void)dispatchAsyncModeChange:(VirtualViewEventEmitter::OnModeChange &)event
231+
{
232+
if (!_eventEmitter) {
233+
return;
234+
}
235+
236+
std::shared_ptr<const VirtualViewEventEmitter> emitter =
237+
std::static_pointer_cast<const VirtualViewEventEmitter>(_eventEmitter);
238+
emitter->onModeChange(event);
239+
}
240+
241+
- (void)dispatchSyncModeChange:(VirtualViewEventEmitter::OnModeChange &)event
242+
{
243+
if (!_eventEmitter) {
244+
return;
245+
}
246+
247+
std::shared_ptr<const VirtualViewEventEmitter> emitter =
248+
std::static_pointer_cast<const VirtualViewEventEmitter>(_eventEmitter);
249+
250+
// TODO: Move this into a custom event emitter. We had to duplicate the codegen code here from onModeChange in order
251+
// to dispatch synchronously and discrete.
252+
emitter->experimental_flushSync([&emitter, &event]() {
253+
emitter->dispatchEvent(
254+
"modeChange",
255+
[event](jsi::Runtime &runtime) {
256+
auto payload = jsi::Object(runtime);
257+
payload.setProperty(runtime, "mode", event.mode);
258+
{
259+
auto targetRect = jsi::Object(runtime);
260+
targetRect.setProperty(runtime, "x", event.targetRect.x);
261+
targetRect.setProperty(runtime, "y", event.targetRect.y);
262+
targetRect.setProperty(runtime, "width", event.targetRect.width);
263+
targetRect.setProperty(runtime, "height", event.targetRect.height);
264+
payload.setProperty(runtime, "targetRect", targetRect);
265+
}
266+
{
267+
auto thresholdRect = jsi::Object(runtime);
268+
thresholdRect.setProperty(runtime, "x", event.thresholdRect.x);
269+
thresholdRect.setProperty(runtime, "y", event.thresholdRect.y);
270+
thresholdRect.setProperty(runtime, "width", event.thresholdRect.width);
271+
thresholdRect.setProperty(runtime, "height", event.thresholdRect.height);
272+
payload.setProperty(runtime, "thresholdRect", thresholdRect);
273+
}
274+
return payload;
275+
},
276+
RawEvent::Category::Discrete);
277+
});
278+
}
279+
280+
+ (ComponentDescriptorProvider)componentDescriptorProvider
281+
{
282+
return concreteComponentDescriptorProvider<VirtualViewComponentDescriptor>();
283+
}
284+
285+
@end
286+
287+
Class<RCTComponentViewProtocol> VirtualViewCls(void)
288+
{
289+
return RCTVirtualViewComponentView.class;
290+
}

0 commit comments

Comments
 (0)