Skip to content

Commit 726ed10

Browse files
ypogribnyibernieperez
authored andcommitted
[ASDisplayNode] Provide safeAreaInsets and layoutMargins bridge (TextureGroup#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
1 parent 4f66f3f commit 726ed10

13 files changed

+485
-3
lines changed

CHANGELOG.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,6 @@
11
## master
22
* Add your own contributions to the next release on the line below this with your name.
3+
- [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)
34
- [ASPINRemoteImageDownloader] Allow cache to provide animated image. [Max Wang](https://github.com/wsdwsd0829) [#850](https://github.com/TextureGroup/Texture/pull/850)
45
- [tvOS] Fixes errors when building against tvOS SDK [Alex Hill](https://github.com/alexhillc) [#728](https://github.com/TextureGroup/Texture/pull/728)
56
- [ASDisplayNode] Add unit tests for layout z-order changes (with an open issue to fix).

Source/ASDisplayNode.h

Lines changed: 41 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -551,6 +551,20 @@ extern NSInteger const ASDefaultDrawingPriority;
551551
*/
552552
@property (nonatomic, readonly) BOOL supportsLayerBacking;
553553

554+
/**
555+
* Whether or not the node layout should be automatically updated when it receives safeAreaInsetsDidChange.
556+
*
557+
* Defaults to NO.
558+
*/
559+
@property (nonatomic, assign) BOOL automaticallyRelayoutOnSafeAreaChanges;
560+
561+
/**
562+
* Whether or not the node layout should be automatically updated when it receives layoutMarginsDidChange.
563+
*
564+
* Defaults to NO.
565+
*/
566+
@property (nonatomic, assign) BOOL automaticallyRelayoutOnLayoutMarginsChanges;
567+
554568
@end
555569

556570
/**
@@ -725,6 +739,33 @@ extern NSInteger const ASDefaultDrawingPriority;
725739
@property (nonatomic, assign) BOOL autoresizesSubviews; // default==YES (undefined for layer-backed nodes)
726740
@property (nonatomic, assign) UIViewAutoresizing autoresizingMask; // default==UIViewAutoresizingNone (undefined for layer-backed nodes)
727741

742+
/**
743+
* @abstract Content margins
744+
*
745+
* @discussion This property is bridged to its UIView counterpart.
746+
*
747+
* If your layout depends on this property, you should probably enable automaticallyRelayoutOnLayoutMarginsChanges to ensure
748+
* that the layout gets automatically updated when the value of this property changes. Or you can override layoutMarginsDidChange
749+
* and make all the necessary updates manually.
750+
*/
751+
@property (nonatomic, assign) UIEdgeInsets layoutMargins;
752+
@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
753+
- (void)layoutMarginsDidChange;
754+
755+
/**
756+
* @abstract Safe area insets
757+
*
758+
* @discussion This property is bridged to its UIVIew counterpart.
759+
*
760+
* If your layout depends on this property, you should probably enable automaticallyRelayoutOnSafeAreaChanges to ensure
761+
* that the layout gets automatically updated when the value of this property changes. Or you can override safeAreaInsetsDidChange
762+
* and make all the necessary updates manually.
763+
*/
764+
@property (nonatomic, readonly) UIEdgeInsets safeAreaInsets;
765+
@property (nonatomic, assign) BOOL insetsLayoutMarginsFromSafeArea; // Default: YES
766+
- (void)safeAreaInsetsDidChange;
767+
768+
728769
// UIResponder methods
729770
// By default these fall through to the underlying view, but can be overridden.
730771
- (BOOL)canBecomeFirstResponder; // default==NO

Source/ASDisplayNode.mm

Lines changed: 108 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -282,6 +282,14 @@ - (void)_initializeInstance
282282

283283
_flags.canClearContentsOfLayer = YES;
284284
_flags.canCallSetNeedsDisplayOfLayer = YES;
285+
286+
_fallbackSafeAreaInsets = UIEdgeInsetsZero;
287+
_fallbackInsetsLayoutMarginsFromSafeArea = YES;
288+
_isViewControllerRoot = NO;
289+
290+
_automaticallyRelayoutOnSafeAreaChanges = NO;
291+
_automaticallyRelayoutOnLayoutMarginsChanges = NO;
292+
285293
ASDisplayNodeLogEvent(self, @"init");
286294
}
287295

@@ -851,7 +859,104 @@ - (void)nodeViewDidAddGestureRecognizer
851859
_flags.viewEverHadAGestureRecognizerAttached = YES;
852860
}
853861

854-
#pragma mark UIResponder
862+
- (UIEdgeInsets)fallbackSafeAreaInsets
863+
{
864+
ASDN::MutexLocker l(__instanceLock__);
865+
return _fallbackSafeAreaInsets;
866+
}
867+
868+
- (void)setFallbackSafeAreaInsets:(UIEdgeInsets)insets
869+
{
870+
BOOL needsManualUpdate;
871+
BOOL updatesLayoutMargins;
872+
873+
{
874+
ASDN::MutexLocker l(__instanceLock__);
875+
ASDisplayNodeAssertThreadAffinity(self);
876+
877+
if (UIEdgeInsetsEqualToEdgeInsets(insets, _fallbackSafeAreaInsets)) {
878+
return;
879+
}
880+
881+
_fallbackSafeAreaInsets = insets;
882+
needsManualUpdate = !AS_AT_LEAST_IOS11 || _flags.layerBacked;
883+
updatesLayoutMargins = needsManualUpdate && [self _locked_insetsLayoutMarginsFromSafeArea];
884+
}
885+
886+
if (needsManualUpdate) {
887+
[self safeAreaInsetsDidChange];
888+
}
889+
890+
if (updatesLayoutMargins) {
891+
[self layoutMarginsDidChange];
892+
}
893+
}
894+
895+
- (void)_fallbackUpdateSafeAreaOnChildren
896+
{
897+
ASDisplayNodeAssertThreadAffinity(self);
898+
899+
UIEdgeInsets insets = self.safeAreaInsets;
900+
CGRect bounds = self.bounds;
901+
902+
for (ASDisplayNode *child in self.subnodes) {
903+
if (AS_AT_LEAST_IOS11 && !child.layerBacked) {
904+
// In iOS 11 view-backed nodes already know what their safe area is.
905+
continue;
906+
}
907+
908+
if (child.viewControllerRoot) {
909+
// Its safe area is controlled by a view controller. Don't override it.
910+
continue;
911+
}
912+
913+
CGRect childFrame = child.frame;
914+
UIEdgeInsets childInsets = UIEdgeInsetsMake(MAX(insets.top - (CGRectGetMinY(childFrame) - CGRectGetMinY(bounds)), 0),
915+
MAX(insets.left - (CGRectGetMinX(childFrame) - CGRectGetMinX(bounds)), 0),
916+
MAX(insets.bottom - (CGRectGetMaxY(bounds) - CGRectGetMaxY(childFrame)), 0),
917+
MAX(insets.right - (CGRectGetMaxX(bounds) - CGRectGetMaxX(childFrame)), 0));
918+
919+
child.fallbackSafeAreaInsets = childInsets;
920+
}
921+
}
922+
923+
- (BOOL)isViewControllerRoot
924+
{
925+
ASDN::MutexLocker l(__instanceLock__);
926+
return _isViewControllerRoot;
927+
}
928+
929+
- (void)setViewControllerRoot:(BOOL)flag
930+
{
931+
ASDN::MutexLocker l(__instanceLock__);
932+
_isViewControllerRoot = flag;
933+
}
934+
935+
- (BOOL)automaticallyRelayoutOnSafeAreaChanges
936+
{
937+
ASDN::MutexLocker l(__instanceLock__);
938+
return _automaticallyRelayoutOnSafeAreaChanges;
939+
}
940+
941+
- (void)setAutomaticallyRelayoutOnSafeAreaChanges:(BOOL)flag
942+
{
943+
ASDN::MutexLocker l(__instanceLock__);
944+
_automaticallyRelayoutOnSafeAreaChanges = flag;
945+
}
946+
947+
- (BOOL)automaticallyRelayoutOnLayoutMarginsChanges
948+
{
949+
ASDN::MutexLocker l(__instanceLock__);
950+
return _automaticallyRelayoutOnLayoutMarginsChanges;
951+
}
952+
953+
- (void)setAutomaticallyRelayoutOnLayoutMarginsChanges:(BOOL)flag
954+
{
955+
ASDN::MutexLocker l(__instanceLock__);
956+
_automaticallyRelayoutOnLayoutMarginsChanges = flag;
957+
}
958+
959+
#pragma mark - UIResponder
855960

856961
#define HANDLE_NODE_RESPONDER_METHOD(__sel) \
857962
/* All responder methods should be called on the main thread */ \
@@ -1042,6 +1147,8 @@ - (void)__layout
10421147
[self layoutDidFinish];
10431148
});
10441149
}
1150+
1151+
[self _fallbackUpdateSafeAreaOnChildren];
10451152
}
10461153

