diff --git a/CHANGELOG.md b/CHANGELOG.md index 22c737c17f..241a9c3ef3 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -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). diff --git a/Source/ASDisplayNode.h b/Source/ASDisplayNode.h index 851155deaf..8968a1f500 100644 --- a/Source/ASDisplayNode.h +++ b/Source/ASDisplayNode.h @@ -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 /** @@ -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 diff --git a/Source/ASDisplayNode.mm b/Source/ASDisplayNode.mm index 72eaf03314..adddc67dfb 100644 --- a/Source/ASDisplayNode.mm +++ b/Source/ASDisplayNode.mm @@ -282,6 +282,14 @@ static ASDisplayNodeMethodOverrides GetASDisplayNodeMethodOverrides(Class c) _flags.canClearContentsOfLayer = YES; _flags.canCallSetNeedsDisplayOfLayer = YES; + + _fallbackSafeAreaInsets = UIEdgeInsetsZero; + _fallbackInsetsLayoutMarginsFromSafeArea = YES; + _isViewControllerRoot = NO; + + _automaticallyRelayoutOnSafeAreaChanges = NO; + _automaticallyRelayoutOnLayoutMarginsChanges = NO; + ASDisplayNodeLogEvent(self, @"init"); } @@ -851,7 +859,104 @@ static ASDisplayNodeMethodOverrides GetASDisplayNodeMethodOverrides(Class c) _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 */ \ @@ -1042,6 +1147,8 @@ static ASDisplayNodeMethodOverrides GetASDisplayNodeMethodOverrides(Class c) [self layoutDidFinish]; }); } + + [self _fallbackUpdateSafeAreaOnChildren]; } - (void)layoutDidFinish diff --git a/Source/ASViewController.h b/Source/ASViewController.h index 28fd134f4b..3d1c7e13a0 100644 --- a/Source/ASViewController.h +++ b/Source/ASViewController.h @@ -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) diff --git a/Source/ASViewController.mm b/Source/ASViewController.mm index 4e1e57c3a6..8aded78c3c 100644 --- a/Source/ASViewController.mm +++ b/Source/ASViewController.mm @@ -32,6 +32,7 @@ NSInteger _visibilityDepth; BOOL _selfConformsToRangeModeProtocol; BOOL _nodeConformsToRangeModeProtocol; + UIEdgeInsets _fallbackAdditionalSafeAreaInsets; } - (instancetype)initWithNibName:(NSString *)nibNameOrNil bundle:(NSBundle *)nibBundleOrNil @@ -73,10 +74,14 @@ 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) { @@ -159,6 +164,20 @@ [_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; @@ -264,6 +283,25 @@ ASVisibilityDepthImplementation; 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 diff --git a/Source/Details/UIView+ASConvenience.h b/Source/Details/UIView+ASConvenience.h index 452dcee6e5..8c76879527 100644 --- a/Source/Details/UIView+ASConvenience.h +++ b/Source/Details/UIView+ASConvenience.h @@ -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. diff --git a/Source/Details/_ASDisplayView.mm b/Source/Details/_ASDisplayView.mm index 2b72d1fd69..9315fa7af7 100644 --- a/Source/Details/_ASDisplayView.mm +++ b/Source/Details/_ASDisplayView.mm @@ -21,11 +21,13 @@ #import #import #import +#import #import #import #import -#import #import +#import +#import #pragma mark - _ASDisplayViewMethodOverrides @@ -234,6 +236,16 @@ static _ASDisplayViewMethodOverrides GetASDisplayViewMethodOverrides(Class c) 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."); @@ -481,6 +493,22 @@ IMPLEMENT_RESPONDER_METHOD(isFirstResponder, _ASDisplayViewMethodOverrideIsFirst 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. diff --git a/Source/Private/ASDisplayNode+FrameworkPrivate.h b/Source/Private/ASDisplayNode+FrameworkPrivate.h index 2f3e7223dd..987fdfa76e 100644 --- a/Source/Private/ASDisplayNode+FrameworkPrivate.h +++ b/Source/Private/ASDisplayNode+FrameworkPrivate.h @@ -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 diff --git a/Source/Private/ASDisplayNode+UIViewBridge.mm b/Source/Private/ASDisplayNode+UIViewBridge.mm index ed92a9f7ec..939ed642c6 100644 --- a/Source/Private/ASDisplayNode+UIViewBridge.mm +++ b/Source/Private/ASDisplayNode+UIViewBridge.mm @@ -860,6 +860,103 @@ if (shouldApply) { _layer.layerProperty = (layerValueExpr); } else { ASDisplayNo #endif } +- (UIEdgeInsets)layoutMargins +{ + _bridge_prologue_read; + ASDisplayNodeAssert(!_flags.layerBacked, @"Danger: this property is undefined on layer-backed nodes."); + UIEdgeInsets margins = _getFromViewOnly(layoutMargins); + + if (!AS_AT_LEAST_IOS11 && self.insetsLayoutMarginsFromSafeArea) { + UIEdgeInsets safeArea = self.safeAreaInsets; + margins = ASConcatInsets(margins, safeArea); + } + + return margins; +} + +- (void)setLayoutMargins:(UIEdgeInsets)layoutMargins +{ + _bridge_prologue_write; + ASDisplayNodeAssert(!_flags.layerBacked, @"Danger: this property is undefined on layer-backed nodes."); + _setToViewOnly(layoutMargins, layoutMargins); +} + +- (BOOL)preservesSuperviewLayoutMargins +{ + _bridge_prologue_read; + ASDisplayNodeAssert(!_flags.layerBacked, @"Danger: this property is undefined on layer-backed nodes."); + return _getFromViewOnly(preservesSuperviewLayoutMargins); +} + +- (void)setPreservesSuperviewLayoutMargins:(BOOL)preservesSuperviewLayoutMargins +{ + _bridge_prologue_write; + ASDisplayNodeAssert(!_flags.layerBacked, @"Danger: this property is undefined on layer-backed nodes."); + _setToViewOnly(preservesSuperviewLayoutMargins, preservesSuperviewLayoutMargins); +} + +- (void)layoutMarginsDidChange +{ + ASDisplayNodeAssertMainThread(); + + if (self.automaticallyRelayoutOnLayoutMarginsChanges) { + [self setNeedsLayout]; + } +} + +- (UIEdgeInsets)safeAreaInsets +{ + _bridge_prologue_read; + + if (AS_AVAILABLE_IOS(11.0)) { + if (!_flags.layerBacked && __loaded(self)) { + return self.view.safeAreaInsets; + } + } + return _fallbackSafeAreaInsets; +} + +- (BOOL)insetsLayoutMarginsFromSafeArea +{ + _bridge_prologue_read; + + return [self _locked_insetsLayoutMarginsFromSafeArea]; +} + +- (void)setInsetsLayoutMarginsFromSafeArea:(BOOL)insetsLayoutMarginsFromSafeArea +{ + ASDisplayNodeAssertThreadAffinity(self); + BOOL shouldNotifyAboutUpdate; + { + _bridge_prologue_write; + + _fallbackInsetsLayoutMarginsFromSafeArea = insetsLayoutMarginsFromSafeArea; + + if (AS_AVAILABLE_IOS(11.0)) { + if (!_flags.layerBacked) { + _setToViewOnly(insetsLayoutMarginsFromSafeArea, insetsLayoutMarginsFromSafeArea); + } + } + + shouldNotifyAboutUpdate = __loaded(self) && (!AS_AT_LEAST_IOS11 || _flags.layerBacked); + } + + if (shouldNotifyAboutUpdate) { + [self layoutMarginsDidChange]; + } +} + +- (void)safeAreaInsetsDidChange +{ + ASDisplayNodeAssertMainThread(); + + if (self.automaticallyRelayoutOnSafeAreaChanges) { + [self setNeedsLayout]; + } + + [self _fallbackUpdateSafeAreaOnChildren]; +} + @end @implementation ASDisplayNode (InternalPropertyBridge) @@ -876,6 +973,16 @@ if (shouldApply) { _layer.layerProperty = (layerValueExpr); } else { ASDisplayNo _setToLayer(cornerRadius, newLayerCornerRadius); } +- (BOOL)_locked_insetsLayoutMarginsFromSafeArea +{ + if (AS_AVAILABLE_IOS(11.0)) { + if (!_flags.layerBacked) { + return _getFromViewOnly(insetsLayoutMarginsFromSafeArea); + } + } + return _fallbackInsetsLayoutMarginsFromSafeArea; +} + @end #pragma mark - UIViewBridgeAccessibility diff --git a/Source/Private/ASDisplayNodeInternal.h b/Source/Private/ASDisplayNodeInternal.h index 5798d53e5d..ab0e3faedb 100644 --- a/Source/Private/ASDisplayNodeInternal.h +++ b/Source/Private/ASDisplayNodeInternal.h @@ -204,6 +204,15 @@ FOUNDATION_EXPORT NSString * const ASRenderingEngineDidDisplayNodesScheduledBefo UIBezierPath *_accessibilityPath; BOOL _isAccessibilityContainer; + // These properties are used on iOS 10 and lower, where safe area is not supported by UIKit. + UIEdgeInsets _fallbackSafeAreaInsets; + BOOL _fallbackInsetsLayoutMarginsFromSafeArea; + + BOOL _automaticallyRelayoutOnSafeAreaChanges; + BOOL _automaticallyRelayoutOnLayoutMarginsChanges; + + BOOL _isViewControllerRoot; + // performance measurement ASDisplayNodePerformanceMeasurementOptions _measurementOptions; NSTimeInterval _layoutSpecTotalTime; @@ -335,12 +344,17 @@ FOUNDATION_EXPORT NSString * const ASRenderingEngineDidDisplayNodesScheduledBefo */ - (void)nodeViewDidAddGestureRecognizer; +// Recalculates fallbackSafeAreaInsets for the subnodes +- (void)_fallbackUpdateSafeAreaOnChildren; + @end @interface ASDisplayNode (InternalPropertyBridge) @property (nonatomic, assign) CGFloat layerCornerRadius; +- (BOOL)_locked_insetsLayoutMarginsFromSafeArea; + @end NS_ASSUME_NONNULL_END diff --git a/Source/Private/ASInternalHelpers.h b/Source/Private/ASInternalHelpers.h index fcd82da0a8..d5c1e035bc 100644 --- a/Source/Private/ASInternalHelpers.h +++ b/Source/Private/ASInternalHelpers.h @@ -100,6 +100,15 @@ ASDISPLAYNODE_INLINE void ASBoundsAndPositionForFrame(CGRect rect, CGPoint origi rect.origin.y + rect.size.height * anchorPoint.y); } +ASDISPLAYNODE_INLINE UIEdgeInsets ASConcatInsets(UIEdgeInsets insetsA, UIEdgeInsets insetsB) +{ + insetsA.top += insetsB.top; + insetsA.left += insetsB.left; + insetsA.bottom += insetsB.bottom; + insetsA.right += insetsB.right; + return insetsA; +} + @interface NSIndexPath (ASInverseComparison) - (NSComparisonResult)asdk_inverseCompare:(NSIndexPath *)otherIndexPath; @end diff --git a/Source/Private/_ASPendingState.mm b/Source/Private/_ASPendingState.mm index d009771bc5..a87707bb67 100644 --- a/Source/Private/_ASPendingState.mm +++ b/Source/Private/_ASPendingState.mm @@ -91,6 +91,9 @@ typedef struct { int setAccessibilityActivationPoint:1; int setAccessibilityPath:1; int setSemanticContentAttribute:1; + int setLayoutMargins:1; + int setPreservesSuperviewLayoutMargins:1; + int setInsetsLayoutMarginsFromSafeArea:1; } ASPendingStateFlags; @implementation _ASPendingState @@ -123,6 +126,9 @@ typedef struct { CGFloat borderWidth; CGColorRef borderColor; BOOL asyncTransactionContainer; + UIEdgeInsets layoutMargins; + BOOL preservesSuperviewLayoutMargins; + BOOL insetsLayoutMarginsFromSafeArea; BOOL isAccessibilityElement; NSString *accessibilityLabel; NSAttributedString *accessibilityAttributedLabel; @@ -208,7 +214,9 @@ ASDISPLAYNODE_INLINE void ASPendingStateApplyMetricsToLayer(_ASPendingState *sta @synthesize borderColor=borderColor; @synthesize asyncdisplaykit_asyncTransactionContainer=asyncTransactionContainer; @synthesize semanticContentAttribute=semanticContentAttribute; - +@synthesize layoutMargins=layoutMargins; +@synthesize preservesSuperviewLayoutMargins=preservesSuperviewLayoutMargins; +@synthesize insetsLayoutMarginsFromSafeArea=insetsLayoutMarginsFromSafeArea; static CGColorRef blackColorRef = NULL; static UIColor *defaultTintColor = nil; @@ -263,6 +271,9 @@ static UIColor *defaultTintColor = nil; shadowRadius = 3; borderWidth = 0; borderColor = blackColorRef; + layoutMargins = UIEdgeInsetsMake(8, 8, 8, 8); + preservesSuperviewLayoutMargins = NO; + insetsLayoutMarginsFromSafeArea = YES; isAccessibilityElement = NO; accessibilityLabel = nil; accessibilityAttributedLabel = nil; @@ -560,6 +571,24 @@ static UIColor *defaultTintColor = nil; _flags.setAsyncTransactionContainer = YES; } +- (void)setLayoutMargins:(UIEdgeInsets)margins +{ + layoutMargins = margins; + _flags.setLayoutMargins = YES; +} + +- (void)setPreservesSuperviewLayoutMargins:(BOOL)flag +{ + preservesSuperviewLayoutMargins = flag; + _flags.setPreservesSuperviewLayoutMargins = YES; +} + +- (void)setInsetsLayoutMarginsFromSafeArea:(BOOL)flag +{ + insetsLayoutMarginsFromSafeArea = flag; + _flags.setInsetsLayoutMarginsFromSafeArea = YES; +} + - (void)setSemanticContentAttribute:(UISemanticContentAttribute)attribute API_AVAILABLE(ios(9.0), tvos(9.0)) { semanticContentAttribute = attribute; _flags.setSemanticContentAttribute = YES; @@ -1036,6 +1065,18 @@ static UIColor *defaultTintColor = nil; if (flags.setOpaque) ASDisplayNodeAssert(layer.opaque == opaque, @"Didn't set opaque as desired"); + if (flags.setLayoutMargins) + view.layoutMargins = layoutMargins; + + if (flags.setPreservesSuperviewLayoutMargins) + view.preservesSuperviewLayoutMargins = preservesSuperviewLayoutMargins; + + if (AS_AVAILABLE_IOS(11.0)) { + if (flags.setInsetsLayoutMarginsFromSafeArea) { + view.insetsLayoutMarginsFromSafeArea = insetsLayoutMarginsFromSafeArea; + } + } + if (flags.setSemanticContentAttribute) { view.semanticContentAttribute = semanticContentAttribute; } @@ -1199,6 +1240,11 @@ static UIColor *defaultTintColor = nil; pendingState.allowsEdgeAntialiasing = layer.allowsEdgeAntialiasing; pendingState.edgeAntialiasingMask = layer.edgeAntialiasingMask; pendingState.semanticContentAttribute = view.semanticContentAttribute; + pendingState.layoutMargins = view.layoutMargins; + pendingState.preservesSuperviewLayoutMargins = view.preservesSuperviewLayoutMargins; + if (AS_AVAILABLE_IOS(11)) { + pendingState.insetsLayoutMarginsFromSafeArea = view.insetsLayoutMarginsFromSafeArea; + } pendingState.isAccessibilityElement = view.isAccessibilityElement; pendingState.accessibilityLabel = view.accessibilityLabel; pendingState.accessibilityHint = view.accessibilityHint; @@ -1285,6 +1331,9 @@ static UIColor *defaultTintColor = nil; || flags.setAsyncTransactionContainer || flags.setOpaque || flags.setSemanticContentAttribute + || flags.setLayoutMargins + || flags.setPreservesSuperviewLayoutMargins + || flags.setInsetsLayoutMarginsFromSafeArea || flags.setIsAccessibilityElement || flags.setAccessibilityLabel || flags.setAccessibilityAttributedLabel diff --git a/Tests/ASDisplayNodeTests.mm b/Tests/ASDisplayNodeTests.mm index 50b340890a..ba956321db 100644 --- a/Tests/ASDisplayNodeTests.mm +++ b/Tests/ASDisplayNodeTests.mm @@ -39,6 +39,7 @@ #import #import #import +#import // Conveniences for making nodes named a certain way #define DeclareNodeNamed(n) ASDisplayNode *n = [[ASDisplayNode alloc] init]; n.debugName = @#n @@ -262,6 +263,12 @@ for (ASDisplayNode *n in @[ nodes ]) {\ @end +@interface ASTestViewController: ASViewController +@end +@implementation ASTestViewController +- (BOOL)prefersStatusBarHidden { return YES; } +@end + @interface UIResponderNodeTestDisplayViewCallingSuper : _ASDisplayView @end @implementation UIResponderNodeTestDisplayViewCallingSuper @@ -477,6 +484,10 @@ for (ASDisplayNode *n in @[ nodes ]) {\ XCTAssertEqual(NO, node.exclusiveTouch, @"default exclusiveTouch broken %@", hasLoadedView); XCTAssertEqual(YES, node.autoresizesSubviews, @"default autoresizesSubviews broken %@", hasLoadedView); XCTAssertEqual(UIViewAutoresizingNone, node.autoresizingMask, @"default autoresizingMask broken %@", hasLoadedView); + XCTAssertTrue(UIEdgeInsetsEqualToEdgeInsets(UIEdgeInsetsMake(8, 8, 8, 8), node.layoutMargins), @"default layoutMargins broken %@", hasLoadedView); + XCTAssertEqual(NO, node.preservesSuperviewLayoutMargins, @"default preservesSuperviewLayoutMargins broken %@", hasLoadedView); + XCTAssertTrue(UIEdgeInsetsEqualToEdgeInsets(UIEdgeInsetsZero, node.safeAreaInsets), @"default safeAreaInsets broken %@", hasLoadedView); + XCTAssertEqual(YES, node.insetsLayoutMarginsFromSafeArea, @"default insetsLayoutMarginsFromSafeArea broken %@", hasLoadedView); } else { XCTAssertEqual(NO, node.userInteractionEnabled, @"layer-backed nodes do not support userInteractionEnabled %@", hasLoadedView); XCTAssertEqual(NO, node.exclusiveTouch, @"layer-backed nodes do not support exclusiveTouch %@", hasLoadedView); @@ -584,6 +595,9 @@ for (ASDisplayNode *n in @[ nodes ]) {\ if (!isLayerBacked) { XCTAssertEqual(UIViewAutoresizingFlexibleLeftMargin, node.autoresizingMask, @"autoresizingMask %@", hasLoadedView); XCTAssertEqual(NO, node.autoresizesSubviews, @"autoresizesSubviews broken %@", hasLoadedView); + XCTAssertTrue(UIEdgeInsetsEqualToEdgeInsets(UIEdgeInsetsMake(3, 5, 8, 11), node.layoutMargins), @"layoutMargins broken %@", hasLoadedView); + XCTAssertEqual(YES, node.preservesSuperviewLayoutMargins, @"preservesSuperviewLayoutMargins broken %@", hasLoadedView); + XCTAssertEqual(NO, node.insetsLayoutMarginsFromSafeArea, @"insetsLayoutMarginsFromSafeArea broken %@", hasLoadedView); } } @@ -652,6 +666,9 @@ for (ASDisplayNode *n in @[ nodes ]) {\ node.exclusiveTouch = YES; node.autoresizesSubviews = NO; node.autoresizingMask = UIViewAutoresizingFlexibleLeftMargin; + node.insetsLayoutMarginsFromSafeArea = NO; + node.layoutMargins = UIEdgeInsetsMake(3, 5, 8, 11); + node.preservesSuperviewLayoutMargins = YES; } }]; @@ -2495,6 +2512,50 @@ static bool stringContainsPointer(NSString *description, id p) { XCTAssert(hasVC); } +- (void)testThatSubnodeSafeAreaInsetsAreCalculatedCorrectly +{ + ASDisplayNode *rootNode = [[ASDisplayNode alloc] init]; + ASDisplayNode *subnode = [[ASDisplayNode alloc] init]; + + rootNode.automaticallyManagesSubnodes = YES; + rootNode.layoutSpecBlock = ^ASLayoutSpec * _Nonnull(__kindof ASDisplayNode * _Nonnull node, ASSizeRange constrainedSize) { + return [ASInsetLayoutSpec insetLayoutSpecWithInsets:UIEdgeInsetsMake(1, 2, 3, 4) child:subnode]; + }; + + ASTestViewController *viewController = [[ASTestViewController alloc] initWithNode:rootNode]; + viewController.additionalSafeAreaInsets = UIEdgeInsetsMake(10, 10, 10, 10); + + // It looks like iOS 11 suppresses safeAreaInsets calculation for the views that are not on screen. + UIWindow *window = [[UIWindow alloc] init]; + window.rootViewController = viewController; + [window setHidden:NO]; + [window layoutIfNeeded]; + + UIEdgeInsets expectedRootNodeSafeArea = UIEdgeInsetsMake(10, 10, 10, 10); + UIEdgeInsets expectedSubnodeSafeArea = UIEdgeInsetsMake(9, 8, 7, 6); + + UIEdgeInsets windowSafeArea = UIEdgeInsetsZero; + if (AS_AVAILABLE_IOS(11.0)) { + windowSafeArea = window.safeAreaInsets; + } + + expectedRootNodeSafeArea = ASConcatInsets(expectedRootNodeSafeArea, windowSafeArea); + expectedSubnodeSafeArea = ASConcatInsets(expectedSubnodeSafeArea, windowSafeArea); + + XCTAssertTrue(UIEdgeInsetsEqualToEdgeInsets(expectedRootNodeSafeArea, rootNode.safeAreaInsets), + @"expected rootNode.safeAreaInsets to be %@ but got %@ (window.safeAreaInsets %@)", + NSStringFromUIEdgeInsets(expectedRootNodeSafeArea), + NSStringFromUIEdgeInsets(rootNode.safeAreaInsets), + NSStringFromUIEdgeInsets(windowSafeArea)); + XCTAssertTrue(UIEdgeInsetsEqualToEdgeInsets(expectedSubnodeSafeArea, subnode.safeAreaInsets), + @"expected subnode.safeAreaInsets to be %@ but got %@ (window.safeAreaInsets %@)", + NSStringFromUIEdgeInsets(expectedSubnodeSafeArea), + NSStringFromUIEdgeInsets(subnode.safeAreaInsets), + NSStringFromUIEdgeInsets(windowSafeArea)); + + [window setHidden:YES]; +} + - (void)testScreenScale { XCTAssertEqual(ASScreenScale(), UIScreen.mainScreen.scale);