Skip to content

Commit d64368b

Browse files
nicklockwoodFacebook Github Bot 8
authored and
Facebook Github Bot 8
committed
Implement CSS z-index for iOS
Summary: This diff implement the CSS z-index for React Native iOS views. We've had numerous pull request for this feature, but they've all attempted to use the `layer.zPosition` property, which is problematic for two reasons: 1. zPosition only affects rendering order, not event processing order. Views with a higher zPosition will appear in front of others in the hierarchy, but won't be the first to receive touch events, and may be blocked by views that are visually behind them. 2. when using a perspective transform matrix, views with a nonzero zPosition will be rendered in a different position due to parallax, which probably isn't desirable. See #7825 for further discussion of this problem. So instead of using `layer.zPosition`, I've implemented this by actually adjusting the order of the subviews within their parent based on the zIndex. This can't be done on the JS side because it would affect layout, which is order-dependent, so I'm doing it inside the view itself. It works as follows: 1. The `reactSubviews` array is set, whose order matches the order of the JS components and shadowView components, as specified by the UIManager. 2. `didUpdateReactSubviews` is called, which in turn calls `sortedSubviews` (which lazily generates a sorted array of `reactSubviews` by zIndex) and inserts the result into the view. 3. If a subview is added or removed, or the zIndex of any subview is changed, the previous `sortedSubviews` array is cleared and `didUpdateReactSubviews` is called again. To demonstrate it working, I've modified the UIExplorer example from #7825 Reviewed By: javache Differential Revision: D3365717 fbshipit-source-id: b34aa8bfad577bce023f8af5414f9b974aafd8aa
1 parent 993a928 commit d64368b

11 files changed

+173
-10
lines changed

Examples/UIExplorer/ViewExample.js