10471154
- (void)layoutDidFinish

Source/ASViewController.h

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -78,6 +78,11 @@ NS_ASSUME_NONNULL_BEGIN
7878
// Refer to examples/SynchronousConcurrency, AsyncViewController.m
7979
@property (nonatomic, assign) BOOL neverShowPlaceholders;
8080

81+
/* Custom container UIViewController subclasses can use this property to add to the overlay
82+
that UIViewController calculates for the safeAreaInsets for contained view controllers.
83+
*/
84+
@property(nonatomic) UIEdgeInsets additionalSafeAreaInsets;
85+
8186
@end
8287

8388
@interface ASViewController (ASRangeControllerUpdateRangeProtocol)

Source/ASViewController.mm

Lines changed: 38 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -32,6 +32,7 @@ @implementation ASViewController
3232
NSInteger _visibilityDepth;
3333
BOOL _selfConformsToRangeModeProtocol;
3434
BOOL _nodeConformsToRangeModeProtocol;
35+
UIEdgeInsets _fallbackAdditionalSafeAreaInsets;
3536
}
3637

3738
- (instancetype)initWithNibName:(NSString *)nibNameOrNil bundle:(NSBundle *)nibBundleOrNil
@@ -73,10 +74,14 @@ - (void)_initializeInstance
7374
if (_node == nil) {
7475
return;
7576
}
77+
78+
_node.viewControllerRoot = YES;
7679

