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

[ASDisplayNode] Provide safeAreaInsets and layoutMargins bridge #685

Merged
merged 13 commits into from
Mar 27, 2018

Conversation

ypogribnyi
Copy link
Contributor

My attempt to implement #658.

Safe area insets are quite important when designing for iPhone X or when dealing with view controller containers with overlapping bars (e.g. UINavigationController with a translucent navigation bar).

This PR includes:

  • safeAreaInsets, insetsLayoutMarginsFromSafeArea, layoutMargins, preservesSuperviewLayoutMargins properties and -safeAreaInsetsDidChange, -layoutMarginsDidChange callbacks on ASDisplayNode all bridged to the underlying view.

  • automaticallyRelayoutOnSafeAreaChanges and automaticallyRelayoutOnLayoutMarginsChanges flags on ASDisplayNode so we can automatically send -setNeedsLayout when the insets change.

  • A compatibility safeAreaInsets calculation algorithm for old iOS versions (for iOS < 11 we have to observe view controller's topLayoutGuide and bottomLayoutGuide lengths, which is very inconvenient).

…ies to ASDisplayNode

* Add layoutMargins bridged to the underlying view

* Add safeAreaInsets bridged to the underlying view

* Add fallback calculation of safeAreaInsets for old iOS versions

* Add automaticallyRelayoutOnSafeAreaChanges and automaticallyRelayoutOnLayoutMarginsChanges properties

* Add additionalSafeAreaInsets property to ASViewController for compatibility with old iOS versions
@ghost
Copy link

ghost commented Nov 24, 2017

🚫 CI failed with log

@ghost
Copy link

ghost commented Nov 28, 2017

🚫 CI failed with log

Copy link
Member

@nguyenhuy nguyenhuy left a comment

Choose a reason for hiding this comment

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

@ypogribnyi Thanks for submitting the PR. Please take a look at failing tests whenever you have time.

@ghost
Copy link

ghost commented Nov 30, 2017

🚫 CI failed with log

@ghost
Copy link

ghost commented Nov 30, 2017

🚫 CI failed with log

@ypogribnyi
Copy link
Contributor Author

@nguyenhuy, I fixed the tests that were failing because of my code. Now the log shows ticks ✓ for all tests and a strange error 'Early unexpected exit, operation never finished bootstrapping - no restart will be attempted' from xcodebuild. Is it because some tests silently fail, or is it some problem with Xcode?

@ghost
Copy link

ghost commented Dec 4, 2017

🚫 CI failed with log

@appleguy
Copy link
Member

@ypogribnyi thanks for developing this! I think we could probably merge it soon if we can get it passing CI (apologies for the long delay over the holiday).

Perhaps you could sync to latest and make any final tweaks you had in mind and see if the build server is cooperating now?

# Conflicts:
#	CHANGELOG.md
@ghost
Copy link

ghost commented Jan 15, 2018

🚫 CI failed with log

@ypogribnyi
Copy link
Contributor Author

@appleguy I have finally fixed failing tests. Have nothing else to add here.

… safeAreaInsets

# Conflicts:
#	Source/Private/_ASPendingState.mm
Copy link
Member

@nguyenhuy nguyenhuy left a comment

Choose a reason for hiding this comment

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

Thanks for working on this, @ypogribnyi. Overall, great work! I saw you took extra steps to make sure the diff follows existing practices and conventions, which is awesome!

@@ -719,6 +733,17 @@ extern NSInteger const ASDefaultDrawingPriority;
@property (nonatomic, assign) BOOL autoresizesSubviews; // default==YES (undefined for layer-backed nodes)
@property (nonatomic, assign) UIViewAutoresizing autoresizingMask; // default==UIViewAutoresizingNone (undefined for layer-backed nodes)

// Content margins
@property (nonatomic, assign) UIEdgeInsets layoutMargins;
Copy link
Member

Choose a reason for hiding this comment

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

Regarding documentation, I think it's good to remind users that if they access this property, as well as safeAreaInsets, during a layout pass, they should enable automaticallyRelayoutOnSafeAreaChanges and automaticallyRelayoutOnLayoutMarginsChanges. That is because their layout relies on these properties.

_fallbackSafeAreaInsets = insets;
}