+66-1
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,11 @@
11
/**
2+
* Copyright (c) 2013-present, Facebook, Inc.
3+
* All rights reserved.
4+
*
5+
* This source code is licensed under the BSD-style license found in the
6+
* LICENSE file in the root directory of this source tree. An additional grant
7+
* of patent rights can be found in the PATENTS file in the same directory.
8+
*
29
* The examples provided by Facebook are for non-commercial testing and
310
* evaluation purposes only.
411
*
@@ -30,7 +37,13 @@ var styles = StyleSheet.create({
3037
backgroundColor: '#527FE4',
3138
borderColor: '#000033',
3239
borderWidth: 1,
33-
}
40+
},
41+
zIndex: {
42+
justifyContent: 'space-around',
43+
width: 100,
44+
height: 50,
45+
marginTop: -10,
46+
},
3447
});
3548

3649
var ViewBorderStyleExample = React.createClass({
@@ -74,6 +87,53 @@ var ViewBorderStyleExample = React.createClass({
7487
}
7588
});
7689

90+
var ZIndexExample = React.createClass({
91+
getInitialState() {
92+
return {
93+
flipped: false
94+
};
95+
},
96+
97+
render() {
98+
const indices = this.state.flipped ? [-1, 0, 1, 2] : [2, 1, 0, -1];
99+
return (
100+
<TouchableWithoutFeedback onPress={this._handlePress}>
101+
<View>
102+
<Text style={{paddingBottom: 10}}>Tap to flip sorting order</Text>
103+
<View style={[
104+
styles.zIndex,
105+
{marginTop: 0, backgroundColor: '#E57373', zIndex: indices[0]}
106+
]}>
107+
<Text>ZIndex {indices[0]}</Text>
108+
</View>
109+
<View style={[
110+
styles.zIndex,
111+
{marginLeft: 50, backgroundColor: '#FFF176', zIndex: indices[1]}
112+
]}>
113+
<Text>ZIndex {indices[1]}</Text>
114+
</View>
115+
<View style={[
116+
styles.zIndex,
117+
{marginLeft: 100, backgroundColor: '#81C784', zIndex: indices[2]}
118+
]}>
119+
<Text>ZIndex {indices[2]}</Text>
120+
</View>
121+
<View style={[
122+
styles.zIndex,
123+
{marginLeft: 150, backgroundColor: '#64B5F6', zIndex: indices[3]}
124+
]}>
125+
<Text>ZIndex {indices[3]}</Text>
126+
</View>
127+
</View>
128+
</TouchableWithoutFeedback>
129+
);
130+
},
131+
132+
_handlePress() {
133+
this.setState({flipped: !this.state.flipped});
134+
}
135+
});
136+
77137
exports.title = '<View>';
78138
exports.description = 'Basic building block of all UI, examples that ' +
79139
'demonstrate some of the many styles available.';
@@ -188,5 +248,10 @@ exports.examples = [
188248
</View>
189249
);
190250
},
251+
}, {
252+
title: 'ZIndex',
253+
render: function() {
254+
return <ZIndexExample />;
255+
},
191256
},
192257
];

Libraries/StyleSheet/LayoutPropTypes.js

+3
Original file line numberDiff line numberDiff line change
@@ -100,6 +100,9 @@ var LayoutPropTypes = {
100100

101101
// https://developer.mozilla.org/en-US/docs/Web/CSS/flex
102102
flex: ReactPropTypes.number,
103+
104+
// https://developer.mozilla.org/en-US/docs/Web/CSS/z-index
105+
zIndex: ReactPropTypes.number,
103106
};
104107

105108
module.exports = LayoutPropTypes;

React/Modules/RCTUIManager.m

-4
Original file line numberDiff line numberDiff line change
@@ -893,8 +893,6 @@ static void RCTSetChildren(NSNumber *containerTag,
893893
[container insertReactSubview:view atIndex:index++];
894894
}
895895
}
896-
897-
[container didUpdateReactSubviews];
898896
}
899897

900898
RCT_EXPORT_METHOD(manageChildren:(nonnull NSNumber *)containerTag
@@ -965,8 +963,6 @@ - (void)_manageChildren:(NSNumber *)containerTag
965963
[container insertReactSubview:destinationsToChildrenToAdd[reactIndex]
966964
atIndex:reactIndex.integerValue];
967965
}
968-
969-
[container didUpdateReactSubviews];
970966
}
971967

972968
RCT_EXPORT_METHOD(createView:(nonnull NSNumber *)reactTag

React/Views/RCTShadowView.h

+5
Original file line numberDiff line numberDiff line change
@@ -129,6 +129,11 @@ typedef void (^RCTApplierBlock)(NSDictionary<NSNumber *, UIView *> *viewRegistry
129129
@property (nonatomic, assign) css_wrap_type_t flexWrap;
130130
@property (nonatomic, assign) CGFloat flex;
131131

132+
/**
133+
* z-index, used to override sibling order in the view
134+
*/
135+
@property (nonatomic, assign) double zIndex;
136+
132137
/**
133138
* Calculate property changes that need to be propagated to the view.
134139
* The applierBlocks set contains RCTApplierBlock functions that must be applied

React/Views/RCTShadowView.m

+24
Original file line numberDiff line numberDiff line change
@@ -13,6 +13,7 @@
1313
#import "RCTLog.h"
1414
#import "RCTUtils.h"
1515
#import "UIView+React.h"
16+
#import "UIView+Private.h"
1617

1718
typedef void (^RCTActionBlock)(RCTShadowView *shadowViewSelf, id value);
1819
typedef void (^RCTResetActionBlock)(RCTShadowView *shadowViewSelf);
@@ -39,6 +40,7 @@ @implementation RCTShadowView
3940
BOOL _recomputePadding;
4041
BOOL _recomputeMargin;
4142
BOOL _recomputeBorder;
43+
BOOL _didUpdateSubviews;
4244
float _paddingMetaProps[META_PROP_COUNT];
4345
float _marginMetaProps[META_PROP_COUNT];
4446
float _borderMetaProps[META_PROP_COUNT];
@@ -179,6 +181,16 @@ - (void)applyLayoutToChildren:(css_node_t *)node
179181
// dirtied, but really we should track which properties have changed and
180182
// only update those.
181183

184+
if (_didUpdateSubviews) {
185+
_didUpdateSubviews = NO;
186+
[self didUpdateReactSubviews];
187+
[applierBlocks addObject:^(NSDictionary<NSNumber *, UIView *> *viewRegistry) {
188+
UIView *view = viewRegistry[_reactTag];
189+
[view clearSortedSubviews];
190+
[view didUpdateReactSubviews];
191+
}];
192+
}
193+
182194
if (!_backgroundColor) {
183195
UIColor *parentBackgroundColor = parentProperties[RCTBackgroundColorProp];
184196
if (parentBackgroundColor) {
@@ -351,6 +363,7 @@ - (void)insertReactSubview:(RCTShadowView *)subview atIndex:(NSInteger)atIndex
351363
[_reactSubviews insertObject:subview atIndex:atIndex];
352364
_cssNode->children_count = (int)_reactSubviews.count;
353365
subview->_superview = self;
366+
_didUpdateSubviews = YES;
354367
[self dirtyText];
355368
[self dirtyLayout];
356369
[self dirtyPropagation];
@@ -361,6 +374,7 @@ - (void)removeReactSubview:(RCTShadowView *)subview
361374
[subview dirtyText];
362375
[subview dirtyLayout];
363376
[subview dirtyPropagation];
377+
_didUpdateSubviews = YES;
364378
subview->_superview = nil;
365379
[_reactSubviews removeObject:subview];
366380
_cssNode->children_count = (int)_reactSubviews.count;
@@ -596,6 +610,16 @@ - (void)setBackgroundColor:(UIColor *)color
596610
[self dirtyPropagation];
597611
}
598612

613+
- (void)setZIndex:(double)zIndex
614+
{
615+
_zIndex = zIndex;
616+
if (_superview) {
617+
// Changing zIndex means the subview order of the parent needs updating
618+
_superview->_didUpdateSubviews = YES;
619+
[_superview dirtyPropagation];
620+
}
621+
}
622+
599623
- (void)didUpdateReactSubviews
600624
{
601625
// Does nothing by default

React/Views/RCTView.h

+7
Original file line numberDiff line numberDiff line change
@@ -41,6 +41,13 @@
4141
*/
4242
+ (UIEdgeInsets)contentInsetsForView:(UIView *)curView;
4343