7780
_selfConformsToRangeModeProtocol = [self conformsToProtocol:@protocol(ASRangeControllerUpdateRangeProtocol)];
7881
_nodeConformsToRangeModeProtocol = [_node conformsToProtocol:@protocol(ASRangeControllerUpdateRangeProtocol)];
7982
_automaticallyAdjustRangeModeBasedOnViewEvents = _selfConformsToRangeModeProtocol || _nodeConformsToRangeModeProtocol;
83+
84+
_fallbackAdditionalSafeAreaInsets = UIEdgeInsetsZero;
8085

8186
// In case the node will get loaded
8287
if (_node.nodeLoaded) {
@@ -159,6 +164,20 @@ - (void)viewDidLayoutSubviews
159164
[_node recursivelyEnsureDisplaySynchronously:YES];
160165
}
161166
[super viewDidLayoutSubviews];
167+
168+
if (!AS_AT_LEAST_IOS11) {
169+
[self _updateNodeFallbackSafeArea];
170+
}
171+
}
172+
173+
- (void)_updateNodeFallbackSafeArea
174+
{
175+
UIEdgeInsets safeArea = UIEdgeInsetsMake(self.topLayoutGuide.length, 0, self.bottomLayoutGuide.length, 0);
176+
UIEdgeInsets additionalInsets = self.additionalSafeAreaInsets;
177+
178+
safeArea = ASConcatInsets(safeArea, additionalInsets);
179+
180+
_node.fallbackSafeAreaInsets = safeArea;
162181
}
163182

164183
ASVisibilityDidMoveToParentViewController;
@@ -264,6 +283,25 @@ - (ASInterfaceState)interfaceState
264283
return _node.interfaceState;
265284
}
266285

286+
- (UIEdgeInsets)additionalSafeAreaInsets
287+
{
288+
if (AS_AVAILABLE_IOS(11.0)) {
289+
return super.additionalSafeAreaInsets;
290+
}
291+
292+
return _fallbackAdditionalSafeAreaInsets;
293+
}
294+
295+
- (void)setAdditionalSafeAreaInsets:(UIEdgeInsets)additionalSafeAreaInsets
296+
{
297+
if (AS_AVAILABLE_IOS(11.0)) {
298+
[super setAdditionalSafeAreaInsets:additionalSafeAreaInsets];
299+
} else {
300+
_fallbackAdditionalSafeAreaInsets = additionalSafeAreaInsets;
301+
[self _updateNodeFallbackSafeArea];
302+
}
303+
}
304+
267305
#pragma mark - ASTraitEnvironment
268306

269307
- (ASPrimitiveTraitCollection)primitiveTraitCollectionForUITraitCollection:(UITraitCollection *)traitCollection

