From 1658f36630f0f3ac2dce96af872530600c3d9d17 Mon Sep 17 00:00:00 2001 From: Janic Duplessis Date: Sun, 28 May 2017 21:35:38 -0700 Subject: [PATCH] Improve z-index implementation on iOS Summary: This avoids reordering views because it created some bugs when the native hierarchy is different from the shadow views. This leverages `layer.zPosition` and takes z-index in consideration when we check what view should be the target of a touch. **Test plan** Tested that this fixes some layout issues that occurred when using sticky headers in the Expo home screen. Closes https://github.com/facebook/react-native/pull/14011 Differential Revision: D5108437 Pulled By: shergin fbshipit-source-id: 0abfe85666e9d236a190e6f54cdd5453cacfbcac --- React/Views/RCTShadowView.m | 1 - React/Views/RCTView.h | 7 ------ React/Views/RCTView.m | 11 ++++---- React/Views/UIView+Private.h | 3 --- React/Views/UIView+React.h | 8 +++--- React/Views/UIView+React.m | 49 ++++++++++++++---------------------- 6 files changed, 29 insertions(+), 50 deletions(-) diff --git a/React/Views/RCTShadowView.m b/React/Views/RCTShadowView.m index d7b431fd4..61b992cb1 100644 --- a/React/Views/RCTShadowView.m +++ b/React/Views/RCTShadowView.m @@ -232,7 +232,6 @@ static void RCTProcessMetaPropsBorder(const YGValue metaProps[META_PROP_COUNT], [self didUpdateReactSubviews]; [applierBlocks addObject:^(NSDictionary *viewRegistry) { UIView *view = viewRegistry[self->_reactTag]; - [view clearSortedSubviews]; [view didUpdateReactSubviews]; }]; } diff --git a/React/Views/RCTView.h b/React/Views/RCTView.h index bf8e0b43f..ddf85dd55 100644 --- a/React/Views/RCTView.h +++ b/React/Views/RCTView.h @@ -47,13 +47,6 @@ */ @property (nonatomic, assign) UIUserInterfaceLayoutDirection reactLayoutDirection; -/** - * z-index, used to override sibling order in didUpdateReactSubviews. This is - * inherited from UIView+React, but we override it here to reduce the boxing - * and associated object overheads. - */ -@property (nonatomic, assign) NSInteger reactZIndex; - /** * This is an optimization used to improve performance * for large scrolling views with many subviews, such as a diff --git a/React/Views/RCTView.m b/React/Views/RCTView.m index 204e41dd7..471edcd8a 100644 --- a/React/Views/RCTView.m +++ b/React/Views/RCTView.m @@ -102,8 +102,6 @@ static NSString *RCTRecursiveAccessibilityLabel(UIView *view) UIColor *_backgroundColor; } -@synthesize reactZIndex = _reactZIndex; - - (instancetype)initWithFrame:(CGRect)frame { if ((self = [super initWithFrame:frame])) { @@ -169,13 +167,16 @@ RCT_NOT_IMPLEMENTED(- (instancetype)initWithCoder:unused) BOOL isPointInside = [self pointInside:point withEvent:event]; BOOL needsHitSubview = !(_pointerEvents == RCTPointerEventsNone || _pointerEvents == RCTPointerEventsBoxOnly); if (needsHitSubview && (![self clipsToBounds] || isPointInside)) { + // Take z-index into account when calculating the touch target. + NSArray *sortedSubviews = [self reactZIndexSortedSubviews]; + // The default behaviour of UIKit is that if a view does not contain a point, // then no subviews will be returned from hit testing, even if they contain // the hit point. By doing hit testing directly on the subviews, we bypass // the strict containment policy (i.e., UIKit guarantees that every ancestor // of the hit view will return YES from -pointInside:withEvent:). See: // - https://developer.apple.com/library/ios/qa/qa2013/qa1812.html - for (UIView *subview in [self.subviews reverseObjectEnumerator]) { + for (UIView *subview in [sortedSubviews reverseObjectEnumerator]) { CGPoint convertedPoint = [subview convertPoint:point fromView:self]; hitSubview = [subview hitTest:convertedPoint withEvent:event]; if (hitSubview != nil) { @@ -291,7 +292,7 @@ RCT_NOT_IMPLEMENTED(- (instancetype)initWithCoder:unused) - (void)react_remountAllSubviews { if (_removeClippedSubviews) { - for (UIView *view in self.sortedReactSubviews) { + for (UIView *view in self.reactSubviews) { if (view.superview != self) { [self addSubview:view]; [view react_remountAllSubviews]; @@ -330,7 +331,7 @@ RCT_NOT_IMPLEMENTED(- (instancetype)initWithCoder:unused) clipView = self; // Mount / unmount views - for (UIView *view in self.sortedReactSubviews) { + for (UIView *view in self.reactSubviews) { if (!CGRectIsEmpty(CGRectIntersection(clipRect, view.frame))) { // View is at least partially visible, so remount it if unmounted diff --git a/React/Views/UIView+Private.h b/React/Views/UIView+Private.h index 057c46293..078f16e07 100644 --- a/React/Views/UIView+Private.h +++ b/React/Views/UIView+Private.h @@ -16,7 +16,4 @@ - (void)react_updateClippedSubviewsWithClipRect:(CGRect)clipRect relativeToView:(UIView *)clipView; - (UIView *)react_findClipView; -// zIndex sorting -- (void)clearSortedSubviews; - @end diff --git a/React/Views/UIView+React.h b/React/Views/UIView+React.h index 45fde2cce..3c7f2f18b 100644 --- a/React/Views/UIView+React.h +++ b/React/Views/UIView+React.h @@ -31,15 +31,15 @@ @property (nonatomic, assign) UIUserInterfaceLayoutDirection reactLayoutDirection; /** - * z-index, used to override sibling order in didUpdateReactSubviews. + * The z-index of the view. */ @property (nonatomic, assign) NSInteger reactZIndex; /** - * The reactSubviews array, sorted by zIndex. This value is cached and - * automatically recalculated if views are added or removed. + * Subviews sorted by z-index. Note that this method doesn't do any caching (yet) + * and sorts all the views each call. */ -@property (nonatomic, copy, readonly) NSArray *sortedReactSubviews; +- (NSArray *)reactZIndexSortedSubviews; /** * Updates the subviews array based on the reactSubviews. Default behavior is diff --git a/React/Views/UIView+React.m b/React/Views/UIView+React.m index 8aa52fdfd..a74ffc2ed 100644 --- a/React/Views/UIView+React.m +++ b/React/Views/UIView+React.m @@ -110,49 +110,38 @@ - (NSInteger)reactZIndex { - return [objc_getAssociatedObject(self, _cmd) integerValue]; + return self.layer.zPosition; } - (void)setReactZIndex:(NSInteger)reactZIndex { - objc_setAssociatedObject(self, @selector(reactZIndex), @(reactZIndex), OBJC_ASSOCIATION_RETAIN_NONATOMIC); + self.layer.zPosition = reactZIndex; } -- (NSArray *)sortedReactSubviews +- (NSArray *)reactZIndexSortedSubviews { - NSArray *subviews = objc_getAssociatedObject(self, _cmd); - if (!subviews) { - // Check if sorting is required - in most cases it won't be - BOOL sortingRequired = NO; - for (UIView *subview in self.reactSubviews) { - if (subview.reactZIndex != 0) { - sortingRequired = YES; - break; - } + // Check if sorting is required - in most cases it won't be. + BOOL sortingRequired = NO; + for (UIView *subview in self.subviews) { + if (subview.reactZIndex != 0) { + sortingRequired = YES; + break; } - subviews = sortingRequired ? [self.reactSubviews sortedArrayUsingComparator:^NSComparisonResult(UIView *a, UIView *b) { - if (a.reactZIndex > b.reactZIndex) { - return NSOrderedDescending; - } else { - // ensure sorting is stable by treating equal zIndex as ascending so - // that original order is preserved - return NSOrderedAscending; - } - }] : self.reactSubviews; - objc_setAssociatedObject(self, _cmd, subviews, OBJC_ASSOCIATION_RETAIN_NONATOMIC); } - return subviews; -} - -// private method, used to reset sort -- (void)clearSortedSubviews -{ - objc_setAssociatedObject(self, @selector(sortedReactSubviews), nil, OBJC_ASSOCIATION_RETAIN_NONATOMIC); + return sortingRequired ? [self.reactSubviews sortedArrayUsingComparator:^NSComparisonResult(UIView *a, UIView *b) { + if (a.reactZIndex > b.reactZIndex) { + return NSOrderedDescending; + } else { + // Ensure sorting is stable by treating equal zIndex as ascending so + // that original order is preserved. + return NSOrderedAscending; + } + }] : self.subviews; } - (void)didUpdateReactSubviews { - for (UIView *subview in self.sortedReactSubviews) { + for (UIView *subview in self.reactSubviews) { [self addSubview:subview]; } }