44+
/**
45+
* z-index, used to override sibling order in didUpdateReactSubviews. This is
46+
* inherited from UIView+React, but we override it here to reduce the boxing
47+
* and associated object overheads.
48+
*/
49+
@property (nonatomic, assign) double reactZIndex;
50+
4451
/**
4552
* This is an optimization used to improve performance
4653
* for large scrolling views with many subviews, such as a

React/Views/RCTView.m

+4-2
Original file line numberDiff line numberDiff line change
@@ -99,6 +99,8 @@ @implementation RCTView
9999
UIColor *_backgroundColor;
100100
}
101101

102+
@synthesize reactZIndex = _reactZIndex;
103+
102104
- (instancetype)initWithFrame:(CGRect)frame
103105
{
104106
if ((self = [super initWithFrame:frame])) {
@@ -274,7 +276,7 @@ + (UIEdgeInsets)contentInsetsForView:(UIView *)view
274276
- (void)react_remountAllSubviews
275277
{
276278
if (_removeClippedSubviews) {
277-
for (UIView *view in self.reactSubviews) {
279+
for (UIView *view in self.sortedReactSubviews) {
278280
if (view.superview != self) {
279281
[self addSubview:view];
280282
[view react_remountAllSubviews];
@@ -313,7 +315,7 @@ - (void)react_updateClippedSubviewsWithClipRect:(CGRect)clipRect relativeToView:
313315
clipView = self;
314316

315317
// Mount / unmount views
316-
for (UIView *view in self.reactSubviews) {
318+
for (UIView *view in self.sortedReactSubviews) {
317319
if (!CGRectIsEmpty(CGRectIntersection(clipRect, view.frame))) {
318320

319321
// View is at least partially visible, so remount it if unmounted

React/Views/RCTViewManager.m

+4
Original file line numberDiff line numberDiff line change
@@ -246,6 +246,8 @@ - (RCTViewManagerUIBlock)uiBlockToAmendWithShadowViewRegistry:(__unused NSDictio
246246
RCT_VIEW_BORDER_RADIUS_PROPERTY(BottomLeft)
247247
RCT_VIEW_BORDER_RADIUS_PROPERTY(BottomRight)
248248

249+
RCT_REMAP_VIEW_PROPERTY(zIndex, reactZIndex, double)
250+
249251
#pragma mark - ShadowView properties
250252

251253
RCT_EXPORT_SHADOW_PROPERTY(backgroundColor, UIColor)
@@ -290,4 +292,6 @@ - (RCTViewManagerUIBlock)uiBlockToAmendWithShadowViewRegistry:(__unused NSDictio
290292

291293
RCT_EXPORT_SHADOW_PROPERTY(onLayout, RCTDirectEventBlock)
292294

295+
RCT_EXPORT_SHADOW_PROPERTY(zIndex, double)
296+
293297
@end

React/Views/UIView+Private.h

+5-1
Original file line numberDiff line numberDiff line change
@@ -9,10 +9,14 @@
99

1010
#import <UIKit/UIKit.h>
1111

12-
@interface UIView (RCTViewUnmounting)
12+
@interface UIView (Private)
1313

14+
// remove clipped subviews implementation
1415
- (void)react_remountAllSubviews;
1516
- (void)react_updateClippedSubviewsWithClipRect:(CGRect)clipRect relativeToView:(UIView *)clipView;
1617
- (UIView *)react_findClipView;
1718

19+
// zIndex sorting
20+
- (void)clearSortedSubviews;
21+
1822
@end

React/Views/UIView+React.h

+12-1
Original file line numberDiff line numberDiff line change
@@ -25,9 +25,20 @@
2525
- (void)insertReactSubview:(UIView *)subview atIndex:(NSInteger)atIndex NS_REQUIRES_SUPER;
2626
- (void)removeReactSubview:(UIView *)subview NS_REQUIRES_SUPER;
2727

28+
/**
29+
* z-index, used to override sibling order in didUpdateReactSubviews.
30+
*/
31+
@property (nonatomic, assign) double reactZIndex;
32+
33+
/**
34+
* The reactSubviews array, sorted by zIndex. This value is cached and
35+
* automatically recalculated if views are added or removed.
36+
*/
37+
@property (nonatomic, copy, readonly) NSArray<UIView *> *sortedReactSubviews;
38+
2839
/**
2940
* Updates the subviews array based on the reactSubviews. Default behavior is
30-
* to insert the reactSubviews into the UIView.
41+
* to insert the sortedReactSubviews into the UIView.
3142
*/
3243
- (void)didUpdateReactSubviews;
3344