BOOL needsManualUpdate = !AS_AT_LEAST_IOS11 || _flags.layerBacked;
Copy link
Member

Choose a reason for hiding this comment

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

Warning: accessing _flags without instance lock. Let's move this line into the scope above.

}

BOOL needsManualUpdate = !AS_AT_LEAST_IOS11 || _flags.layerBacked;
BOOL updatesLayoutMargins = needsManualUpdate && self.insetsLayoutMarginsFromSafeArea;
Copy link
Member

Choose a reason for hiding this comment

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

Calling self.insetsLayoutMarginsFromSafeArea is fine here as the getter acquires the lock itself. However, given the fact that we did acquire the lock just a moment ago (in the scope above), let's try to grab it during that time. One common pattern we use frequently is to add an internal _locked_ version of the getter. See isNodeLoaded and _locked_isNodeLoaded as an example.

- (void)safeAreaInsetsDidChange
{
ASDisplayNodeAssertMainThread();
BOOL automaticallyRelayout;
Copy link
Member

Choose a reason for hiding this comment

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

Let's use the automaticallyRelayoutOnSafeAreaChanges getter here.

- (void)layoutMarginsDidChange
{
ASDisplayNodeAssertMainThread();
BOOL automaticallyRelayout;
Copy link
Member

Choose a reason for hiding this comment

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

Let's call self.automaticallyRelayoutOnLayoutMarginsChanges here.


safeArea = ASConcatInsets(safeArea, additionalInsets);

_node.fallbackSafeAreaInsets = safeArea;
Copy link
Member

@nguyenhuy nguyenhuy Jan 16, 2018

Choose a reason for hiding this comment

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

  • This node is guaranteed to be view-backed and loaded. The only reason the fallback insets is needed here is that the runtime is iOS 10 or earlier. Let's take advantage of that and avoid doing unnecessary work if possible.

[super setAdditionalSafeAreaInsets:additionalSafeAreaInsets];
}

_fallbackAdditionalSafeAreaInsets = additionalSafeAreaInsets;
Copy link
Member

Choose a reason for hiding this comment

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

Should we recalculate the root node's insets here?

* YES here only means that this node is used as an ASViewController node. It doesn't mean that this node is a root of
* ASDisplayNode hierarchy, e.g. when its view controller is parented by another ASViewController.
*/
@property (nonatomic, assign, getter=isViewControllerRoot) BOOL viewControllerRoot;
Copy link
Member

Choose a reason for hiding this comment

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

Good call on making this property private :)

@@ -326,6 +335,9 @@ FOUNDATION_EXPORT NSString * const ASRenderingEngineDidDisplayNodesScheduledBefo
*/
- (void)nodeViewDidAddGestureRecognizer;

// Recalculates fallbackSafeAreaInsets for the subnodes
- (void)_fallbackUpdateSafeAreaOnChildren;
Copy link
Member

Choose a reason for hiding this comment

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

Nit: May not needed because _fallbackUpdateSafeAreaOnChildren is implemented and called in ASDisplayNode.mm only.

Copy link
Contributor Author

Choose a reason for hiding this comment

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

This is also used by -[ASDisplayNode safeAreaDidChange], which is implemented in ASDisplayNode+UIViewBridge.mm.

}
}

