2424
2525using namespace facebook ::react;
2626
27+ static CGFloat const kClippingLeeway = 44.0 ;
28+
2729static void RCTSendPaperScrollEvent_DEPRECATED (UIScrollView *scrollView, NSInteger tag)
2830{
2931 static uint16_t coalescingKey = 0 ;
@@ -39,6 +41,13 @@ static void RCTSendPaperScrollEvent_DEPRECATED(UIScrollView *scrollView, NSInteg
3941 [[RCTBridge currentBridge ].eventDispatcher sendEvent: scrollEvent];
4042}
4143
44+ static BOOL isOnDemandViewMountingEnabledGlobally = NO ;
45+
46+ void RCTSetEnableOnDemandViewMounting (BOOL value)
47+ {
48+ isOnDemandViewMountingEnabledGlobally = value;
49+ }
50+
4251@interface RCTScrollViewComponentView () <UIScrollViewDelegate, RCTScrollViewProtocol, RCTScrollableProtocol>
4352
4453@end
@@ -53,6 +62,10 @@ @implementation RCTScrollViewComponentView {
5362 // This helps to only update state from `scrollViewDidScroll` in case
5463 // some other part of the system scrolls scroll view.
5564 BOOL _isUserTriggeredScrolling;
65+
66+ BOOL _isOnDemandViewMountingEnabled;
67+ CGPoint _contentOffsetWhenClipped;
68+ NSMutableArray <UIView<RCTComponentViewProtocol> *> *_childComponentViews;
5669}
5770
5871+ (RCTScrollViewComponentView *_Nullable)findScrollViewComponentViewForView : (UIView *)view
@@ -69,6 +82,9 @@ - (instancetype)initWithFrame:(CGRect)frame
6982 static const auto defaultProps = std::make_shared<const ScrollViewProps>();
7083 _props = defaultProps;
7184
85+ _isOnDemandViewMountingEnabled = isOnDemandViewMountingEnabledGlobally;
86+ _childComponentViews = [[NSMutableArray alloc ] init ];
87+
7288 _scrollView = [[RCTEnhancedScrollView alloc ] initWithFrame: self .bounds];
7389 _scrollView.autoresizingMask = UIViewAutoresizingFlexibleWidth | UIViewAutoresizingFlexibleHeight;
7490 _scrollView.delaysContentTouches = NO ;
@@ -98,6 +114,13 @@ - (void)dealloc
98114 return ((RCTEnhancedScrollView *)_scrollView).delegateSplitter ;
99115}
100116
117+ #pragma mark - RCTMountingTransactionObserving
118+
119+ - (void )mountingTransactionDidMountWithMetadata : (MountingTransactionMetadata const &)metadata
120+ {
121+ [self _remountChildren ];
122+ }
123+
101124#pragma mark - RCTComponentViewProtocol
102125
103126+ (ComponentDescriptorProvider)componentDescriptorProvider
@@ -192,13 +215,27 @@ - (void)updateState:(State::Shared const &)state oldState:(State::Shared const &
192215
193216- (void )mountChildComponentView : (UIView<RCTComponentViewProtocol> *)childComponentView index : (NSInteger )index
194217{
195- [_containerView insertSubview: childComponentView atIndex: index];
218+ if (_isOnDemandViewMountingEnabled) {
219+ [_childComponentViews insertObject: childComponentView atIndex: index];
220+ } else {
221+ [_containerView insertSubview: childComponentView atIndex: index];
222+ }
196223}
197224
198225- (void )unmountChildComponentView : (UIView<RCTComponentViewProtocol> *)childComponentView index : (NSInteger )index
199226{
200- RCTAssert (childComponentView.superview == _containerView, @" Attempt to unmount improperly mounted component view." );
201- [childComponentView removeFromSuperview ];
227+ if (_isOnDemandViewMountingEnabled) {
228+ RCTAssert (
229+ [_childComponentViews objectAtIndex: index] == childComponentView,
230+ @" Attempt to unmount improperly mounted component view." );
231+ [_childComponentViews removeObjectAtIndex: index];
232+ // In addition to removing a view from `_childComponentViews`,
233+ // we have to unmount views immediately to not mess with recycling.
234+ [childComponentView removeFromSuperview ];
235+ } else {
236+ RCTAssert (childComponentView.superview == _containerView, @" Attempt to unmount improperly mounted component view." );
237+ [childComponentView removeFromSuperview ];
238+ }
202239}
203240
204241- (ScrollViewMetrics)_scrollViewMetrics
@@ -241,18 +278,19 @@ - (void)scrollViewDidScroll:(UIScrollView *)scrollView
241278 if (!_isUserTriggeredScrolling) {
242279 [self _updateStateWithContentOffset ];
243280 }
244- if (!_eventEmitter) {
245- return ;
246- }
247281
248282 NSTimeInterval now = CACurrentMediaTime ();
249283 if ((_lastScrollEventDispatchTime == 0 ) || (now - _lastScrollEventDispatchTime > _scrollEventThrottle)) {
250284 _lastScrollEventDispatchTime = now;
251- std::static_pointer_cast<ScrollViewEventEmitter const >(_eventEmitter)->onScroll ([self _scrollViewMetrics ]);
285+ if (_eventEmitter) {
286+ std::static_pointer_cast<ScrollViewEventEmitter const >(_eventEmitter)->onScroll ([self _scrollViewMetrics ]);
287+ }
252288 // Once Fabric implements proper NativeAnimationDriver, this should be removed.
253289 // This is just a workaround to allow animations based on onScroll event.
254290 RCTSendPaperScrollEvent_DEPRECATED (scrollView, self.tag );
255291 }
292+
293+ [self _remountChildrenIfNeeded ];
256294}
257295
258296- (void )scrollViewDidZoom : (UIScrollView *)scrollView
@@ -395,6 +433,66 @@ - (void)scrollToEnd:(BOOL)animated
395433 [_scrollView setContentOffset: offset animated: animated];
396434}
397435
436+ #pragma mark - Child views mounting
437+
438+ - (void )_remountChildrenIfNeeded
439+ {
440+ if (!_isOnDemandViewMountingEnabled) {
441+ return ;
442+ }
443+
444+ CGPoint contentOffset = _scrollView.contentOffset ;
445+
446+ if (std::abs (_contentOffsetWhenClipped.x - contentOffset.x ) < kClippingLeeway &&
447+ std::abs (_contentOffsetWhenClipped.y - contentOffset.y ) < kClippingLeeway ) {
448+ return ;
449+ }
450+
451+ _contentOffsetWhenClipped = contentOffset;
452+
453+ [self _remountChildren ];
454+ }
455+
456+ - (void )_remountChildren
457+ {
458+ if (!_isOnDemandViewMountingEnabled) {
459+ return ;
460+ }
461+
462+ CGRect containerFrame = CGRect{CGPointZero, _scrollView.bounds .size };
463+ containerFrame = UIEdgeInsetsInsetRect (containerFrame, _scrollView.contentInset );
464+ containerFrame.origin = _scrollView.contentOffset ;
465+
466+ NSInteger mountedIndex = 0 ;
467+ for (UIView *componentView in _childComponentViews) {
468+ BOOL shouldBeMounted = YES ;
469+ BOOL isMounted = componentView.superview != nil ;
470+
471+ // If a view is mounted, it must be mounted exactly at `mountedIndex` position.
472+ RCTAssert (
473+ !isMounted || [_containerView.subviews objectAtIndex: mountedIndex] == componentView,
474+ @" Attempt to unmount improperly mounted component view." );
475+
476+ // It's simpler and faster to not mess with views that are not `RCTViewComponentView` subclasses.
477+ if ([componentView isKindOfClass: [RCTViewComponentView class ]]) {
478+ CGRect viewFrameWithLeeway = CGRectInset (componentView.frame , -kClippingLeeway , -kClippingLeeway );
479+ shouldBeMounted = CGRectIntersectsRect (containerFrame, viewFrameWithLeeway);
480+ }
481+
482+ if (shouldBeMounted != isMounted) {
483+ if (shouldBeMounted) {
484+ [_containerView insertSubview: componentView atIndex: mountedIndex];
485+ } else {
486+ [componentView removeFromSuperview ];
487+ }
488+ }
489+
490+ if (shouldBeMounted) {
491+ mountedIndex++;
492+ }
493+ }
494+ }
495+
398496#pragma mark - RCTScrollableProtocol
399497
400498- (CGSize)contentSize
0 commit comments