diff --git a/Examples/UIExplorer/UIExplorerUnitTests/RCTUIManagerTests.m b/Examples/UIExplorer/UIExplorerUnitTests/RCTUIManagerTests.m index 4ae4b9f5c..c3bd09ff1 100644 --- a/Examples/UIExplorer/UIExplorerUnitTests/RCTUIManagerTests.m +++ b/Examples/UIExplorer/UIExplorerUnitTests/RCTUIManagerTests.m @@ -77,8 +77,8 @@ @"Expect to have 5 react subviews after calling manage children \ with 5 tags to add, instead have %lu", (unsigned long)[[containerView reactSubviews] count]); for (UIView *view in addedViews) { - XCTAssertTrue([view superview] == containerView, - @"Expected to have manage children successfully add children"); + XCTAssertTrue([view reactSuperview] == containerView, + @"Expected to have manage children successfully add children"); [view removeFromSuperview]; } } @@ -95,7 +95,7 @@ } for (NSInteger i = 2; i < 20; i++) { UIView *view = _uiManager.viewRegistry[@(i)]; - [containerView addSubview:view]; + [containerView insertReactSubview:view atIndex:containerView.reactSubviews.count]; } // Remove views 1-5 from view 20 @@ -112,7 +112,7 @@ with 5 tags to remove and 18 prior children, instead have %zd", containerView.reactSubviews.count); for (UIView *view in removedViews) { - XCTAssertTrue([view superview] == nil, + XCTAssertTrue([view reactSuperview] == nil, @"Expected to have manage children successfully remove children"); // After removing views are unregistered - we need to reregister _uiManager.viewRegistry[view.reactTag] = view; @@ -155,7 +155,7 @@ for (NSInteger i = 1; i < 11; i++) { UIView *view = _uiManager.viewRegistry[@(i)]; - [containerView addSubview:view]; + [containerView insertReactSubview:view atIndex:containerView.reactSubviews.count]; } [_uiManager _manageChildren:@20 diff --git a/Libraries/Text/RCTText.m b/Libraries/Text/RCTText.m index 864fa096b..fc5f60bdc 100644 --- a/Libraries/Text/RCTText.m +++ b/Libraries/Text/RCTText.m @@ -27,7 +27,6 @@ static void collectNonTextDescendants(RCTText *view, NSMutableArray *nonTextDesc @implementation RCTText { NSTextStorage *_textStorage; - NSMutableArray *_reactSubviews; CAShapeLayer *_highlightLayer; } @@ -35,7 +34,6 @@ static void collectNonTextDescendants(RCTText *view, NSMutableArray *nonTextDesc { if ((self = [super initWithFrame:frame])) { _textStorage = [NSTextStorage new]; - _reactSubviews = [NSMutableArray array]; self.isAccessibilityElement = YES; self.accessibilityTraits |= UIAccessibilityTraitStaticText; @@ -68,19 +66,9 @@ static void collectNonTextDescendants(RCTText *view, NSMutableArray *nonTextDesc self.backgroundColor = inheritedBackgroundColor; } -- (void)insertReactSubview:(UIView *)subview atIndex:(NSInteger)atIndex +- (void)reactUpdateSubviews { - [_reactSubviews insertObject:subview atIndex:atIndex]; -} - -- (void)removeReactSubview:(UIView *)subview -{ - [_reactSubviews removeObject:subview]; -} - -- (NSArray *)reactSubviews -{ - return _reactSubviews; + // Do nothing, as subviews are managed by `setTextStorage:` method } - (void)setTextStorage:(NSTextStorage *)textStorage @@ -88,6 +76,7 @@ static void collectNonTextDescendants(RCTText *view, NSMutableArray *nonTextDesc if (_textStorage != textStorage) { _textStorage = textStorage; + // Update subviews NSMutableArray *nonTextDescendants = [NSMutableArray new]; collectNonTextDescendants(self, nonTextDescendants); NSArray *subviews = self.subviews; diff --git a/Libraries/Text/RCTTextField.m b/Libraries/Text/RCTTextField.m index 24b077baf..71859ad4d 100644 --- a/Libraries/Text/RCTTextField.m +++ b/Libraries/Text/RCTTextField.m @@ -17,7 +17,6 @@ @implementation RCTTextField { RCTEventDispatcher *_eventDispatcher; - NSMutableArray *_reactSubviews; BOOL _jsRequestingFirstResponder; NSInteger _nativeEventCount; BOOL _submitted; @@ -35,7 +34,6 @@ [self addTarget:self action:@selector(textFieldEndEditing) forControlEvents:UIControlEventEditingDidEnd]; [self addTarget:self action:@selector(textFieldSubmitEditing) forControlEvents:UIControlEventEditingDidEndOnExit]; [self addObserver:self forKeyPath:@"selectedTextRange" options:0 context:nil]; - _reactSubviews = [NSMutableArray new]; _blurOnSubmit = YES; } return self; @@ -112,30 +110,6 @@ static void RCTUpdatePlaceholder(RCTTextField *self) RCTUpdatePlaceholder(self); } -- (NSArray *)reactSubviews -{ - // TODO: do we support subviews of textfield in React? - // In any case, we should have a better approach than manually - // maintaining array in each view subclass like this - return _reactSubviews; -} - -- (void)removeReactSubview:(UIView *)subview -{ - // TODO: this is a bit broken - if the TextField inserts any of - // its own views below or between React's, the indices won't match - [_reactSubviews removeObject:subview]; - [subview removeFromSuperview]; -} - -- (void)insertReactSubview:(UIView *)view atIndex:(NSInteger)atIndex -{ - // TODO: this is a bit broken - if the TextField inserts any of - // its own views below or between React's, the indices won't match - [_reactSubviews insertObject:view atIndex:atIndex]; - [super insertSubview:view atIndex:atIndex]; -} - - (CGRect)caretRectForPosition:(UITextPosition *)position { if (_caretHidden) { diff --git a/Libraries/Text/RCTTextView.m b/Libraries/Text/RCTTextView.m index 84e83214f..2d4c0a7d9 100644 --- a/Libraries/Text/RCTTextView.m +++ b/Libraries/Text/RCTTextView.m @@ -67,7 +67,6 @@ NSInteger _nativeEventCount; RCTText *_richTextView; NSAttributedString *_pendingAttributedText; - NSMutableArray *_subviews; BOOL _blockTextShouldChange; UITextRange *_previousSelectionRange; NSUInteger _previousTextLength; @@ -97,7 +96,6 @@ _previousSelectionRange = _textView.selectedTextRange; - _subviews = [NSMutableArray new]; [self addSubview:_scrollView]; } return self; @@ -106,19 +104,14 @@ RCT_NOT_IMPLEMENTED(- (instancetype)initWithFrame:(CGRect)frame) RCT_NOT_IMPLEMENTED(- (instancetype)initWithCoder:(NSCoder *)aDecoder) -- (NSArray *)reactSubviews -{ - return _subviews; -} - - (void)insertReactSubview:(UIView *)subview atIndex:(NSInteger)index { + [super insertReactSubview:subview atIndex:index]; if ([subview isKindOfClass:[RCTText class]]) { if (_richTextView) { RCTLogError(@"Tried to insert a second into - there can only be one."); } _richTextView = (RCTText *)subview; - [_subviews insertObject:_richTextView atIndex:index]; // If this is in rich text editing mode, and the child node providing rich text // styling has a backgroundColor, then the attributedText produced by the child node will have an @@ -131,23 +124,22 @@ RCT_NOT_IMPLEMENTED(- (instancetype)initWithCoder:(NSCoder *)aDecoder) attrs[NSBackgroundColorAttributeName] = subview.backgroundColor; _textView.typingAttributes = attrs; } - } else { - [_subviews insertObject:subview atIndex:index]; - [self insertSubview:subview atIndex:index]; } } - (void)removeReactSubview:(UIView *)subview { + [super removeReactSubview:subview]; if (_richTextView == subview) { - [_subviews removeObject:_richTextView]; _richTextView = nil; - } else { - [_subviews removeObject:subview]; - [subview removeFromSuperview]; } } +- (void)reactUpdateSubviews +{ + // Do nothing, as we don't allow non-text subviews +} + - (void)setMostRecentEventCount:(NSInteger)mostRecentEventCount { _mostRecentEventCount = mostRecentEventCount; diff --git a/React/Base/RCTRootView.m b/React/Base/RCTRootView.m index cff7861a1..78896f5fe 100644 --- a/React/Base/RCTRootView.m +++ b/React/Base/RCTRootView.m @@ -337,7 +337,7 @@ RCT_NOT_IMPLEMENTED(- (instancetype)initWithCoder:(NSCoder *)aDecoder) RCT_NOT_IMPLEMENTED(-(instancetype)initWithFrame:(CGRect)frame) RCT_NOT_IMPLEMENTED(-(instancetype)initWithCoder:(nonnull NSCoder *)aDecoder) -- (void)insertReactSubview:(id)subview atIndex:(NSInteger)atIndex +- (void)insertReactSubview:(UIView *)subview atIndex:(NSInteger)atIndex { [super insertReactSubview:subview atIndex:atIndex]; RCTPerformanceLoggerEnd(RCTPLTTI); diff --git a/React/Modules/RCTUIManager.m b/React/Modules/RCTUIManager.m index 168e115dc..4ab30f991 100644 --- a/React/Modules/RCTUIManager.m +++ b/React/Modules/RCTUIManager.m @@ -893,16 +893,18 @@ static void RCTSetChildren(NSNumber *containerTag, [container insertReactSubview:view atIndex:index++]; } } + + [container didUpdateReactSubviews]; } -RCT_EXPORT_METHOD(manageChildren:(nonnull NSNumber *)containerReactTag +RCT_EXPORT_METHOD(manageChildren:(nonnull NSNumber *)containerTag moveFromIndices:(NSArray *)moveFromIndices moveToIndices:(NSArray *)moveToIndices addChildReactTags:(NSArray *)addChildReactTags addAtIndices:(NSArray *)addAtIndices removeAtIndices:(NSArray *)removeAtIndices) { - [self _manageChildren:containerReactTag + [self _manageChildren:containerTag moveFromIndices:moveFromIndices moveToIndices:moveToIndices addChildReactTags:addChildReactTags @@ -911,7 +913,7 @@ RCT_EXPORT_METHOD(manageChildren:(nonnull NSNumber *)containerReactTag registry:(NSMutableDictionary> *)_shadowViewRegistry]; [self addUIBlock:^(RCTUIManager *uiManager, NSDictionary *viewRegistry){ - [uiManager _manageChildren:containerReactTag + [uiManager _manageChildren:containerTag moveFromIndices:moveFromIndices moveToIndices:moveToIndices addChildReactTags:addChildReactTags @@ -921,7 +923,7 @@ RCT_EXPORT_METHOD(manageChildren:(nonnull NSNumber *)containerReactTag }]; } -- (void)_manageChildren:(NSNumber *)containerReactTag +- (void)_manageChildren:(NSNumber *)containerTag moveFromIndices:(NSArray *)moveFromIndices moveToIndices:(NSArray *)moveToIndices addChildReactTags:(NSArray *)addChildReactTags @@ -929,7 +931,7 @@ RCT_EXPORT_METHOD(manageChildren:(nonnull NSNumber *)containerReactTag removeAtIndices:(NSArray *)removeAtIndices registry:(NSMutableDictionary> *)registry { - id container = registry[containerReactTag]; + id container = registry[containerTag]; RCTAssert(moveFromIndices.count == moveToIndices.count, @"moveFromIndices had size %tu, moveToIndices had size %tu", moveFromIndices.count, moveToIndices.count); RCTAssert(addChildReactTags.count == addAtIndices.count, @"there should be at least one React child to add"); @@ -963,6 +965,8 @@ RCT_EXPORT_METHOD(manageChildren:(nonnull NSNumber *)containerReactTag [container insertReactSubview:destinationsToChildrenToAdd[reactIndex] atIndex:reactIndex.integerValue]; } + + [container didUpdateReactSubviews]; } RCT_EXPORT_METHOD(createView:(nonnull NSNumber *)reactTag diff --git a/React/Views/RCTComponent.h b/React/Views/RCTComponent.h index 5236f850b..5c9beab09 100644 --- a/React/Views/RCTComponent.h +++ b/React/Views/RCTComponent.h @@ -43,6 +43,11 @@ typedef void (^RCTBubblingEventBlock)(NSDictionary *body); */ - (void)didSetProps:(NSArray *)changedProps; +/** + * Called each time subviews have been updated + */ +- (void)didUpdateReactSubviews; + // TODO: Deprecate this // This method is called after layout has been performed for all views known // to the RCTViewManager. It is only called on UIViews, not shadow views. diff --git a/React/Views/RCTMap.m b/React/Views/RCTMap.m index 1b3472530..7a1334668 100644 --- a/React/Views/RCTMap.m +++ b/React/Views/RCTMap.m @@ -23,7 +23,6 @@ const CGFloat RCTMapZoomBoundBuffer = 0.01; { UIView *_legalLabel; CLLocationManager *_locationManager; - NSMutableArray *_reactSubviews; } - (instancetype)init @@ -31,7 +30,6 @@ const CGFloat RCTMapZoomBoundBuffer = 0.01; if ((self = [super init])) { _hasStartedRendering = NO; - _reactSubviews = [NSMutableArray new]; // Find Apple link label for (UIView *subview in self.subviews) { @@ -51,19 +49,9 @@ const CGFloat RCTMapZoomBoundBuffer = 0.01; [_regionChangeObserveTimer invalidate]; } -- (void)insertReactSubview:(UIView *)subview atIndex:(NSInteger)atIndex +- (void)reactUpdateSubviews { - [_reactSubviews insertObject:subview atIndex:atIndex]; -} - -- (void)removeReactSubview:(UIView *)subview -{ - [_reactSubviews removeObject:subview]; -} - -- (NSArray *)reactSubviews -{ - return _reactSubviews; + // Do nothing, as annotation views are managed by `setAnnotations:` method } - (void)layoutSubviews diff --git a/React/Views/RCTModalHostView.m b/React/Views/RCTModalHostView.m index c31402493..3dce5ed75 100644 --- a/React/Views/RCTModalHostView.m +++ b/React/Views/RCTModalHostView.m @@ -55,14 +55,10 @@ RCT_NOT_IMPLEMENTED(- (instancetype)initWithCoder:coder) } } -- (NSArray *)reactSubviews -{ - return _reactSubview ? @[_reactSubview] : @[]; -} - -- (void)insertReactSubview:(UIView *)subview atIndex:(__unused NSInteger)atIndex +- (void)insertReactSubview:(UIView *)subview atIndex:(NSInteger)atIndex { RCTAssert(_reactSubview == nil, @"Modal view can only have one subview"); + [super insertReactSubview:subview atIndex:atIndex]; [subview addGestureRecognizer:_touchHandler]; subview.autoresizingMask = UIViewAutoresizingFlexibleHeight | UIViewAutoresizingFlexibleWidth; @@ -74,11 +70,16 @@ RCT_NOT_IMPLEMENTED(- (instancetype)initWithCoder:coder) - (void)removeReactSubview:(UIView *)subview { RCTAssert(subview == _reactSubview, @"Cannot remove view other than modal view"); + [super removeReactSubview:subview]; [subview removeGestureRecognizer:_touchHandler]; - [subview removeFromSuperview]; _reactSubview = nil; } +- (void)didUpdateReactSubviews +{ + // Do nothing, as subview (singular) is managed by `insertReactSubview:atIndex:` +} + - (void)dismissModalViewController { if (_isPresented) { diff --git a/React/Views/RCTNavigator.m b/React/Views/RCTNavigator.m index d95089ee4..eb19b1f6c 100644 --- a/React/Views/RCTNavigator.m +++ b/React/Views/RCTNavigator.m @@ -217,7 +217,6 @@ NSInteger kNeverProgressed = -10000; // Previous views are only mainted in order to detect incorrect // addition/removal of views below the `requestedTopOfStack` @property (nonatomic, copy, readwrite) NSArray *previousViews; -@property (nonatomic, readwrite, strong) NSMutableArray *currentViews; @property (nonatomic, readwrite, strong) RCTNavigationController *navigationController; /** * Display link is used to get high frequency sample rate during @@ -299,7 +298,6 @@ NSInteger kNeverProgressed = -10000; _dummyView = [[UIView alloc] initWithFrame:CGRectZero]; _previousRequestedTopOfStack = kNeverRequested; // So that we initialize with a push. _previousViews = @[]; - _currentViews = [[NSMutableArray alloc] initWithCapacity:0]; __weak RCTNavigator *weakSelf = self; _navigationController = [[RCTNavigationController alloc] initWithScrollCallback:^{ [weakSelf dispatchFakeScrollEvent]; @@ -351,7 +349,7 @@ RCT_NOT_IMPLEMENTED(- (instancetype)initWithCoder:(NSCoder *)aDecoder) - (void)setInteractivePopGestureEnabled:(BOOL)interactivePopGestureEnabled { _interactivePopGestureEnabled = interactivePopGestureEnabled; - + _navigationController.interactivePopGestureRecognizer.delegate = self; _navigationController.interactivePopGestureRecognizer.enabled = interactivePopGestureEnabled; @@ -402,8 +400,8 @@ RCT_NOT_IMPLEMENTED(- (instancetype)initWithCoder:(NSCoder *)aDecoder) return; } - NSUInteger indexOfFrom = [_currentViews indexOfObject:fromController.navItem]; - NSUInteger indexOfTo = [_currentViews indexOfObject:toController.navItem]; + NSUInteger indexOfFrom = [self.reactSubviews indexOfObject:fromController.navItem]; + NSUInteger indexOfTo = [self.reactSubviews indexOfObject:toController.navItem]; CGFloat destination = indexOfFrom < indexOfTo ? 1.0 : -1.0; _dummyView.frame = (CGRect){{destination, 0}, CGSizeZero}; _currentlyTransitioningFrom = indexOfFrom; @@ -433,7 +431,7 @@ RCT_NOT_IMPLEMENTED(- (instancetype)initWithCoder:(NSCoder *)aDecoder) - (void)freeLock { _navigationController.navigationLock = RCTNavigationLockNone; - + // Unless the pop gesture has been explicitly disabled (RCTPopGestureStateDisabled), // Set interactivePopGestureRecognizer.enabled to YES // If the popGestureState is RCTPopGestureStateDefault the default behavior will be maintained @@ -452,12 +450,12 @@ RCT_NOT_IMPLEMENTED(- (instancetype)initWithCoder:(NSCoder *)aDecoder) _navigationController.navigationLock == RCTNavigationLockJavaScript, @"Cannot change subviews from JS without first locking." ); - [_currentViews insertObject:view atIndex:atIndex]; + [super insertReactSubview:view atIndex:atIndex]; } -- (NSArray *)reactSubviews +- (void)didUpdateReactSubviews { - return _currentViews; + // Do nothing, as subviews are managed by `reactBridgeDidFinishTransaction` } - (void)layoutSubviews @@ -469,11 +467,11 @@ RCT_NOT_IMPLEMENTED(- (instancetype)initWithCoder:(NSCoder *)aDecoder) - (void)removeReactSubview:(RCTNavItem *)subview { - if (_currentViews.count <= 0 || subview == _currentViews[0]) { + if (self.reactSubviews.count <= 0 || subview == self.reactSubviews[0]) { RCTLogError(@"Attempting to remove invalid RCT subview of RCTNavigator"); return; } - [_currentViews removeObject:subview]; + [super removeReactSubview:subview]; } - (void)handleTopOfStackChanged @@ -497,7 +495,8 @@ RCT_NOT_IMPLEMENTED(- (instancetype)initWithCoder:(NSCoder *)aDecoder) - (UIView *)reactSuperview { RCTAssert(!_bridge.isValid || self.superview != nil, @"put reactNavSuperviewLink back"); - return self.superview ? self.superview : self.reactNavSuperviewLink; + UIView *superview = [super reactSuperview]; + return superview ?: self.reactNavSuperviewLink; } - (void)reactBridgeDidFinishTransaction @@ -545,14 +544,14 @@ BOOL jsGettingtooSlow = jsGettingtooSlow)) { RCTLogError(@"JS has only made partial progress to catch up to UIKit"); } - if (currentReactCount > _currentViews.count) { + if (currentReactCount > self.reactSubviews.count) { RCTLogError(@"Cannot adjust current top of stack beyond available views"); } // Views before the previous React count must not have changed. Views greater than previousReactCount // up to currentReactCount may have changed. - for (NSUInteger i = 0; i < MIN(_currentViews.count, MIN(_previousViews.count, previousReactCount)); i++) { - if (_currentViews[i] != _previousViews[i]) { + for (NSUInteger i = 0; i < MIN(self.reactSubviews.count, MIN(_previousViews.count, previousReactCount)); i++) { + if (self.reactSubviews[i] != _previousViews[i]) { RCTLogError(@"current view should equal previous view"); } } @@ -561,7 +560,7 @@ BOOL jsGettingtooSlow = } if (jsGettingAhead) { if (reactPushOne) { - UIView *lastView = _currentViews.lastObject; + UIView *lastView = self.reactSubviews.lastObject; RCTWrapperViewController *vc = [[RCTWrapperViewController alloc] initWithNavItem:(RCTNavItem *)lastView]; vc.navigationListener = self; _numberOfViewControllerMovesToIgnore = 1; @@ -580,7 +579,7 @@ BOOL jsGettingtooSlow = return; } - _previousViews = [_currentViews copy]; + _previousViews = [self.reactSubviews copy]; _previousRequestedTopOfStack = _requestedTopOfStack; } diff --git a/React/Views/RCTScrollView.m b/React/Views/RCTScrollView.m index a2d1e5d85..3f2cd82ef 100644 --- a/React/Views/RCTScrollView.m +++ b/React/Views/RCTScrollView.m @@ -418,8 +418,9 @@ RCT_NOT_IMPLEMENTED(- (instancetype)initWithCoder:(NSCoder *)aDecoder) // Does nothing } -- (void)insertReactSubview:(UIView *)view atIndex:(__unused NSInteger)atIndex +- (void)insertReactSubview:(UIView *)view atIndex:(NSInteger)atIndex { + [super insertReactSubview:view atIndex:atIndex]; if ([view isKindOfClass:[RCTRefreshControl class]]) { _scrollView.refreshControl = (RCTRefreshControl*)view; } else { @@ -431,21 +432,18 @@ RCT_NOT_IMPLEMENTED(- (instancetype)initWithCoder:(NSCoder *)aDecoder) - (void)removeReactSubview:(UIView *)subview { + [super removeReactSubview:subview]; if ([subview isKindOfClass:[RCTRefreshControl class]]) { _scrollView.refreshControl = nil; } else { RCTAssert(_contentView == subview, @"Attempted to remove non-existent subview"); _contentView = nil; - [subview removeFromSuperview]; } } -- (NSArray *)reactSubviews +- (void)didUpdateReactSubviews { - if (_contentView && _scrollView.refreshControl) { - return @[_contentView, _scrollView.refreshControl]; - } - return _contentView ? @[_contentView] : @[]; + // Do nothing, as subviews are managed by `insertReactSubview:atIndex:` } - (BOOL)centerContent diff --git a/React/Views/RCTShadowView.m b/React/Views/RCTShadowView.m index 83ce22a5b..0d31e1507 100644 --- a/React/Views/RCTShadowView.m +++ b/React/Views/RCTShadowView.m @@ -297,6 +297,11 @@ static void RCTProcessMetaProps(const float metaProps[META_PROP_COUNT], float st return RCTIsReactRootView(self.reactTag); } +- (void)didUpdateReactSubviews +{ + // Does nothing by default +} + - (void)dealloc { free_css_node(_cssNode); diff --git a/React/Views/RCTTabBar.m b/React/Views/RCTTabBar.m index 6f31f6998..d544d75b2 100644 --- a/React/Views/RCTTabBar.m +++ b/React/Views/RCTTabBar.m @@ -26,13 +26,11 @@ { BOOL _tabsChanged; UITabBarController *_tabController; - NSMutableArray *_tabViews; } - (instancetype)initWithFrame:(CGRect)frame { if ((self = [super initWithFrame:frame])) { - _tabViews = [NSMutableArray new]; _tabController = [UITabBarController new]; _tabController.delegate = self; [self addSubview:_tabController.view]; @@ -53,31 +51,31 @@ RCT_NOT_IMPLEMENTED(- (instancetype)initWithCoder:(NSCoder *)aDecoder) [_tabController removeFromParentViewController]; } -- (NSArray *)reactSubviews +- (void)insertReactSubview:(RCTTabBarItem *)subview atIndex:(NSInteger)atIndex { - return _tabViews; -} - -- (void)insertReactSubview:(RCTTabBarItem *)view atIndex:(NSInteger)atIndex -{ - if (![view isKindOfClass:[RCTTabBarItem class]]) { + if (![subview isKindOfClass:[RCTTabBarItem class]]) { RCTLogError(@"subview should be of type RCTTabBarItem"); return; } - [_tabViews insertObject:view atIndex:atIndex]; + [super insertReactSubview:subview atIndex:atIndex]; _tabsChanged = YES; } - (void)removeReactSubview:(RCTTabBarItem *)subview { - if (_tabViews.count == 0) { + if (self.reactSubviews.count == 0) { RCTLogError(@"should have at least one view to remove a subview"); return; } - [_tabViews removeObject:subview]; + [super removeReactSubview:subview]; _tabsChanged = YES; } +- (void)didUpdateReactSubviews +{ + // Do nothing, as subviews are managed by `reactBridgeDidFinishTransaction` +} + - (void)layoutSubviews { [super layoutSubviews]; @@ -106,8 +104,9 @@ RCT_NOT_IMPLEMENTED(- (instancetype)initWithCoder:(NSCoder *)aDecoder) _tabsChanged = NO; } - [_tabViews enumerateObjectsUsingBlock: - ^(RCTTabBarItem *tab, NSUInteger index, __unused BOOL *stop) { + [self.reactSubviews enumerateObjectsUsingBlock:^(UIView *view, NSUInteger index, __unused BOOL *stop) { + + RCTTabBarItem *tab = (RCTTabBarItem *)view; UIViewController *controller = _tabController.viewControllers[index]; if (_unselectedTintColor) { [tab.barItem setTitleTextAttributes:@{NSForegroundColorAttributeName: _unselectedTintColor} forState:UIControlStateNormal]; @@ -165,7 +164,7 @@ RCT_NOT_IMPLEMENTED(- (instancetype)initWithCoder:(NSCoder *)aDecoder) - (BOOL)tabBarController:(UITabBarController *)tabBarController shouldSelectViewController:(UIViewController *)viewController { NSUInteger index = [tabBarController.viewControllers indexOfObject:viewController]; - RCTTabBarItem *tab = _tabViews[index]; + RCTTabBarItem *tab = (RCTTabBarItem *)self.reactSubviews[index]; if (tab.onPress) tab.onPress(nil); return NO; } diff --git a/React/Views/RCTView.m b/React/Views/RCTView.m index ad2ea1424..ffce4b7fd 100644 --- a/React/Views/RCTView.m +++ b/React/Views/RCTView.m @@ -97,7 +97,6 @@ static NSString *RCTRecursiveAccessibilityLabel(UIView *view) @implementation RCTView { - NSMutableArray *_reactSubviews; UIColor *_backgroundColor; } @@ -275,76 +274,31 @@ RCT_NOT_IMPLEMENTED(- (instancetype)initWithCoder:unused) - (void)react_remountAllSubviews { - if (_reactSubviews) { - NSUInteger index = 0; - for (UIView *view in _reactSubviews) { + if (_removeClippedSubviews) { + for (UIView *view in self.reactSubviews) { if (view.superview != self) { - if (index < self.subviews.count) { - [self insertSubview:view atIndex:index]; - } else { - [self addSubview:view]; - } + [self addSubview:view]; [view react_remountAllSubviews]; } - index++; } } else { - // If react_subviews is nil, we must already be showing all subviews + // If _removeClippedSubviews is false, we must already be showing all subviews [super react_remountAllSubviews]; } } -- (void)remountSubview:(UIView *)view -{ - // Calculate insertion index for view - NSInteger index = 0; - for (UIView *subview in _reactSubviews) { - if (subview == view) { - [self insertSubview:view atIndex:index]; - break; - } - if (subview.superview) { - // View is mounted, so bump the index - index++; - } - } -} - -- (void)mountOrUnmountSubview:(UIView *)view withClipRect:(CGRect)clipRect relativeToView:(UIView *)clipView -{ - if (!CGRectIsEmpty(CGRectIntersection(clipRect, view.frame))) { - - // View is at least partially visible, so remount it if unmounted - if (view.superview == nil) { - [self remountSubview:view]; - } - - // Then test its subviews - if (CGRectContainsRect(clipRect, view.frame)) { - [view react_remountAllSubviews]; - } else { - [view react_updateClippedSubviewsWithClipRect:clipRect relativeToView:clipView]; - } - - } else if (view.superview) { - - // View is completely outside the clipRect, so unmount it - [view removeFromSuperview]; - } -} - - (void)react_updateClippedSubviewsWithClipRect:(CGRect)clipRect relativeToView:(UIView *)clipView { // TODO (#5906496): for scrollviews (the primary use-case) we could // optimize this by only doing a range check along the scroll axis, // instead of comparing the whole frame - if (_reactSubviews == nil) { + if (!_removeClippedSubviews) { // Use default behavior if unmounting is disabled return [super react_updateClippedSubviewsWithClipRect:clipRect relativeToView:clipView]; } - if (_reactSubviews.count == 0) { + if (self.reactSubviews.count == 0) { // Do nothing if we have no subviews return; } @@ -360,63 +314,46 @@ RCT_NOT_IMPLEMENTED(- (instancetype)initWithCoder:unused) clipView = self; // Mount / unmount views - for (UIView *view in _reactSubviews) { - [self mountOrUnmountSubview:view withClipRect:clipRect relativeToView:clipView]; + for (UIView *view in self.reactSubviews) { + if (!CGRectIsEmpty(CGRectIntersection(clipRect, view.frame))) { + + // View is at least partially visible, so remount it if unmounted + [self addSubview:view]; + + // Then test its subviews + if (CGRectContainsRect(clipRect, view.frame)) { + // View is fully visible, so remount all subviews + [view react_remountAllSubviews]; + } else { + // View is partially visible, so update clipped subviews + [view react_updateClippedSubviewsWithClipRect:clipRect relativeToView:clipView]; + } + + } else if (view.superview) { + + // View is completely outside the clipRect, so unmount it + [view removeFromSuperview]; + } } } - (void)setRemoveClippedSubviews:(BOOL)removeClippedSubviews { - if (removeClippedSubviews && !_reactSubviews) { - _reactSubviews = [self.subviews mutableCopy]; - } else if (!removeClippedSubviews && _reactSubviews) { + if (!removeClippedSubviews && _removeClippedSubviews) { [self react_remountAllSubviews]; - _reactSubviews = nil; } + _removeClippedSubviews = removeClippedSubviews; } -- (BOOL)removeClippedSubviews +- (void)didUpdateReactSubviews { - return _reactSubviews != nil; -} - -- (void)insertReactSubview:(UIView *)view atIndex:(NSInteger)atIndex -{ - if (_reactSubviews == nil) { - [self insertSubview:view atIndex:atIndex]; + if (_removeClippedSubviews) { + [self updateClippedSubviews]; } else { - [_reactSubviews insertObject:view atIndex:atIndex]; - - // Find a suitable view to use for clipping - UIView *clipView = [self react_findClipView]; - if (clipView) { - - // If possible, don't add subviews if they are clipped - [self mountOrUnmountSubview:view withClipRect:clipView.bounds relativeToView:clipView]; - - } else { - - // Fallback if we can't find a suitable clipView - [self remountSubview:view]; - } + [super didUpdateReactSubviews]; } } -- (void)removeReactSubview:(UIView *)subview -{ - [_reactSubviews removeObject:subview]; - [subview removeFromSuperview]; -} - -- (NSArray *)reactSubviews -{ - // The _reactSubviews array is only used when we have hidden - // offscreen views. If _reactSubviews is nil, we can assume - // that [self reactSubviews] and [self subviews] are the same - - return _reactSubviews ?: self.subviews; -} - - (void)updateClippedSubviews { // Find a suitable view to use for clipping @@ -435,7 +372,7 @@ RCT_NOT_IMPLEMENTED(- (instancetype)initWithCoder:unused) [super layoutSubviews]; - if (_reactSubviews) { + if (_removeClippedSubviews) { [self updateClippedSubviews]; } } diff --git a/React/Views/UIView+React.h b/React/Views/UIView+React.h index 794c977e9..fa779e520 100644 --- a/React/Views/UIView+React.h +++ b/React/Views/UIView+React.h @@ -17,8 +17,19 @@ @interface UIView (React) -- (NSArray *)reactSubviews; -- (UIView *)reactSuperview; +/** + * RCTComponent interface. + */ +- (NSArray *)reactSubviews NS_REQUIRES_SUPER; +- (UIView *)reactSuperview NS_REQUIRES_SUPER; +- (void)insertReactSubview:(UIView *)subview atIndex:(NSInteger)atIndex NS_REQUIRES_SUPER; +- (void)removeReactSubview:(UIView *)subview NS_REQUIRES_SUPER; + +/** + * Updates the subviews array based on the reactSubviews. Default behavior is + * to insert the reactSubviews into the UIView. + */ +- (void)didUpdateReactSubviews; /** * Used by the UIIManager to set the view frame. diff --git a/React/Views/UIView+React.m b/React/Views/UIView+React.m index e7736f3a1..69658d132 100644 --- a/React/Views/UIView+React.m +++ b/React/Views/UIView+React.m @@ -56,20 +56,9 @@ return view.reactTag; } -- (void)insertReactSubview:(UIView *)subview atIndex:(NSInteger)atIndex -{ - [self insertSubview:subview atIndex:atIndex]; -} - -- (void)removeReactSubview:(UIView *)subview -{ - RCTAssert(subview.superview == self, @"%@ is a not a subview of %@", subview, self); - [subview removeFromSuperview]; -} - - (NSArray *)reactSubviews { - return self.subviews; + return objc_getAssociatedObject(self, _cmd); } - (UIView *)reactSuperview @@ -77,6 +66,29 @@ return self.superview; } +- (void)insertReactSubview:(UIView *)subview atIndex:(NSInteger)atIndex +{ + NSMutableArray *reactSubviews = (NSMutableArray *)self.reactSubviews; + if (!reactSubviews) { + reactSubviews = [NSMutableArray new]; + objc_setAssociatedObject(self, @selector(reactSubviews), reactSubviews, OBJC_ASSOCIATION_RETAIN_NONATOMIC); + } + [reactSubviews insertObject:subview atIndex:atIndex]; +} + +- (void)removeReactSubview:(UIView *)subview +{ + [(NSMutableArray *)self.reactSubviews removeObject:subview]; + [subview removeFromSuperview]; +} + +- (void)didUpdateReactSubviews +{ + for (UIView *subview in self.reactSubviews) { + [self addSubview:subview]; + } +} + - (void)reactSetFrame:(CGRect)frame { // These frames are in terms of anchorPoint = topLeft, but internally the