React/Views/UIView+React.m

+43-1
Original file line numberDiff line numberDiff line change
@@ -87,9 +87,51 @@ - (void)removeReactSubview:(UIView *)subview
8787
[subview removeFromSuperview];
8888
}
8989

90+
- (double)reactZIndex
91+
{
92+
return [objc_getAssociatedObject(self, _cmd) doubleValue];
93+
}
94+
95+
- (void)setReactZIndex:(double)reactZIndex
96+
{
97+
objc_setAssociatedObject(self, @selector(reactZIndex), @(reactZIndex), OBJC_ASSOCIATION_RETAIN_NONATOMIC);
98+
}
99+
100+
- (NSArray<UIView *> *)sortedReactSubviews
101+
{
102+
NSArray *subviews = objc_getAssociatedObject(self, _cmd);
103+
if (!subviews) {
104+
// Check if sorting is required - in most cases it won't be
105+
BOOL sortingRequired = NO;
106+
for (UIView *subview in self.reactSubviews) {
107+
if (subview.reactZIndex != 0) {
108+
sortingRequired = YES;
109+
break;
110+
}
111+
}
112+
subviews = sortingRequired ? [self.reactSubviews sortedArrayUsingComparator:^NSComparisonResult(UIView *a, UIView *b) {
113+
if (a.reactZIndex > b.reactZIndex) {
114+
return NSOrderedDescending;
115+
} else {
116+
// ensure sorting is stable by treating equal zIndex as ascending so
117+
// that original order is preserved
118+
return NSOrderedAscending;
119+
}
120+
}] : self.reactSubviews;
121+
objc_setAssociatedObject(self, _cmd, subviews, OBJC_ASSOCIATION_RETAIN_NONATOMIC);
122+
}
123+
return subviews;
124+
}
125+
126+
// private method, used to reset sort
127+
- (void)clearSortedSubviews
128+
{
129+
objc_setAssociatedObject(self, @selector(sortedReactSubviews), nil, OBJC_ASSOCIATION_RETAIN_NONATOMIC);
130+
}
131+
90132
- (void)didUpdateReactSubviews
91133
{
92-
for (UIView *subview in self.reactSubviews) {
134+
for (UIView *subview in self.sortedReactSubviews) {
93135
[self addSubview:subview];
94136
}
95137
}

0 commit comments

Comments
 (0)