Skip to content

Commit

Permalink
[ASDisplayNode] Provide safeAreaInsets and layoutMargins bridge (#685)
Browse files Browse the repository at this point in the history
* [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
  • Loading branch information
ypogribnyi authored and nguyenhuy committed Mar 27, 2018
1 parent 063194c commit 7f01b89
Show file tree
Hide file tree
Showing 13 changed files with 485 additions and 3 deletions.
1 change: 1 addition & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
## master
* Add your own contributions to the next release on the line below this with your name.
- [ASDisplayNode] Add safeAreaInsets, layoutMargins and related properties to ASDisplayNode, with full support for older OS versions [Yevgen Pogribnyi](https://github.com/ypogribnyi) [#685](https://github.com/TextureGroup/Texture/pull/685)
- [ASPINRemoteImageDownloader] Allow cache to provide animated image. [Max Wang](https://github.com/wsdwsd0829) [#850](https://github.com/TextureGroup/Texture/pull/850)
- [tvOS] Fixes errors when building against tvOS SDK [Alex Hill](https://github.com/alexhillc) [#728](https://github.com/TextureGroup/Texture/pull/728)
- [ASDisplayNode] Add unit tests for layout z-order changes (with an open issue to fix).
Expand Down
41 changes: 41 additions & 0 deletions Source/ASDisplayNode.h
Original file line number Diff line number Diff line change
Expand Up @@ -551,6 +551,20 @@ extern NSInteger const ASDefaultDrawingPriority;
*/
@property (nonatomic, readonly) BOOL supportsLayerBacking;

/**
* Whether or not the node layout should be automatically updated when it receives safeAreaInsetsDidChange.
*
* Defaults to NO.
*/
@property (nonatomic, assign) BOOL automaticallyRelayoutOnSafeAreaChanges;

/**
* Whether or not the node layout should be automatically updated when it receives layoutMarginsDidChange.
*
* Defaults to NO.
*/
@property (nonatomic, assign) BOOL automaticallyRelayoutOnLayoutMarginsChanges;

@end

/**
Expand Down Expand Up @@ -725,6 +739,33 @@ 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)

/**
* @abstract Content margins
*
* @discussion This property is bridged to its UIView counterpart.
*
* If your layout depends on this property, you should probably enable automaticallyRelayoutOnLayoutMarginsChanges to ensure
* that the layout gets automatically updated when the value of this property changes. Or you can override layoutMarginsDidChange
* and make all the necessary updates manually.
*/
@property (nonatomic, assign) UIEdgeInsets layoutMargins;
@property (nonatomic, assign) BOOL preservesSuperviewLayoutMargins; // default is NO - set to enable pass-through or cascading behavior of margins from this view’s parent to its children
- (void)layoutMarginsDidChange;

/**
* @abstract Safe area insets
*
* @discussion This property is bridged to its UIVIew counterpart.
*
* If your layout depends on this property, you should probably enable automaticallyRelayoutOnSafeAreaChanges to ensure
* that the layout gets automatically updated when the value of this property changes. Or you can override safeAreaInsetsDidChange
* and make all the necessary updates manually.
*/
@property (nonatomic, readonly) UIEdgeInsets safeAreaInsets;
@property (nonatomic, assign) BOOL insetsLayoutMarginsFromSafeArea; // Default: YES
- (void)safeAreaInsetsDidChange;


// UIResponder methods
// By default these fall through to the underlying view, but can be overridden.
- (BOOL)canBecomeFirstResponder; // default==NO
Expand Down
109 changes: 108 additions & 1 deletion Source/ASDisplayNode.mm
Original file line number Diff line number Diff line change
Expand Up @@ -282,6 +282,14 @@ - (void)_initializeInstance

_flags.canClearContentsOfLayer = YES;
_flags.canCallSetNeedsDisplayOfLayer = YES;

_fallbackSafeAreaInsets = UIEdgeInsetsZero;
_fallbackInsetsLayoutMarginsFromSafeArea = YES;
_isViewControllerRoot = NO;

_automaticallyRelayoutOnSafeAreaChanges = NO;
_automaticallyRelayoutOnLayoutMarginsChanges = NO;

ASDisplayNodeLogEvent(self, @"init");
}

Expand Down Expand Up @@ -851,7 +859,104 @@ - (void)nodeViewDidAddGestureRecognizer
_flags.viewEverHadAGestureRecognizerAttached = YES;
}

#pragma mark UIResponder
- (UIEdgeInsets)fallbackSafeAreaInsets
{
ASDN::MutexLocker l(__instanceLock__);
return _fallbackSafeAreaInsets;
}

- (void)setFallbackSafeAreaInsets:(UIEdgeInsets)insets
{
BOOL needsManualUpdate;
BOOL updatesLayoutMargins;

{
ASDN::MutexLocker l(__instanceLock__);
ASDisplayNodeAssertThreadAffinity(self);

if (UIEdgeInsetsEqualToEdgeInsets(insets, _fallbackSafeAreaInsets)) {
return;
}

_fallbackSafeAreaInsets = insets;
needsManualUpdate = !AS_AT_LEAST_IOS11 || _flags.layerBacked;
updatesLayoutMargins = needsManualUpdate && [self _locked_insetsLayoutMarginsFromSafeArea];
}

if (needsManualUpdate) {
[self safeAreaInsetsDidChange];
}

if (updatesLayoutMargins) {
[self layoutMarginsDidChange];
}
}

- (void)_fallbackUpdateSafeAreaOnChildren
{
ASDisplayNodeAssertThreadAffinity(self);

UIEdgeInsets insets = self.safeAreaInsets;
CGRect bounds = self.bounds;

for (ASDisplayNode *child in self.subnodes) {
if (AS_AT_LEAST_IOS11 && !child.layerBacked) {
// In iOS 11 view-backed nodes already know what their safe area is.
continue;
}

if (child.viewControllerRoot) {
// Its safe area is controlled by a view controller. Don't override it.
continue;
}

CGRect childFrame = child.frame;
UIEdgeInsets childInsets = UIEdgeInsetsMake(MAX(insets.top - (CGRectGetMinY(childFrame) - CGRectGetMinY(bounds)), 0),
MAX(insets.left - (CGRectGetMinX(childFrame) - CGRectGetMinX(bounds)), 0),
MAX(insets.bottom - (CGRectGetMaxY(bounds) - CGRectGetMaxY(childFrame)), 0),
MAX(insets.right - (CGRectGetMaxX(bounds) - CGRectGetMaxX(childFrame)), 0));

child.fallbackSafeAreaInsets = childInsets;
}
}

- (BOOL)isViewControllerRoot
{
ASDN::MutexLocker l(__instanceLock__);
return _isViewControllerRoot;
}

- (void)setViewControllerRoot:(BOOL)flag
{
ASDN::MutexLocker l(__instanceLock__);
_isViewControllerRoot = flag;
}

- (BOOL)automaticallyRelayoutOnSafeAreaChanges
{
ASDN::MutexLocker l(__instanceLock__);
return _automaticallyRelayoutOnSafeAreaChanges;
}

- (void)setAutomaticallyRelayoutOnSafeAreaChanges:(BOOL)flag
{
ASDN::MutexLocker l(__instanceLock__);
_automaticallyRelayoutOnSafeAreaChanges = flag;
}

- (BOOL)automaticallyRelayoutOnLayoutMarginsChanges
{
ASDN::MutexLocker l(__instanceLock__);
return _automaticallyRelayoutOnLayoutMarginsChanges;
}

- (void)setAutomaticallyRelayoutOnLayoutMarginsChanges:(BOOL)flag
{
ASDN::MutexLocker l(__instanceLock__);
_automaticallyRelayoutOnLayoutMarginsChanges = flag;
}

#pragma mark - UIResponder

#define HANDLE_NODE_RESPONDER_METHOD(__sel) \
/* All responder methods should be called on the main thread */ \
Expand Down Expand Up @@ -1042,6 +1147,8 @@ - (void)__layout
[self layoutDidFinish];
});
}