if (__loaded(self) && (!AS_AT_LEAST_IOS11 || _flags.layerBacked)) {
Copy link
Member

Choose a reason for hiding this comment

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

Warning: accessing _flags without the lock.

* Update documentation for layoutMargins and safeAreaInsets properties. Suggest that users set the automaticallyRelayout* flags to ensure that their layout is synchronized to the margin's values.
* Fix accessing ASDisplayNode internal structures without a lock.
* Add shortcut in -[ASDisplayNode _fallbackUpdateSafeAreaOnChildren] to skip a child when possible.
* Add shortcut in ASViewController to avoid fallback safe area insets recalculation in iOS 11. Fix fallback safe area insets recalculation when the additionalSafeAreaInsets are set.
* Add debug check that a view controller's node is never reused without its view controller, so the viewControllerRoot flag value is always consistent.
* Use getters instead of reading ivars directly in -layoutMarginsDidChange and -safeAreaInsetsDidChange.
… safeAreaInsets

# Conflicts:
#	CHANGELOG.md
@ypogribnyi
Copy link
Contributor Author

@nguyenhuy, thank you for the detailed review!

@ay8s
Copy link
Collaborator

ay8s commented Jan 23, 2018

This is awesome @ypogribnyi. I wonder if similar could be done to expose the readableContentGuide within cell nodes etc? Think it could even just be the layoutFrame from the readable content guide that is accessible so you can then constrain the layout to that within layoutSpecThatFits:

@nguyenhuy nguyenhuy self-assigned this Jan 31, 2018
@appleguy
Copy link
Member

@ypogribnyi do you have any cycles to wrap up remaining requests from Huy? Let us know either way - if you need someone to take this over, I'm not sure who might be able to, but then we'd be progressing towards landing it.

This is a great contribution overall, so thanks for taking the time to work with us!

@philtre
Copy link

philtre commented Mar 16, 2018

It would be super helpful to have a ASSafeAreaLayoutSpec that abstracts this functionality. Looking forward to the merge!

Copy link
Member

@nguyenhuy nguyenhuy left a comment

Choose a reason for hiding this comment

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

@ypogribnyi Sorry for the long delay here. Awesome contribution. Thank you!

}
}

- (void)_fallbackUpdateSafeAreaOnChildren
Copy link
Member

Choose a reason for hiding this comment

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

Thanks for the note. I think this implementation and your trade-offs are reasonable. Let's roll with this.

@@ -73,10 +74,14 @@ - (void)_initializeInstance
if (_node == nil) {
return;
}

_node.viewControllerRoot = YES;
Copy link
Member

Choose a reason for hiding this comment

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

👍

@nguyenhuy nguyenhuy merged commit 7f01b89 into TextureGroup:master Mar 27, 2018
alexhillc added a commit to franklyinc/Texture that referenced this pull request Apr 19, 2018
bernieperez pushed a commit to AtomTickets/Texture that referenced this pull request Apr 25, 2018
…ureGroup#685)

* [ASDisplayNode] Add safeAreaInsets, layoutMargins and related properties to ASDisplayNode

* Add layoutMargins bridged to the underlying view

* Add safeAreaInsets bridged to the underlying view

* Add fallback calculation of safeAreaInsets for old iOS versions

* Add automaticallyRelayoutOnSafeAreaChanges and automaticallyRelayoutOnLayoutMarginsChanges properties

* Add additionalSafeAreaInsets property to ASViewController for compatibility with old iOS versions

* Provide safeAreaInsets for layer-backed nodes.

This also fixes tests.

* Fix crash when insetsLayoutMarginsFromSafeArea is set from a background thread

* Changes requested at code review:
* Update documentation for layoutMargins and safeAreaInsets properties. Suggest that users set the automaticallyRelayout* flags to ensure that their layout is synchronized to the margin's values.
* Fix accessing ASDisplayNode internal structures without a lock.
* Add shortcut in -[ASDisplayNode _fallbackUpdateSafeAreaOnChildren] to skip a child when possible.
* Add shortcut in ASViewController to avoid fallback safe area insets recalculation in iOS 11. Fix fallback safe area insets recalculation when the additionalSafeAreaInsets are set.
* Add debug check that a view controller's node is never reused without its view controller, so the viewControllerRoot flag value is always consistent.
* Use getters instead of reading ivars directly in -layoutMarginsDidChange and -safeAreaInsetsDidChange.

* Minor change in CHANGELOG

* Minor change in ASDisplayNodeTests.mm
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

Successfully merging this pull request may close these issues.

5 participants