diff --git a/Examples/UIExplorer/UIExplorer/NativeExampleViews/FlexibleSizeExampleView.m b/Examples/UIExplorer/UIExplorer/NativeExampleViews/FlexibleSizeExampleView.m index ce899207027a2e..0465a14f6c155c 100644 --- a/Examples/UIExplorer/UIExplorer/NativeExampleViews/FlexibleSizeExampleView.m +++ b/Examples/UIExplorer/UIExplorer/NativeExampleViews/FlexibleSizeExampleView.m @@ -90,6 +90,7 @@ - (void)layoutSubviews - (NSArray *> *)reactSubviews { // this is to avoid unregistering our RCTRootView when the component is removed from RN hierarchy + (void)[super reactSubviews]; return @[]; } diff --git a/Examples/UIExplorer/UIExplorer/NativeExampleViews/UpdatePropertiesExampleView.m b/Examples/UIExplorer/UIExplorer/NativeExampleViews/UpdatePropertiesExampleView.m index cc78f6cff84024..0416024a8b7da3 100644 --- a/Examples/UIExplorer/UIExplorer/NativeExampleViews/UpdatePropertiesExampleView.m +++ b/Examples/UIExplorer/UIExplorer/NativeExampleViews/UpdatePropertiesExampleView.m @@ -89,6 +89,7 @@ - (void)changeColor - (NSArray *> *)reactSubviews { // this is to avoid unregistering our RCTRootView when the component is removed from RN hierarchy + (void)[super reactSubviews]; return @[]; } diff --git a/Examples/UIExplorer/UIExplorerUnitTests/RCTUIManagerTests.m b/Examples/UIExplorer/UIExplorerUnitTests/RCTUIManagerTests.m index 4ae4b9f5ca8027..c3bd09ff1313ef 100644 --- a/Examples/UIExplorer/UIExplorerUnitTests/RCTUIManagerTests.m +++ b/Examples/UIExplorer/UIExplorerUnitTests/RCTUIManagerTests.m @@ -77,8 +77,8 @@ - (void)testManagingChildrenToAddViews @"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 @@ - (void)testManagingChildrenToRemoveViews } 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 @@ - (void)testManagingChildrenToRemoveViews 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 @@ - (void)testManagingChildrenToAddRemoveAndMove 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 864fa096b42dc0..fc5f60bdcea0d2 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 @@ - (instancetype)initWithFrame:(CGRect)frame { if ((self = [super initWithFrame:frame])) { _textStorage = [NSTextStorage new]; - _reactSubviews = [NSMutableArray array]; self.isAccessibilityElement = YES; self.accessibilityTraits |= UIAccessibilityTraitStaticText; @@ -68,19 +66,9 @@ - (void)reactSetInheritedBackgroundColor:(UIColor *)inheritedBackgroundColor 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 @@ - (void)setTextStorage:(NSTextStorage *)textStorage 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 24b077baff106b..71859ad4d77443 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 @@ - (instancetype)initWithEventDispatcher:(RCTEventDispatcher *)eventDispatcher [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 @@ - (void)setPlaceholder:(NSString *)placeholder 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 627fd7ae47c7e9..9b0a4d737cd4eb 100644 --- a/Libraries/Text/RCTTextView.m +++ b/Libraries/Text/RCTTextView.m @@ -67,7 +67,6 @@ @implementation RCTTextView NSInteger _nativeEventCount; RCTText *_richTextView; NSAttributedString *_pendingAttributedText; - NSMutableArray *_subviews; BOOL _blockTextShouldChange; UITextRange *_previousSelectionRange; NSUInteger _previousTextLength; @@ -98,7 +97,6 @@ - (instancetype)initWithEventDispatcher:(RCTEventDispatcher *)eventDispatcher _previousSelectionRange = _textView.selectedTextRange; - _subviews = [NSMutableArray new]; [self addSubview:_scrollView]; } return self; @@ -107,19 +105,14 @@ - (instancetype)initWithEventDispatcher:(RCTEventDispatcher *)eventDispatcher 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 @@ -132,23 +125,22 @@ - (void)insertReactSubview:(UIView *)subview atIndex:(NSInteger)index 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 cff7861a1b9833..78896f5fe482e1 100644 --- a/React/Base/RCTRootView.m +++ b/React/Base/RCTRootView.m @@ -337,7 +337,7 @@ - (instancetype)initWithFrame:(CGRect)frame 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 168e115dc82f3d..4ab30f9918b723 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 @@ static void RCTSetChildren(NSNumber *containerTag, 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 @@ static void RCTSetChildren(NSNumber *containerTag, }]; } -- (void)_manageChildren:(NSNumber *)containerReactTag +- (void)_manageChildren:(NSNumber *)containerTag moveFromIndices:(NSArray *)moveFromIndices moveToIndices:(NSArray *)moveToIndices addChildReactTags:(NSArray *)addChildReactTags @@ -929,7 +931,7 @@ - (void)_manageChildren:(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 @@ - (void)_manageChildren:(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 5236f850b7993e..5c9beab09ba96c 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 1b3472530155d0..7a1334668761ca 100644 --- a/React/Views/RCTMap.m +++ b/React/Views/RCTMap.m @@ -23,7 +23,6 @@ @implementation RCTMap { UIView *_legalLabel; CLLocationManager *_locationManager; - NSMutableArray *_reactSubviews; } - (instancetype)init @@ -31,7 +30,6 @@ - (instancetype)init if ((self = [super init])) { _hasStartedRendering = NO; - _reactSubviews = [NSMutableArray new]; // Find Apple link label for (UIView *subview in self.subviews) { @@ -51,19 +49,9 @@ - (void)dealloc [_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 c3140249305873..3dce5ed759910b 100644 --- a/React/Views/RCTModalHostView.m +++ b/React/Views/RCTModalHostView.m @@ -55,14 +55,10 @@ - (void)notifyForBoundsChange:(CGRect)newBounds } } -- (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 @@ - (void)insertReactSubview:(UIView *)subview atIndex:(__unused NSInteger)atIndex - (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 d95089ee461559..eb19b1f6c36579 100644 --- a/React/Views/RCTNavigator.m +++ b/React/Views/RCTNavigator.m @@ -217,7 +217,6 @@ @interface RCTNavigator() *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 @@ - (instancetype)initWithBridge:(RCTBridge *)bridge _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 @@ - (void)setPaused:(BOOL)paused - (void)setInteractivePopGestureEnabled:(BOOL)interactivePopGestureEnabled { _interactivePopGestureEnabled = interactivePopGestureEnabled; - + _navigationController.interactivePopGestureRecognizer.delegate = self; _navigationController.interactivePopGestureRecognizer.enabled = interactivePopGestureEnabled; @@ -402,8 +400,8 @@ - (void)navigationController:(UINavigationController *)navigationController 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 @@ - (BOOL)requestSchedulingJavaScriptNavigation - (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 @@ - (void)insertReactSubview:(RCTNavItem *)view atIndex:(NSInteger)atIndex _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 @@ - (void)layoutSubviews - (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 @@ - (void)dispatchFakeScrollEvent - (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 @@ - (void)reactBridgeDidFinishTransaction 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 @@ - (void)reactBridgeDidFinishTransaction } 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 @@ - (void)reactBridgeDidFinishTransaction return; } - _previousViews = [_currentViews copy]; + _previousViews = [self.reactSubviews copy]; _previousRequestedTopOfStack = _requestedTopOfStack; } diff --git a/React/Views/RCTScrollView.m b/React/Views/RCTScrollView.m index a2d1e5d859cf58..3f2cd82efc057b 100644 --- a/React/Views/RCTScrollView.m +++ b/React/Views/RCTScrollView.m @@ -418,8 +418,9 @@ - (void)setRemoveClippedSubviews:(__unused BOOL)removeClippedSubviews // 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 @@ - (void)insertReactSubview:(UIView *)view atIndex:(__unused NSInteger)atIndex - (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.h b/React/Views/RCTShadowView.h index 67955a9bc2d671..ed3e4298890ec8 100644 --- a/React/Views/RCTShadowView.h +++ b/React/Views/RCTShadowView.h @@ -35,8 +35,13 @@ typedef void (^RCTApplierBlock)(NSDictionary *viewRegistry */ @interface RCTShadowView : NSObject -- (NSArray *)reactSubviews; -- (RCTShadowView *)reactSuperview; +/** + * RCTComponent interface. + */ +- (NSArray *)reactSubviews NS_REQUIRES_SUPER; +- (RCTShadowView *)reactSuperview NS_REQUIRES_SUPER; +- (void)insertReactSubview:(RCTShadowView *)subview atIndex:(NSInteger)atIndex NS_REQUIRES_SUPER; +- (void)removeReactSubview:(RCTShadowView *)subview NS_REQUIRES_SUPER; @property (nonatomic, weak, readonly) RCTShadowView *superview; @property (nonatomic, assign, readonly) css_node_t *cssNode; @@ -181,6 +186,10 @@ typedef void (^RCTApplierBlock)(NSDictionary *viewRegistry - (void)setTextComputed NS_REQUIRES_SUPER; - (BOOL)isTextDirty; +/** + * As described in RCTComponent protocol. + */ +- (void)didUpdateReactSubviews NS_REQUIRES_SUPER; - (void)didSetProps:(NSArray *)changedProps NS_REQUIRES_SUPER; /** diff --git a/React/Views/RCTShadowView.m b/React/Views/RCTShadowView.m index 83ce22a5beb1fe..d257e96594e93b 100644 --- a/React/Views/RCTShadowView.m +++ b/React/Views/RCTShadowView.m @@ -596,6 +596,11 @@ - (void)setBackgroundColor:(UIColor *)color [self dirtyPropagation]; } +- (void)didUpdateReactSubviews +{ + // Does nothing by default +} + - (void)didSetProps:(__unused NSArray *)changedProps { if (_recomputePadding) { diff --git a/React/Views/RCTTabBar.m b/React/Views/RCTTabBar.m index 6f31f69985e20c..d544d75b2efa48 100644 --- a/React/Views/RCTTabBar.m +++ b/React/Views/RCTTabBar.m @@ -26,13 +26,11 @@ @implementation RCTTabBar { 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 @@ - (void)dealloc [_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 @@ - (void)reactBridgeDidFinishTransaction _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 @@ - (void)setItemPositioning:(UITabBarItemPositioning)itemPositioning - (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 ad2ea1424bbdaf..4d1b362ae4ff31 100644 --- a/React/Views/RCTView.m +++ b/React/Views/RCTView.m @@ -56,8 +56,7 @@ - (UIView *)react_findClipView UIView *clipView = nil; CGRect clipRect = self.bounds; // We will only look for a clipping view up the view hierarchy until we hit the root view. - BOOL passedRootView = NO; - while (testView && !passedRootView) { + while (testView) { if (testView.clipsToBounds) { if (clipView) { CGRect testRect = [clipView convertRect:clipRect toView:testView]; @@ -71,7 +70,7 @@ - (UIView *)react_findClipView } } if ([testView isReactRootView]) { - passedRootView = YES; + break; } testView = testView.superview; } @@ -97,7 +96,6 @@ - (UIView *)react_findClipView @implementation RCTView { - NSMutableArray *_reactSubviews; UIColor *_backgroundColor; } @@ -275,76 +273,31 @@ + (UIEdgeInsets)contentInsetsForView:(UIView *)view - (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,61 +313,44 @@ - (void)react_updateClippedSubviewsWithClipRect:(CGRect)clipRect relativeToView: 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))) { -- (void)setRemoveClippedSubviews:(BOOL)removeClippedSubviews -{ - if (removeClippedSubviews && !_reactSubviews) { - _reactSubviews = [self.subviews mutableCopy]; - } else if (!removeClippedSubviews && _reactSubviews) { - [self react_remountAllSubviews]; - _reactSubviews = nil; - } -} - -- (BOOL)removeClippedSubviews -{ - return _reactSubviews != nil; -} - -- (void)insertReactSubview:(UIView *)view atIndex:(NSInteger)atIndex -{ - if (_reactSubviews == nil) { - [self insertSubview:view atIndex:atIndex]; - } else { - [_reactSubviews insertObject:view atIndex:atIndex]; - - // Find a suitable view to use for clipping - UIView *clipView = [self react_findClipView]; - if (clipView) { + // View is at least partially visible, so remount it if unmounted + [self addSubview:view]; - // If possible, don't add subviews if they are clipped - [self mountOrUnmountSubview:view withClipRect:clipView.bounds relativeToView:clipView]; + // 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 { + } else if (view.superview) { - // Fallback if we can't find a suitable clipView - [self remountSubview:view]; + // View is completely outside the clipRect, so unmount it + [view removeFromSuperview]; } } } -- (void)removeReactSubview:(UIView *)subview +- (void)setRemoveClippedSubviews:(BOOL)removeClippedSubviews { - [_reactSubviews removeObject:subview]; - [subview removeFromSuperview]; + if (!removeClippedSubviews && _removeClippedSubviews) { + [self react_remountAllSubviews]; + } + _removeClippedSubviews = removeClippedSubviews; } -- (NSArray *)reactSubviews +- (void)didUpdateReactSubviews { - // 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; + if (_removeClippedSubviews) { + [self updateClippedSubviews]; + } else { + [super didUpdateReactSubviews]; + } } - (void)updateClippedSubviews @@ -435,7 +371,7 @@ - (void)layoutSubviews [super layoutSubviews]; - if (_reactSubviews) { + if (_removeClippedSubviews) { [self updateClippedSubviews]; } } diff --git a/React/Views/RCTViewManager.h b/React/Views/RCTViewManager.h index df532c1eb7aac7..d963e2b4b03217 100644 --- a/React/Views/RCTViewManager.h +++ b/React/Views/RCTViewManager.h @@ -11,10 +11,10 @@ #import "RCTBridgeModule.h" #import "RCTConvert.h" -#import "RCTComponent.h" #import "RCTDefines.h" #import "RCTEventDispatcher.h" #import "RCTLog.h" +#import "UIView+React.h" @class RCTBridge; @class RCTShadowView; diff --git a/React/Views/RCTViewManager.m b/React/Views/RCTViewManager.m index eeea7cbce0d211..899b3f0317801e 100644 --- a/React/Views/RCTViewManager.m +++ b/React/Views/RCTViewManager.m @@ -115,8 +115,8 @@ - (RCTViewManagerUIBlock)uiBlockToAmendWithShadowViewRegistry:(__unused NSDictio RCT_REMAP_VIEW_PROPERTY(testID, accessibilityIdentifier, NSString) RCT_REMAP_VIEW_PROPERTY(backfaceVisibility, layer.doubleSided, css_backface_visibility_t) RCT_REMAP_VIEW_PROPERTY(opacity, alpha, CGFloat) -RCT_REMAP_VIEW_PROPERTY(shadowColor, layer.shadowColor, CGColor); -RCT_REMAP_VIEW_PROPERTY(shadowOffset, layer.shadowOffset, CGSize); +RCT_REMAP_VIEW_PROPERTY(shadowColor, layer.shadowColor, CGColor) +RCT_REMAP_VIEW_PROPERTY(shadowOffset, layer.shadowOffset, CGSize) RCT_REMAP_VIEW_PROPERTY(shadowOpacity, layer.shadowOpacity, float) RCT_REMAP_VIEW_PROPERTY(shadowRadius, layer.shadowRadius, CGFloat) RCT_REMAP_VIEW_PROPERTY(overflow, clipsToBounds, css_clip_t) diff --git a/React/Views/UIView+React.h b/React/Views/UIView+React.h index 794c977e9e7413..fa779e5208a821 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 e7736f3a125651..f25c716447741a 100644 --- a/React/Views/UIView+React.m +++ b/React/Views/UIView+React.m @@ -56,25 +56,42 @@ - (NSNumber *)reactTagAtPoint:(CGPoint)point return view.reactTag; } -- (void)insertReactSubview:(UIView *)subview atIndex:(NSInteger)atIndex +- (NSArray *)reactSubviews { - [self insertSubview:subview atIndex:atIndex]; + return objc_getAssociatedObject(self, _cmd); } -- (void)removeReactSubview:(UIView *)subview +- (UIView *)reactSuperview { - RCTAssert(subview.superview == self, @"%@ is a not a subview of %@", subview, self); - [subview removeFromSuperview]; + return self.superview; } -- (NSArray *)reactSubviews +- (void)insertReactSubview:(UIView *)subview atIndex:(NSInteger)atIndex { - return self.subviews; + // We access the associated object directly here in case someone overrides + // the `reactSubviews` getter method and returns an immutable array. + NSMutableArray *subviews = objc_getAssociatedObject(self, @selector(reactSubviews)); + if (!subviews) { + subviews = [NSMutableArray new]; + objc_setAssociatedObject(self, @selector(reactSubviews), subviews, OBJC_ASSOCIATION_RETAIN_NONATOMIC); + } + [subviews insertObject:subview atIndex:atIndex]; } -- (UIView *)reactSuperview +- (void)removeReactSubview:(UIView *)subview { - return self.superview; + // We access the associated object directly here in case someone overrides + // the `reactSubviews` getter method and returns an immutable array. + NSMutableArray *subviews = objc_getAssociatedObject(self, @selector(reactSubviews)); + [subviews removeObject:subview]; + [subview removeFromSuperview]; +} + +- (void)didUpdateReactSubviews +{ + for (UIView *subview in self.reactSubviews) { + [self addSubview:subview]; + } } - (void)reactSetFrame:(CGRect)frame