Skip to content

Commit

Permalink
Improve z-index implementation on iOS
Browse files Browse the repository at this point in the history
  • Loading branch information
janicduplessis committed May 22, 2017
1 parent af94987 commit a7ad964
Show file tree
Hide file tree
Showing 6 changed files with 29 additions and 50 deletions.
1 change: 0 additions & 1 deletion React/Views/RCTShadowView.m
Original file line number Diff line number Diff line change
Expand Up @@ -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];
}];
}
Expand Down
7 changes: 0 additions & 7 deletions React/Views/RCTView.h
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
11 changes: 6 additions & 5 deletions React/Views/RCTView.m
Original file line number Diff line number Diff line change
Expand Up @@ -105,8 +105,6 @@ @implementation RCTView
UIColor *_backgroundColor;
}

@synthesize reactZIndex = _reactZIndex;

- (instancetype)initWithFrame:(CGRect)frame
{
if ((self = [super initWithFrame:frame])) {
Expand Down Expand Up @@ -172,13 +170,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) {
Expand Down Expand Up @@ -294,7 +295,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];
Expand Down Expand Up @@ -333,7 +334,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
Expand Down
3 changes: 0 additions & 3 deletions React/Views/UIView+Private.h
Original file line number Diff line number Diff line change
Expand Up @@ -16,7 +16,4 @@
- (void)react_updateClippedSubviewsWithClipRect:(CGRect)clipRect relativeToView:(UIView *)clipView;
- (UIView *)react_findClipView;

// zIndex sorting
- (void)clearSortedSubviews;

@end
8 changes: 4 additions & 4 deletions React/Views/UIView+React.h
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
49 changes: 19 additions & 30 deletions React/Views/UIView+React.m
Original file line number Diff line number Diff line change
Expand Up @@ -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];
}
}
Expand Down

0 comments on commit a7ad964

Please sign in to comment.