Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Set background color directly to view if not layer backed #1588

Merged
merged 8 commits into from
Jul 20, 2019
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
56 changes: 46 additions & 10 deletions Source/Private/ASDisplayNode+UIViewBridge.mm
Original file line number Diff line number Diff line change
Expand Up @@ -511,17 +511,32 @@ - (void)layoutIfNeeded
- (BOOL)isOpaque
{
_bridge_prologue_read;
return _getFromLayer(opaque);
return _getFromViewOrLayer(opaque, opaque);
}


- (void)setOpaque:(BOOL)newOpaque
{
_bridge_prologue_write;

BOOL shouldApply = ASDisplayNodeShouldApplyBridgedWriteToView(self);

if (shouldApply) {
/*
NOTE: The values of `opaque` can be different between a view and layer.

In debugging on Xcode 11 I saw the following in lldb:
- Initially for a new ASDisplayNode layer.isOpaque and _view.isOpaque are true
- Set the backgroundColor of the node to a valid UIColor
Expected: layer.isOpaque and view.isOpaque would be equal and true
Actual: view.isOpaque is true and layer.isOpaque is now false

This broke some unit tests for view-backed nodes so I think we need to read directly from the view and can't rely on the layers value at this point.
*/
BOOL oldOpaque = _layer.opaque;
nguyenhuy marked this conversation as resolved.
Show resolved Hide resolved
if (!_flags.layerBacked) {
oldOpaque = _view.opaque;
_view.opaque = newOpaque;
}
_layer.opaque = newOpaque;
if (oldOpaque != newOpaque) {
[self setNeedsDisplay];
Expand Down Expand Up @@ -727,26 +742,47 @@ - (void)setContentMode:(UIViewContentMode)contentMode
- (UIColor *)backgroundColor
{
_bridge_prologue_read;
return [UIColor colorWithCGColor:_getFromLayer(backgroundColor)];
if (_loaded(self)) {
/*
Note: We can no longer rely simply on the layers backgroundColor value if the color is set directly on `_view`
There is no longer a 1:1 mapping between _view.backgroundColor and _layer.backgroundColor after testing in iOS 13 / Xcode 11 so we should prefer one or the other depending on the backing type for the node (view or layer)
*/
if (!_flags.layerBacked) {
return _view.backgroundColor;
} else {
return [UIColor colorWithCGColor:_getFromLayer(backgroundColor)];
}
}
return [UIColor colorWithCGColor:ASDisplayNodeGetPendingState(self).backgroundColor];
nguyenhuy marked this conversation as resolved.
Show resolved Hide resolved
}

- (void)setBackgroundColor:(UIColor *)newBackgroundColor
{
_bridge_prologue_write;

CGColorRef newBackgroundCGColor = [newBackgroundColor CGColor];

BOOL shouldApply = ASDisplayNodeShouldApplyBridgedWriteToView(self);

CGColorRef newBackgroundCGColor = newBackgroundColor.CGColor;
if (shouldApply) {
CGColorRef oldBackgroundCGColor = _layer.backgroundColor;

BOOL specialPropertiesHandling = ASDisplayNodeNeedsSpecialPropertiesHandling(checkFlag(Synchronous), _flags.layerBacked);
if (specialPropertiesHandling) {
_view.backgroundColor = newBackgroundColor;
if (_flags.layerBacked) {
_layer.backgroundColor = newBackgroundColor.CGColor;
} else {
_layer.backgroundColor = newBackgroundCGColor;
/*
NOTE: Setting to the view and layer individually is necessary.

As observed in lldb, the view does not appear to immediately propagate background color to the layer and actually clears it's value (`nil`) initially. This was caught by our snapshot tests.

Given that UIColor / UIView has dynamic capabilties now, we should set directly to the view and make sure that the layers value is consistent here.

*/
_view.backgroundColor = newBackgroundColor;
_layer.backgroundColor = _view.backgroundColor.CGColor;
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Same as below if you update the view I don't think you need to update the layer.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@nguyenhuy and I were debugging and realized we needed to set both of these. UIView seems to be setting the _layer.backgroundColor to nil when setting the views background color.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

We assume these values are bridged but when debugging it seems like the relationship between view and layer is more complex. We're only hitting this now since we are setting backgroundColor directly on the view itself

nguyenhuy marked this conversation as resolved.
Show resolved Hide resolved
// Gather the CGColorRef from the view incase there are any changes it might apply to which CGColorRef is returned for dynamic colors
newBackgroundCGColor = _view.backgroundColor.CGColor;
}

if (!CGColorEqualToColor(oldBackgroundCGColor, newBackgroundCGColor)) {
[self setNeedsDisplay];
}
Expand Down
13 changes: 5 additions & 8 deletions Source/Private/_ASPendingState.mm
Original file line number Diff line number Diff line change
Expand Up @@ -1095,20 +1095,17 @@ - (void)applyToView:(UIView *)view withSpecialPropertiesHandling:(BOOL)specialPr
view.clipsToBounds = _flags.clipsToBounds;

if (flags.setBackgroundColor) {
// We have to make sure certain nodes get the background color call directly set
if (specialPropertiesHandling) {
view.backgroundColor = [UIColor colorWithCGColor:backgroundColor];
} else {
// Set the background color to the layer as in the UIView bridge we use this value as background color
layer.backgroundColor = backgroundColor;
}
view.backgroundColor = [UIColor colorWithCGColor:backgroundColor];
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

If we set the background color to the view the layer should automatically be updated. Could you double cbeck that this is not the case.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@maicki - We tested this today and surprisingly, the layer is not updated (at least not immediately)

Copy link
Member

@nguyenhuy nguyenhuy Jul 19, 2019

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Confirm what Rahul said: the view doesn't propagate the background color immediately which causes many of our tests to fail because they use the layer to render snapshots. For example this test failed because the parent node has a nil background color by the time the test takes a snapshot using ASSnapshotVerifyNode.

An alternative approach would be updating ASSnapshotVerifyNode to use the view if the node is view-backed. But by doing so, we're accepting the risk that there is a time window in which the view and the layer disagree. And this time window is undocumented by UIKit.

Given that now UIColor and UIView has a new dynamic capability, and that we should rely on UIView more to take advantage of this new capability, we should update the framework to set to both the view and the layer and thus eliminate the time window and its associated risks.

layer.backgroundColor = backgroundColor;
}

if (flags.setTintColor)
view.tintColor = self.tintColor;

if (flags.setOpaque)
if (flags.setOpaque) {
view.opaque = _flags.opaque;
layer.opaque = _flags.opaque;
}

if (flags.setHidden)
view.hidden = _flags.hidden;
Expand Down
29 changes: 22 additions & 7 deletions Tests/ASDisplayNodeTests.mm
Original file line number Diff line number Diff line change
Expand Up @@ -719,6 +719,7 @@ - (void)checkSimpleBridgePropertiesSetPropagate:(BOOL)isLayerBacked
// Assert that the realized view/layer have the correct values.
[node layer];


[self checkValuesMatchSetValues:node isLayerBacked:isLayerBacked];

// As a final sanity check, change a value on the realized view and ensure it is fetched through the node.
Expand Down Expand Up @@ -1921,19 +1922,33 @@ - (void)checkBackgroundColorOpaqueRelationshipWithViewLoaded:(BOOL)loaded layerB
[node layer];
}

/*
NOTE: The values of `opaque` can be different between a view and layer.

In debugging on Xcode 11 I saw the following in lldb:
- Initially for a new ASDisplayNode layer.isOpaque and _view.isOpaque are true
- Set the backgroundColor of the node to a valid UIColor
Expected: layer.isOpaque and view.isOpaque would be equal and true
Actual: view.isOpaque is true and layer.isOpaque is now false

For these reasons we have the following variations of asserts depending on the backing type of the node.
*/
XCTAssertTrue(node.opaque, @"Node should start opaque");
XCTAssertTrue(node.layer.opaque, @"Node should start opaque");
if (isLayerBacked) {
XCTAssertTrue(node.layer.opaque, @"Set background color should not have made this layer not opaque");
} else {
XCTAssertTrue(node.view.opaque, @"Set background color should not have made this view not opaque");
}

node.backgroundColor = [UIColor clearColor];

// This could be debated, but at the moment we differ from UIView's behavior to change the other property in response
XCTAssertTrue(node.opaque, @"Set background color should not have made this not opaque");
XCTAssertTrue(node.layer.opaque, @"Set background color should not have made this not opaque");

[node layer];

XCTAssertTrue(node.opaque, @"Set background color should not have made this not opaque");
XCTAssertTrue(node.layer.opaque, @"Set background color should not have made this not opaque");
if (isLayerBacked) {
XCTAssertTrue(node.layer.opaque, @"Set background color should not have made this layer not opaque");
} else {
XCTAssertTrue(node.view.opaque, @"Set background color should not have made this view not opaque");
}
}

- (void)testBackgroundColorOpaqueRelationshipView
Expand Down