[self _fallbackUpdateSafeAreaOnChildren];
}

- (void)layoutDidFinish
Expand Down
5 changes: 5 additions & 0 deletions Source/ASViewController.h
Original file line number Diff line number Diff line change
Expand Up @@ -78,6 +78,11 @@ NS_ASSUME_NONNULL_BEGIN
// Refer to examples/SynchronousConcurrency, AsyncViewController.m
@property (nonatomic, assign) BOOL neverShowPlaceholders;

/* Custom container UIViewController subclasses can use this property to add to the overlay
that UIViewController calculates for the safeAreaInsets for contained view controllers.
*/
@property(nonatomic) UIEdgeInsets additionalSafeAreaInsets;

@end

@interface ASViewController (ASRangeControllerUpdateRangeProtocol)
Expand Down
38 changes: 38 additions & 0 deletions Source/ASViewController.mm
Original file line number Diff line number Diff line change
Expand Up @@ -32,6 +32,7 @@ @implementation ASViewController
NSInteger _visibilityDepth;
BOOL _selfConformsToRangeModeProtocol;
BOOL _nodeConformsToRangeModeProtocol;
UIEdgeInsets _fallbackAdditionalSafeAreaInsets;
}

- (instancetype)initWithNibName:(NSString *)nibNameOrNil bundle:(NSBundle *)nibBundleOrNil
Expand Down Expand Up @@ -73,10 +74,14 @@ - (void)_initializeInstance
if (_node == nil) {
return;
}