Source/Details/UIView+ASConvenience.h

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -75,6 +75,9 @@ NS_ASSUME_NONNULL_BEGIN
7575
@property (nonatomic, assign, getter=isUserInteractionEnabled) BOOL userInteractionEnabled;
7676
@property (nonatomic, assign, getter=isExclusiveTouch) BOOL exclusiveTouch;
7777
@property (nonatomic, assign, getter=asyncdisplaykit_isAsyncTransactionContainer, setter = asyncdisplaykit_setAsyncTransactionContainer:) BOOL asyncdisplaykit_asyncTransactionContainer;
78+
@property (nonatomic, assign) UIEdgeInsets layoutMargins;
79+
@property (nonatomic, assign) BOOL preservesSuperviewLayoutMargins;
80+
@property (nonatomic, assign) BOOL insetsLayoutMarginsFromSafeArea;
7881

7982
/**
8083
Following properties of the UIAccessibility informal protocol are supported as well.

Source/Details/_ASDisplayView.mm

Lines changed: 29 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -21,11 +21,13 @@
2121
#import <AsyncDisplayKit/_ASCoreAnimationExtras.h>
2222
#import <AsyncDisplayKit/_ASDisplayLayer.h>
2323
#import <AsyncDisplayKit/ASDisplayNodeInternal.h>
24+
#import <AsyncDisplayKit/ASDisplayNode+Convenience.h>
2425
#import <AsyncDisplayKit/ASDisplayNode+FrameworkPrivate.h>
2526
#import <AsyncDisplayKit/ASDisplayNode+Subclasses.h>
2627
#import <AsyncDisplayKit/ASInternalHelpers.h>
27-
#import <AsyncDisplayKit/ASObjectDescriptionHelpers.h>
2828
#import <AsyncDisplayKit/ASLayout.h>
29+
#import <AsyncDisplayKit/ASObjectDescriptionHelpers.h>
30+
#import <AsyncDisplayKit/ASViewController.h>
2931

3032
#pragma mark - _ASDisplayViewMethodOverrides
3133

@@ -234,6 +236,16 @@ - (void)didMoveToSuperview
234236
self.keepalive_node = nil;
235237
}
236238

239+
#if DEBUG
240+
// This is only to help detect issues when a root-of-view-controller node is reused separately from its view controller.
241+
// Avoid overhead in release.
242+
if (superview && node.viewControllerRoot) {
243+
UIViewController *vc = [node closestViewController];
244+
245+
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.");
246+
}
247+
#endif
248+
237249
ASDisplayNode *supernode = node.supernode;
238250
ASDisplayNodeAssert(!supernode.isLayerBacked, @"Shouldn't be possible for superview's node to be layer-backed.");
239251

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

496+
- (void)layoutMarginsDidChange
497+
{
498+
ASDisplayNode *node = _asyncdisplaykit_node; // Create strong reference to weak ivar.
499+
[super layoutMarginsDidChange];
500+
501+
[node layoutMarginsDidChange];
502+
}
503+
504+
- (void)safeAreaInsetsDidChange
505+
{
506+
ASDisplayNode *node = _asyncdisplaykit_node; // Create strong reference to weak ivar.
507+
[super safeAreaInsetsDidChange];
508+
509+
[node safeAreaInsetsDidChange];
510+
}
511+
484512
- (id)forwardingTargetForSelector:(SEL)aSelector
485513
{
486514
// Ideally, we would implement -targetForAction:withSender: and simply return the node where we don't respond personally.

Source/Private/ASDisplayNode+FrameworkPrivate.h

Lines changed: 19 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -241,6 +241,25 @@ __unused static NSString * _Nonnull NSStringFromASHierarchyStateChange(ASHierarc
241241
*/
242242
- (BOOL)shouldScheduleDisplayWithNewInterfaceState:(ASInterfaceState)newInterfaceState;
243243

244+
/**
245+
* @abstract safeAreaInsets will fallback to this value if the corresponding UIKit property is not available
246+
* (due to an old iOS version).
247+
*
248+
* @discussion This should be set by the owning view controller based on it's layout guides.
249+
* If this is not a view controllet's node the value will be calculated automatically by the parent node.
250+
*/
251+
@property (nonatomic, assign) UIEdgeInsets fallbackSafeAreaInsets;
252+
253+
/**
254+
* @abstract Indicates if this node is a view controller's root node. Defaults to NO.
255+
*
256+
* @discussion Set to YES in -[ASViewController initWithNode:].
257+
*
258+
* YES here only means that this node is used as an ASViewController node. It doesn't mean that this node is a root of
259+
* ASDisplayNode hierarchy, e.g. when its view controller is parented by another ASViewController.
260+
*/
261+
@property (nonatomic, assign, getter=isViewControllerRoot) BOOL viewControllerRoot;
262+
244263
@end
245264

246265

0 commit comments

Comments
 (0)