From 1545384c7c28a6e3bbfdfd993d0414f866e93e50 Mon Sep 17 00:00:00 2001 From: James Ide Date: Mon, 19 Jan 2015 20:10:11 -0800 Subject: [PATCH] Let ASDisplayNode take a block that returns the backing view/layer This adds new initializer methods to ASDisplayNode: ```objc initWithViewBlock:(ASDisplayNodeViewBlock)viewBlock initWithLayerBlock:(ASDisplayNodeLayerBlock)layerBlock ``` Sometimes a view can't be constructed with `-[initWithViewClass:]` but you want to use it with ASDK, so these new methods provide a way to wrap an existing view in a node. The API is meant to preserve ASDisplayNode's behavior, so you can still construct and set properties on the node on a background queue before its view is loaded; even though the view was created a priori, it is not considered to be loaded until `node.view` is accessed. Using the API looks like this: dispatch_async(backgroundQueue, ^{ ASDisplayNode *node = [ASDisplayNode alloc] initWithViewBlock:^{ // Guaranteed to run on the main queue UIButton *button = [UIButton buttonWithType:UIButtonTypeSystem]; [button sizeToFit]; node.frame = button.frame; return button; }]; // Use `node` as you normally would... node.backgroundColor = [UIColor redColor]; }); The main thing this bridging API doesn't do (can't do?) is layout. Methods like `-[ASDisplayNode calculateSizeThatFits:]` and `-[ASDisplayNode layout]` cannot delegate to `[UIView sizeThatFits:]` and `[UIView layoutSubviews]` since the UIView methods must run on the main thread. If ASDK were internally asynchronous and could dispatch its layout methods to different threads (sort of like how ASTableView computes its cells' layouts) then we could mark nodes with externally provided views/layers as having "main-queue affinity" and delegate its layout to UIKit. Test cases are included and all existing tests pass. --- AsyncDisplayKit/ASDisplayNode.h | 18 ++ AsyncDisplayKit/ASDisplayNode.mm | 89 ++++++++- .../Private/ASDisplayNodeInternal.h | 2 + AsyncDisplayKitTests/ASDisplayNodeTests.m | 180 ++++++++++++++++++ 4 files changed, 280 insertions(+), 9 deletions(-) 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];