_node.viewControllerRoot = YES;

_selfConformsToRangeModeProtocol = [self conformsToProtocol:@protocol(ASRangeControllerUpdateRangeProtocol)];
_nodeConformsToRangeModeProtocol = [_node conformsToProtocol:@protocol(ASRangeControllerUpdateRangeProtocol)];
_automaticallyAdjustRangeModeBasedOnViewEvents = _selfConformsToRangeModeProtocol || _nodeConformsToRangeModeProtocol;

_fallbackAdditionalSafeAreaInsets = UIEdgeInsetsZero;

// In case the node will get loaded
if (_node.nodeLoaded) {
Expand Down Expand Up @@ -159,6 +164,20 @@ - (void)viewDidLayoutSubviews
[_node recursivelyEnsureDisplaySynchronously:YES];
}
[super viewDidLayoutSubviews];

if (!AS_AT_LEAST_IOS11) {
[self _updateNodeFallbackSafeArea];
}
}

- (void)_updateNodeFallbackSafeArea
{
UIEdgeInsets safeArea = UIEdgeInsetsMake(self.topLayoutGuide.length, 0, self.bottomLayoutGuide.length, 0);
UIEdgeInsets additionalInsets = self.additionalSafeAreaInsets;

safeArea = ASConcatInsets(safeArea, additionalInsets);

_node.fallbackSafeAreaInsets = safeArea;
}

ASVisibilityDidMoveToParentViewController;
Expand Down Expand Up @@ -264,6 +283,25 @@ - (ASInterfaceState)interfaceState
return _node.interfaceState;
}

- (UIEdgeInsets)additionalSafeAreaInsets
{
if (AS_AVAILABLE_IOS(11.0)) {
return super.additionalSafeAreaInsets;
}

return _fallbackAdditionalSafeAreaInsets;
}

- (void)setAdditionalSafeAreaInsets:(UIEdgeInsets)additionalSafeAreaInsets
{
if (AS_AVAILABLE_IOS(11.0)) {
[super setAdditionalSafeAreaInsets:additionalSafeAreaInsets];
} else {
_fallbackAdditionalSafeAreaInsets = additionalSafeAreaInsets;
[self _updateNodeFallbackSafeArea];
}
}

#pragma mark - ASTraitEnvironment

- (ASPrimitiveTraitCollection)primitiveTraitCollectionForUITraitCollection:(UITraitCollection *)traitCollection
Expand Down
3 changes: 3 additions & 0 deletions Source/Details/UIView+ASConvenience.h
Original file line number Diff line number Diff line change
Expand Up @@ -75,6 +75,9 @@ NS_ASSUME_NONNULL_BEGIN
@property (nonatomic, assign, getter=isUserInteractionEnabled) BOOL userInteractionEnabled;
@property (nonatomic, assign, getter=isExclusiveTouch) BOOL exclusiveTouch;
@property (nonatomic, assign, getter=asyncdisplaykit_isAsyncTransactionContainer, setter = asyncdisplaykit_setAsyncTransactionContainer:) BOOL asyncdisplaykit_asyncTransactionContainer;
@property (nonatomic, assign) UIEdgeInsets layoutMargins;
@property (nonatomic, assign) BOOL preservesSuperviewLayoutMargins;
@property (nonatomic, assign) BOOL insetsLayoutMarginsFromSafeArea;

