Permalink
Browse files

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 #14011

Differential Revision: D5108437

Pulled By: shergin

fbshipit-source-id: 0abfe85666e9d236a190e6f54cdd5453cacfbcac
  • Loading branch information...
janicduplessis authored and facebook-github-bot committed May 29, 2017
1 parent 7807247 commit 1658f36630f0f3ac2dce96af872530600c3d9d17
@@ -232,7 +232,6 @@ - (void)applyLayoutToChildren:(YGNodeRef)node
[self didUpdateReactSubviews];
[applierBlocks addObject:^(NSDictionary<NSNumber *, UIView *> *viewRegistry) {
UIView *view = viewRegistry[self->_reactTag];
[view clearSortedSubviews];
[view didUpdateReactSubviews];
}];
}
View
@@ -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
View
@@ -102,8 +102,6 @@ @implementation RCTView
UIColor *_backgroundColor;
}
@synthesize reactZIndex = _reactZIndex;
- (instancetype)initWithFrame:(CGRect)frame
{
if ((self = [super initWithFrame:frame])) {
@@ -169,13 +167,16 @@ - (UIView *)hitTest:(CGPoint)point withEvent:(UIEvent *)event
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<UIView *> *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 @@ + (UIEdgeInsets)contentInsetsForView:(UIView *)view
- (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 @@ - (void)react_updateClippedSubviewsWithClipRect:(CGRect)clipRect relativeToView:
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
@@ -16,7 +16,4 @@
- (void)react_updateClippedSubviewsWithClipRect:(CGRect)clipRect relativeToView:(UIView *)clipView;
- (UIView *)react_findClipView;
// zIndex sorting
- (void)clearSortedSubviews;
@end
@@ -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<UIView *> *sortedReactSubviews;
- (NSArray<UIView *> *)reactZIndexSortedSubviews;
/**
* Updates the subviews array based on the reactSubviews. Default behavior is
View
@@ -110,49 +110,38 @@ - (void)setReactLayoutDirection:(UIUserInterfaceLayoutDirection)layoutDirection
- (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<UIView *> *)sortedReactSubviews
- (NSArray<UIView *> *)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];
}
}

0 comments on commit 1658f36

Please sign in to comment.