diff --git a/AsyncDisplayKit/ASDisplayNode.h b/AsyncDisplayKit/ASDisplayNode.h index 2065a9fcf4..e635080665 100644 --- a/AsyncDisplayKit/ASDisplayNode.h +++ b/AsyncDisplayKit/ASDisplayNode.h @@ -12,6 +12,8 @@ #import "ASBaseDefines.h" #import "ASDealloc2MainObject.h" +typedef UIView *(^ASDisplayNodeViewBlock)(); +typedef CALayer *(^ASDisplayNodeLayerBlock)(); /** * An `ASDisplayNode` is an abstraction over `UIView` and `CALayer` that allows you to perform calculations about a view @@ -69,6 +71,22 @@ */ - (id)initWithLayerClass:(Class)layerClass; +/** + * @abstract Alternative initializer with a block to create the backing view. + * + * @return An ASDisplayNode instance that loads its view with the given block that is guaranteed to run on the main + * queue. The view will render synchronously and -layout and touch handling methods on the node will not be called. + */ +- (id)initWithViewBlock:(ASDisplayNodeViewBlock)viewBlock; + +/** + * @abstract Alternative initializer with a block to create the backing layer. + * + * @return An ASDisplayNode instance that loads its layer with the given block that is guaranteed to run on the main + * queue. The layer will render synchronously and -layout and touch handling methods on the node will not be called. + */ +- (id)initWithLayerBlock:(ASDisplayNodeLayerBlock)viewBlock; + /** @name Properties */ diff --git a/AsyncDisplayKit/ASDisplayNode.mm b/AsyncDisplayKit/ASDisplayNode.mm index 2669bc68e6..1e3609ca96 100644 --- a/AsyncDisplayKit/ASDisplayNode.mm +++ b/AsyncDisplayKit/ASDisplayNode.mm @@ -181,6 +181,35 @@ void ASDisplayNodePerformBlockOnMainThread(void (^block)()) return self; } +- (id)initWithViewBlock:(ASDisplayNodeViewBlock)viewBlock +{ + if (!(self = [super init])) + return nil; + + ASDisplayNodeAssertNotNil(viewBlock, @"should initialize with a valid block that returns a UIView"); + + [self _initializeInstance]; + _viewBlock = viewBlock; + _flags.synchronous = YES; + + return self; +} + +- (id)initWithLayerBlock:(ASDisplayNodeLayerBlock)layerBlock +{ + if (!(self = [super init])) + return nil; + + ASDisplayNodeAssertNotNil(layerBlock, @"should initialize with a valid block that returns a CALayer"); + + [self _initializeInstance]; + _layerBlock = layerBlock; + _flags.synchronous = YES; + _flags.layerBacked = YES; + + return self; +} + - (void)dealloc { ASDisplayNodeAssertMainThread(); @@ -249,6 +278,48 @@ void ASDisplayNodePerformBlockOnMainThread(void (^block)()) } +- (UIView *)_viewToLoad +{ + UIView *view; + ASDN::MutexLocker l(_propertyLock); + + if (_viewBlock) { + view = _viewBlock(); + ASDisplayNodeAssertNotNil(view, @"View block returned nil"); + ASDisplayNodeAssert(![view isKindOfClass:[_ASDisplayView class]], @"View block should return a synchronously displayed view"); + _viewBlock = nil; + _viewClass = [view class]; + } else { + if (!_viewClass) { + _viewClass = [self.class viewClass]; + } + view = [[_viewClass alloc] init]; + } + + return view; +} + +- (CALayer *)_layerToLoad +{ + CALayer *layer; + ASDN::MutexLocker l(_propertyLock); + + if (_layerBlock) { + layer = _layerBlock(); + ASDisplayNodeAssertNotNil(layer, @"Layer block returned nil"); + ASDisplayNodeAssert(![layer isKindOfClass:[_ASDisplayLayer class]], @"Layer block should return a synchronously displayed layer"); + _layerBlock = nil; + _layerClass = [layer class]; + } else { + if (!_layerClass) { + _layerClass = [self.class layerClass]; + } + layer = [[_layerClass alloc] init]; + } + + return layer; +} + - (void)_loadViewOrLayerIsLayerBacked:(BOOL)isLayerBacked { ASDN::MutexLocker l(_propertyLock); @@ -263,18 +334,11 @@ void ASDisplayNodePerformBlockOnMainThread(void (^block)()) if (isLayerBacked) { TIME_SCOPED(_debugTimeToCreateView); - if (!_layerClass) { - _layerClass = [self.class layerClass]; - } - - _layer = [[_layerClass alloc] init]; + _layer = [self _layerToLoad]; _layer.delegate = self; } else { TIME_SCOPED(_debugTimeToCreateView); - if (!_viewClass) { - _viewClass = [self.class viewClass]; - } - _view = [[_viewClass alloc] init]; + _view = [self _viewToLoad]; _view.asyncdisplaykit_node = self; _layer = _view.layer; } @@ -363,6 +427,9 @@ void ASDisplayNodePerformBlockOnMainThread(void (^block)()) ASDN::MutexLocker l(_propertyLock); ASDisplayNodeAssert(!_view && !_layer, @"Cannot change isLayerBacked after layer or view has loaded"); + ASDisplayNodeAssert(!_viewBlock && !_layerBlock, @"Cannot change isLayerBacked when a layer or view block is provided"); + ASDisplayNodeAssert(!_viewClass && !_layerClass, @"Cannot change isLayerBacked when a layer or view class is provided"); + if (isLayerBacked != _flags.layerBacked && !_view && !_layer) { _flags.layerBacked = isLayerBacked; } @@ -1616,6 +1683,10 @@ static void _recursivelySetDisplaySuspended(ASDisplayNode *node, CALayer *layer, notableTargetDesc = [NSString stringWithFormat:@" [%@]", _viewClass]; } else if (_layerClass) { // Nonstandard layer class unloaded notableTargetDesc = [NSString stringWithFormat:@" [%@]", _layerClass]; + } else if (_viewBlock) { // Nonstandard lazy view unloaded + notableTargetDesc = @" [block]"; + } else if (_layerBlock) { // Nonstandard lazy layer unloaded + notableTargetDesc = @" [block]"; } if (self.name) { return [NSString stringWithFormat:@"<%@ %p name = %@%@>", self.class, self, self.name, notableTargetDesc]; diff --git a/AsyncDisplayKit/Private/ASDisplayNodeInternal.h b/AsyncDisplayKit/Private/ASDisplayNodeInternal.h index 8cc6be571a..50c9a4a42a 100644 --- a/AsyncDisplayKit/Private/ASDisplayNodeInternal.h +++ b/AsyncDisplayKit/Private/ASDisplayNodeInternal.h @@ -56,6 +56,8 @@ typedef NS_OPTIONS(NSUInteger, ASDisplayNodeMethodOverrides) { UIEdgeInsets _hitTestSlop; NSMutableArray *_subnodes; + ASDisplayNodeViewBlock _viewBlock; + ASDisplayNodeLayerBlock _layerBlock; Class _viewClass; Class _layerClass; UIView *_view; diff --git a/AsyncDisplayKitTests/ASDisplayNodeTests.m b/AsyncDisplayKitTests/ASDisplayNodeTests.m index fbb652ec98..67726020ab 100644 --- a/AsyncDisplayKitTests/ASDisplayNodeTests.m +++ b/AsyncDisplayKitTests/ASDisplayNodeTests.m @@ -11,6 +11,7 @@ #import #import "_ASDisplayLayer.h" +#import "_ASDisplayView.h" #import "ASDisplayNode+Subclasses.h" #import "ASDisplayNodeTestsHelper.h" #import "UIView+ASConvenience.h" @@ -87,6 +88,12 @@ for (ASDisplayNode *n in @[ nodes ]) {\ @end +@interface UIDisplayNodeTestView : UIView +@end + +@implementation UIDisplayNodeTestView +@end + @interface ASDisplayNodeTests : XCTestCase @end @@ -118,6 +125,53 @@ for (ASDisplayNode *n in @[ nodes ]) {\ XCTAssertNotNil(view, @"Getting node's view on-thread should succeed."); } +- (void)testNodeCreatedOffThreadWithExistingView +{ + UIView *view = [[UIDisplayNodeTestView alloc] init]; + + __block ASDisplayNode *node = nil; + [self executeOffThread:^{ + node = [[ASDisplayNode alloc] initWithViewBlock:^UIView *{ + return view; + }]; + }]; + + XCTAssertFalse(node.layerBacked, @"Can't be layer backed"); + XCTAssertTrue(node.synchronous, @"Node with plain view should be synchronous"); + XCTAssertFalse(node.nodeLoaded, @"Shouldn't have a view yet"); + XCTAssertEqual(view, node.view, @"Getting node's view on-thread should succeed."); +} + +- (void)testNodeCreatedOffThreadWithLazyView +{ + __block UIView *view = nil; + __block ASDisplayNode *node = nil; + [self executeOffThread:^{ + node = [[ASDisplayNode alloc] initWithViewBlock:^UIView *{ + XCTAssertTrue([NSThread isMainThread], @"View block must run on the main queue"); + view = [[UIDisplayNodeTestView alloc] init]; + return view; + }]; + }]; + + XCTAssertNil(view, @"View block should not be invoked yet"); + [node view]; + XCTAssertNotNil(view, @"View block should have been invoked"); + XCTAssertEqual(view, node.view, @"Getting node's view on-thread should succeed."); + XCTAssertTrue(node.synchronous, @"Node with plain view should be synchronous"); +} + +- (void)testNodeCreatedWithLazyAsyncView +{ + ASDisplayNode *node = [[ASDisplayNode alloc] initWithViewBlock:^UIView *{ + XCTAssertTrue([NSThread isMainThread], @"View block must run on the main queue"); + return [[_ASDisplayView alloc] init]; + }]; + + XCTAssertThrows([node view], @"Externally provided views should be synchronous"); + XCTAssertTrue(node.synchronous, @"Node with externally provided view should be synchronous"); +} + - (void)checkValuesMatchDefaults:(ASDisplayNode *)node isLayerBacked:(BOOL)isLayerBacked { NSString *targetName = isLayerBacked ? @"layer" : @"view"; @@ -350,6 +404,92 @@ for (ASDisplayNode *n in @[ nodes ]) {\ [self checkSimpleBridgePropertiesSetPropagate:YES]; } +- (void)testPropertiesSetOffThreadBeforeLoadingExternalView +{ + UIView *view = [[UIDisplayNodeTestView alloc] init]; + + __block ASDisplayNode *node = nil; + [self executeOffThread:^{ + node = [[ASDisplayNode alloc] initWithViewBlock:^{ + return view; + }]; + node.backgroundColor = [UIColor blueColor]; + node.frame = CGRectMake(10, 20, 30, 40); + node.autoresizingMask = UIViewAutoresizingFlexibleWidth; + node.userInteractionEnabled = YES; + }]; + + [self checkExternalViewAppliedPropertiesMatch:node]; +} + +- (void)testPropertiesSetOnThreadAfterLoadingExternalView +{ + UIView *view = [[UIDisplayNodeTestView alloc] init]; + ASDisplayNode *node = [[ASDisplayNode alloc] initWithViewBlock:^{ + return view; + }]; + + // Load the backing view first + [node view]; + + node.backgroundColor = [UIColor blueColor]; + node.frame = CGRectMake(10, 20, 30, 40); + node.autoresizingMask = UIViewAutoresizingFlexibleWidth; + node.userInteractionEnabled = YES; + + [self checkExternalViewAppliedPropertiesMatch:node]; +} + +- (void)checkExternalViewAppliedPropertiesMatch:(ASDisplayNode *)node +{ + UIView *view = node.view; + + XCTAssertEqualObjects([UIColor blueColor], view.backgroundColor, @"backgroundColor not propagated to view"); + XCTAssertTrue(CGRectEqualToRect(CGRectMake(10, 20, 30, 40), view.frame), @"frame not propagated to view"); + XCTAssertEqual(UIViewAutoresizingFlexibleWidth, view.autoresizingMask, @"autoresizingMask not propagated to view"); + XCTAssertEqual(YES, view.userInteractionEnabled, @"userInteractionEnabled not propagated to view"); +} + +- (void)testPropertiesSetOffThreadBeforeLoadingExternalLayer +{ + CALayer *layer = [[CAShapeLayer alloc] init]; + + __block ASDisplayNode *node = nil; + [self executeOffThread:^{ + node = [[ASDisplayNode alloc] initWithLayerBlock:^{ + return layer; + }]; + node.backgroundColor = [UIColor blueColor]; + node.frame = CGRectMake(10, 20, 30, 40); + }]; + + [self checkExternalLayerAppliedPropertiesMatch:node]; +} + +- (void)testPropertiesSetOnThreadAfterLoadingExternalLayer +{ + CALayer *layer = [[CAShapeLayer alloc] init]; + ASDisplayNode *node = [[ASDisplayNode alloc] initWithLayerBlock:^{ + return layer; + }]; + + // Load the backing layer first + [node layer]; + + node.backgroundColor = [UIColor blueColor]; + node.frame = CGRectMake(10, 20, 30, 40); + + [self checkExternalLayerAppliedPropertiesMatch:node]; +} + +- (void)checkExternalLayerAppliedPropertiesMatch:(ASDisplayNode *)node +{ + CALayer *layer = node.layer; + + XCTAssertTrue(CGColorEqualToColor([UIColor blueColor].CGColor, layer.backgroundColor), @"backgroundColor not propagated to layer"); + XCTAssertTrue(CGRectEqualToRect(CGRectMake(10, 20, 30, 40), layer.frame), @"frame not propagated to layer"); +} + // Perform parallel updates of a standard UIView/CALayer and an ASDisplayNode and ensure they are equivalent. - (void)testDeriveFrameFromBoundsPositionAnchorPoint @@ -1355,6 +1495,46 @@ static inline BOOL _CGPointEqualToPointWithEpsilon(CGPoint point1, CGPoint point [c release]; } +- (void)testSubnodeAddedBeforeLoadingExternalView +{ + UIView *view = [[UIDisplayNodeTestView alloc] init]; + + __block ASDisplayNode *parent = nil; + __block ASDisplayNode *child = nil; + [self executeOffThread:^{ + parent = [[ASDisplayNode alloc] initWithViewBlock:^{ + return view; + }]; + child = [[ASDisplayNode alloc] init]; + [parent addSubnode:child]; + }]; + + XCTAssertEqual(1, parent.subnodes.count, @"Parent should have 1 subnode"); + XCTAssertEqualObjects(parent, child.supernode, @"Child has the wrong parent"); + XCTAssertEqual(0, view.subviews.count, @"View shouldn't have any subviews"); + + [parent view]; + + XCTAssertEqual(1, view.subviews.count, @"View should have 1 subview"); +} + +- (void)testSubnodeAddedAfterLoadingExternalView +{ + UIView *view = [[UIDisplayNodeTestView alloc] init]; + ASDisplayNode *parent = [[ASDisplayNode alloc] initWithViewBlock:^{ + return view; + }]; + + [parent view]; + + ASDisplayNode *child = [[ASDisplayNode alloc] init]; + [parent addSubnode:child]; + + XCTAssertEqual(1, parent.subnodes.count, @"Parent should have 1 subnode"); + XCTAssertEqualObjects(parent, child.supernode, @"Child has the wrong parent"); + XCTAssertEqual(1, view.subviews.count, @"View should have 1 subview"); +} + - (void)checkBackgroundColorOpaqueRelationshipWithViewLoaded:(BOOL)loaded layerBacked:(BOOL)isLayerBacked { ASDisplayNode *node = [[ASDisplayNode alloc] init];