/**
Following properties of the UIAccessibility informal protocol are supported as well.
Expand Down
30 changes: 29 additions & 1 deletion Source/Details/_ASDisplayView.mm
Original file line number Diff line number Diff line change
Expand Up @@ -21,11 +21,13 @@
#import <AsyncDisplayKit/_ASCoreAnimationExtras.h>
#import <AsyncDisplayKit/_ASDisplayLayer.h>
#import <AsyncDisplayKit/ASDisplayNodeInternal.h>
#import <AsyncDisplayKit/ASDisplayNode+Convenience.h>
#import <AsyncDisplayKit/ASDisplayNode+FrameworkPrivate.h>
#import <AsyncDisplayKit/ASDisplayNode+Subclasses.h>
#import <AsyncDisplayKit/ASInternalHelpers.h>
#import <AsyncDisplayKit/ASObjectDescriptionHelpers.h>
#import <AsyncDisplayKit/ASLayout.h>
#import <AsyncDisplayKit/ASObjectDescriptionHelpers.h>
#import <AsyncDisplayKit/ASViewController.h>

#pragma mark - _ASDisplayViewMethodOverrides

Expand Down Expand Up @@ -234,6 +236,16 @@ - (void)didMoveToSuperview
self.keepalive_node = nil;
}

#if DEBUG
// This is only to help detect issues when a root-of-view-controller node is reused separately from its view controller.
// Avoid overhead in release.
if (superview && node.viewControllerRoot) {
UIViewController *vc = [node closestViewController];

ASDisplayNodeAssert(vc != nil && [vc isKindOfClass:[ASViewController class]] && ((ASViewController*)vc).node == node, @"This node was once used as a view controller's node. You should not reuse it without its view controller.");
}
#endif

ASDisplayNode *supernode = node.supernode;
ASDisplayNodeAssert(!supernode.isLayerBacked, @"Shouldn't be possible for superview's node to be layer-backed.");

Expand Down Expand Up @@ -481,6 +493,22 @@ - (BOOL)canPerformAction:(SEL)action withSender:(id)sender
return ([super canPerformAction:action withSender:sender] || [node respondsToSelector:action]);
}

- (void)layoutMarginsDidChange
{
ASDisplayNode *node = _asyncdisplaykit_node; // Create strong reference to weak ivar.
[super layoutMarginsDidChange];

[node layoutMarginsDidChange];
}

- (void)safeAreaInsetsDidChange
{
ASDisplayNode *node = _asyncdisplaykit_node; // Create strong reference to weak ivar.
[super safeAreaInsetsDidChange];

[node safeAreaInsetsDidChange];
}

- (id)forwardingTargetForSelector:(SEL)aSelector
{
// Ideally, we would implement -targetForAction:withSender: and simply return the node where we don't respond personally.
Expand Down
19 changes: 19 additions & 0 deletions Source/Private/ASDisplayNode+FrameworkPrivate.h
Original file line number Diff line number Diff line change
Expand Up @@ -241,6 +241,25 @@ __unused static NSString * _Nonnull NSStringFromASHierarchyStateChange(ASHierarc
*/
- (BOOL)shouldScheduleDisplayWithNewInterfaceState:(ASInterfaceState)newInterfaceState;

/**
* @abstract safeAreaInsets will fallback to this value if the corresponding UIKit property is not available
* (due to an old iOS version).
*
* @discussion This should be set by the owning view controller based on it's layout guides.
* If this is not a view controllet's node the value will be calculated automatically by the parent node.
*/
@property (nonatomic, assign) UIEdgeInsets fallbackSafeAreaInsets;

/**
* @abstract Indicates if this node is a view controller's root node. Defaults to NO.
*
* @discussion Set to YES in -[ASViewController initWithNode:].
*
* 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;

@end


Expand Down
Loading

0 comments on commit 7f01b89

Please sign in to comment.