Merge pull request #235 from ide/bridge-node

ASBridgeNode: wrapper around an existing UIView with ASDisplayNode semantics
This commit is contained in:
Nadine Salter 2015-01-23 12:29:50 -08:00
commit 46f2d723f2
4 changed files with 280 additions and 9 deletions

View File

@ -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 */

View File

@ -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];

View File

@ -56,6 +56,8 @@ typedef NS_OPTIONS(NSUInteger, ASDisplayNodeMethodOverrides) {
UIEdgeInsets _hitTestSlop;
NSMutableArray *_subnodes;
ASDisplayNodeViewBlock _viewBlock;
ASDisplayNodeLayerBlock _layerBlock;
Class _viewClass;
Class _layerClass;
UIView *_view;

View File

@ -11,6 +11,7 @@
#import <XCTest/XCTest.h>
#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];