From 44feece701e964c95f9ea0f958d8171cd2bb5f86 Mon Sep 17 00:00:00 2001 From: Scott Goodson Date: Sat, 26 Dec 2015 23:04:16 -0800 Subject: [PATCH] Implement node-backing for ASTableView and ASCollectionView, with a strong back-pointer in these cases. --- AsyncDisplayKit/ASCollectionNode.m | 13 +++++- AsyncDisplayKit/ASCollectionView.h | 12 ++++-- AsyncDisplayKit/ASCollectionView.mm | 25 +++++++++--- AsyncDisplayKit/ASDisplayNode.h | 1 + AsyncDisplayKit/ASTableNode.m | 25 ++++++++---- AsyncDisplayKit/ASTableView.h | 12 ++++-- AsyncDisplayKit/ASTableView.mm | 40 +++++++++++-------- AsyncDisplayKit/ASTableViewInternal.h | 2 +- AsyncDisplayKit/Layout/ASLayoutOptions.h | 2 +- AsyncDisplayKit/Layout/ASLayoutOptions.mm | 4 +- .../Private/ASDisplayNodeInternal.h | 14 +++++++ AsyncDisplayKitTests/ASTableViewTests.m | 36 +++++++---------- 12 files changed, 124 insertions(+), 62 deletions(-) diff --git a/AsyncDisplayKit/ASCollectionNode.m b/AsyncDisplayKit/ASCollectionNode.m index 1b8484b777..d75d4ad58a 100644 --- a/AsyncDisplayKit/ASCollectionNode.m +++ b/AsyncDisplayKit/ASCollectionNode.m @@ -22,7 +22,7 @@ @end @interface ASCollectionView () -- (instancetype)_initWithFrame:(CGRect)frame collectionViewLayout:(UICollectionViewLayout *)layout; +- (instancetype)_initWithFrame:(CGRect)frame collectionViewLayout:(UICollectionViewLayout *)layout ownedByNode:(BOOL)ownedByNode; @end @implementation ASCollectionNode @@ -40,10 +40,19 @@ return [self initWithFrame:CGRectZero collectionViewLayout:layout]; } +- (instancetype)_initWithCollectionView:(ASCollectionView *)collectionView +{ + if (self = [super initWithViewBlock:^UIView *{ return collectionView; }]) { + __unused ASCollectionView *collectionView = [self view]; + return self; + } + return nil; +} + - (instancetype)initWithFrame:(CGRect)frame collectionViewLayout:(UICollectionViewLayout *)layout { ASDisplayNodeViewBlock collectionViewBlock = ^UIView *{ - return [[ASCollectionView alloc] _initWithFrame:frame collectionViewLayout:layout]; + return [[ASCollectionView alloc] _initWithFrame:frame collectionViewLayout:layout ownedByNode:YES]; }; if (self = [super initWithViewBlock:collectionViewBlock]) { diff --git a/AsyncDisplayKit/ASCollectionView.h b/AsyncDisplayKit/ASCollectionView.h index 55b3afafb1..fb818eb7e6 100644 --- a/AsyncDisplayKit/ASCollectionView.h +++ b/AsyncDisplayKit/ASCollectionView.h @@ -22,10 +22,16 @@ NS_ASSUME_NONNULL_BEGIN /** - * Node-based collection view. + * Asynchronous UICollectionView with Intelligent Preloading capabilities. * - * ASCollectionView is a version of UICollectionView that uses nodes -- specifically, ASCellNode subclasses -- with asynchronous - * pre-rendering instead of synchronously loading UICollectionViewCells. + * ASCollectionNode is recommended over ASCollectionView. This class exists for adoption convenience. + * + * ASCollectionView is a true subclass of UICollectionView, meaning it is pointer-compatible + * with code that currently uses UICollectionView. + * + * The main difference is that asyncDataSource expects -nodeForItemAtIndexPath, an ASCellNode, and + * the sizeForItemAtIndexPath: method is eliminated (as are the performance problems caused by it). + * This is made possible because ASCellNodes can calculate their own size, and preload ahead of time. */ @interface ASCollectionView : UICollectionView diff --git a/AsyncDisplayKit/ASCollectionView.mm b/AsyncDisplayKit/ASCollectionView.mm index 7c2bcfa42d..2e7c17c017 100644 --- a/AsyncDisplayKit/ASCollectionView.mm +++ b/AsyncDisplayKit/ASCollectionView.mm @@ -99,6 +99,16 @@ static NSString * const kCellReuseIdentifier = @"_ASCollectionViewCell"; @property (atomic, assign) BOOL asyncDataSourceLocked; +// Used only when ASCollectionView is created directly rather than through ASCollectionNode. +// We create a node so that logic related to appearance, memory management, etc can be located there +// for both the node-based and view-based version of the table. +// This also permits sharing logic with ASTableNode, as the superclass is not UIKit-controlled. +@property (nonatomic, retain) ASCollectionNode *strongCollectionNode; + +@end + +@interface ASCollectionNode () +- (instancetype)_initWithCollectionView:(ASCollectionView *)collectionView; @end @implementation ASCollectionView @@ -108,26 +118,31 @@ static NSString * const kCellReuseIdentifier = @"_ASCollectionViewCell"; - (instancetype)initWithCollectionViewLayout:(UICollectionViewLayout *)layout { - return [self initWithFrame:CGRectZero collectionViewLayout:layout]; + return [self _initWithFrame:CGRectZero collectionViewLayout:layout ownedByNode:NO]; } - (instancetype)initWithFrame:(CGRect)frame collectionViewLayout:(UICollectionViewLayout *)layout { - ASCollectionNode *collectionNode = [[ASCollectionNode alloc] initWithFrame:frame collectionViewLayout:layout]; - return collectionNode.view; + return [self _initWithFrame:frame collectionViewLayout:layout ownedByNode:NO]; } // FIXME: This method is deprecated and will probably be removed in or shortly after 2.0. - (instancetype)initWithFrame:(CGRect)frame collectionViewLayout:(UICollectionViewLayout *)layout asyncDataFetching:(BOOL)asyncDataFetchingEnabled { - return [self initWithFrame:frame collectionViewLayout:layout]; + return [self _initWithFrame:frame collectionViewLayout:layout ownedByNode:NO]; } -- (instancetype)_initWithFrame:(CGRect)frame collectionViewLayout:(UICollectionViewLayout *)layout +- (instancetype)_initWithFrame:(CGRect)frame collectionViewLayout:(UICollectionViewLayout *)layout ownedByNode:(BOOL)ownedByNode { if (!(self = [super initWithFrame:frame collectionViewLayout:layout])) return nil; + if (!ownedByNode) { + // See commentary at the definition of .strongCollectionNode for why we create an ASCollectionNode. + ASCollectionNode *collectionNode = [[ASCollectionNode alloc] _initWithCollectionView:self]; + self.strongCollectionNode = collectionNode; + } + _layoutController = [[ASCollectionViewLayoutController alloc] initWithCollectionView:self]; _rangeController = [[ASRangeController alloc] init]; diff --git a/AsyncDisplayKit/ASDisplayNode.h b/AsyncDisplayKit/ASDisplayNode.h index 84d29f0f81..3e2a32fb44 100644 --- a/AsyncDisplayKit/ASDisplayNode.h +++ b/AsyncDisplayKit/ASDisplayNode.h @@ -205,6 +205,7 @@ NS_ASSUME_NONNULL_BEGIN */ @property (nonatomic, readonly) ASInterfaceState interfaceState; + /** @name Managing dimensions */ /** diff --git a/AsyncDisplayKit/ASTableNode.m b/AsyncDisplayKit/ASTableNode.m index 2917b73cc8..e81366ed99 100644 --- a/AsyncDisplayKit/ASTableNode.m +++ b/AsyncDisplayKit/ASTableNode.m @@ -7,7 +7,7 @@ // #import "ASFlowLayoutController.h" -#import "ASTableNode.h" +#import "ASTableViewInternal.h" #import "ASDisplayNode+Subclasses.h" @interface _ASTablePendingState : NSObject @@ -28,11 +28,22 @@ @implementation ASTableNode -- (instancetype)_initWithStyle:(UITableViewStyle)style dataControllerClass:(Class)dataControllerClass +- (instancetype)_initWithTableView:(ASTableView *)tableView { - if (self = [super initWithViewBlock:^UIView *{ return [[ASTableView alloc] _initWithFrame:CGRectZero - style:style - dataControllerClass:dataControllerClass]; }]) { + if (self = [super initWithViewBlock:^UIView *{ return tableView; }]) { + __unused ASTableView *tableView = [self view]; + return self; + } + return nil; +} + +- (instancetype)_initWithFrame:(CGRect)frame style:(UITableViewStyle)style dataControllerClass:(Class)dataControllerClass +{ + ASDisplayNodeViewBlock tableViewBlock = ^UIView *{ + return [[ASTableView alloc] _initWithFrame:frame style:style dataControllerClass:dataControllerClass ownedByNode:YES]; + }; + + if (self = [super initWithViewBlock:tableViewBlock]) { return self; } return nil; @@ -40,12 +51,12 @@ - (instancetype)initWithStyle:(UITableViewStyle)style { - return [self _initWithStyle:style dataControllerClass:nil]; + return [self _initWithFrame:CGRectZero style:style dataControllerClass:nil]; } - (instancetype)init { - return [self _initWithStyle:UITableViewStylePlain dataControllerClass:nil]; + return [self _initWithFrame:CGRectZero style:UITableViewStylePlain dataControllerClass:nil]; } - (void)didLoad diff --git a/AsyncDisplayKit/ASTableView.h b/AsyncDisplayKit/ASTableView.h index d3e2b05b51..f68606c66e 100644 --- a/AsyncDisplayKit/ASTableView.h +++ b/AsyncDisplayKit/ASTableView.h @@ -19,10 +19,16 @@ NS_ASSUME_NONNULL_BEGIN @protocol ASTableDelegate; /** - * Node-based table view. + * Asynchronous UITableView with Intelligent Preloading capabilities. * - * ASTableView is a version of UITableView that uses nodes -- specifically, ASCellNode subclasses -- with asynchronous - * pre-rendering instead of synchronously loading UITableViewCells. + * ASTableNode is recommended over ASTableView. This class is provided for adoption convenience. + * + * ASTableView is a true subclass of UITableView, meaning it is pointer-compatible with code that + * currently uses UITableView + * + * The main difference is that asyncDataSource expects -nodeForRowAtIndexPath, an ASCellNode, and + * the heightForRowAtIndexPath: method is eliminated (as are the performance problems caused by it). + * This is made possible because ASCellNodes can calculate their own size, and preload ahead of time. */ @interface ASTableView : UITableView diff --git a/AsyncDisplayKit/ASTableView.mm b/AsyncDisplayKit/ASTableView.mm index b3e423a030..36c990b993 100644 --- a/AsyncDisplayKit/ASTableView.mm +++ b/AsyncDisplayKit/ASTableView.mm @@ -81,7 +81,7 @@ static NSString * const kCellReuseIdentifier = @"_ASTableViewCell"; #pragma mark ASTableView @interface ASTableNode () -- (instancetype)_initWithStyle:(UITableViewStyle)style dataControllerClass:(Class)dataControllerClass; +- (instancetype)_initWithTableView:(ASTableView *)tableView; @end @interface ASTableView () { @@ -110,6 +110,12 @@ static NSString * const kCellReuseIdentifier = @"_ASTableViewCell"; @property (atomic, assign) BOOL asyncDataSourceLocked; @property (nonatomic, retain, readwrite) ASDataController *dataController; +// Used only when ASTableView is created directly rather than through ASTableNode. +// We create a node so that logic related to appearance, memory management, etc can be located there +// for both the node-based and view-based version of the table. +// This also permits sharing logic with ASCollectionNode, as the superclass is not UIKit-controlled. +@property (nonatomic, retain) ASTableNode *strongTableNode; + @end @implementation ASTableView @@ -122,7 +128,7 @@ static NSString * const kCellReuseIdentifier = @"_ASTableViewCell"; #pragma mark - #pragma mark Lifecycle -- (void)configureWithDataControllerClass:(Class)dataControllerClass asyncDataFetching:(BOOL)asyncDataFetching +- (void)configureWithDataControllerClass:(Class)dataControllerClass { _layoutController = [[ASFlowLayoutController alloc] initWithScrollOption:ASFlowLayoutDirectionVertical]; @@ -131,13 +137,13 @@ static NSString * const kCellReuseIdentifier = @"_ASTableViewCell"; _rangeController.dataSource = self; _rangeController.delegate = self; - _dataController = [[dataControllerClass alloc] initWithAsyncDataFetching:asyncDataFetching]; + _dataController = [[dataControllerClass alloc] initWithAsyncDataFetching:NO]; _dataController.dataSource = self; _dataController.delegate = _rangeController; _layoutController.dataSource = _dataController; - _asyncDataFetchingEnabled = asyncDataFetching; + _asyncDataFetchingEnabled = NO; _asyncDataSourceLocked = NO; _leadingScreensForBatching = 1.0; @@ -161,32 +167,32 @@ static NSString * const kCellReuseIdentifier = @"_ASTableViewCell"; - (instancetype)initWithFrame:(CGRect)frame style:(UITableViewStyle)style { - return [self initWithFrame:frame style:style asyncDataFetching:NO]; + return [self _initWithFrame:frame style:style dataControllerClass:nil ownedByNode:NO]; } // FIXME: This method is deprecated and will probably be removed in or shortly after 2.0. - (instancetype)initWithFrame:(CGRect)frame style:(UITableViewStyle)style asyncDataFetching:(BOOL)asyncDataFetchingEnabled { - return [self initWithFrame:frame style:style dataControllerClass:[self.class dataControllerClass] asyncDataFetching:asyncDataFetchingEnabled]; + return [self _initWithFrame:frame style:style dataControllerClass:nil ownedByNode:NO]; } -- (instancetype)initWithFrame:(CGRect)frame style:(UITableViewStyle)style dataControllerClass:(Class)dataControllerClass asyncDataFetching:(BOOL)asyncDataFetchingEnabled +- (instancetype)_initWithFrame:(CGRect)frame style:(UITableViewStyle)style dataControllerClass:(Class)dataControllerClass ownedByNode:(BOOL)ownedByNode { -// ASTableNode *tableNode = [[ASTableNode alloc] _initWithStyle:style dataControllerClass:dataControllerClass]; -// tableNode.frame = frame; -// return tableNode.view; - return [self _initWithFrame:frame style:style dataControllerClass:dataControllerClass]; -} - -- (instancetype)_initWithFrame:(CGRect)frame style:(UITableViewStyle)style dataControllerClass:(Class)dataControllerClass -{ - if (!(self = [super initWithFrame:frame style:style])) + if (!(self = [super initWithFrame:frame style:style])) { return nil; + } if (!dataControllerClass) { dataControllerClass = [self.class dataControllerClass]; } - [self configureWithDataControllerClass:dataControllerClass asyncDataFetching:NO]; + + [self configureWithDataControllerClass:dataControllerClass]; + + if (!ownedByNode) { + // See commentary at the definition of .strongTableNode for why we create an ASTableNode. + ASTableNode *tableNode = [[ASTableNode alloc] _initWithTableView:self]; + self.strongTableNode = tableNode; + } return self; } diff --git a/AsyncDisplayKit/ASTableViewInternal.h b/AsyncDisplayKit/ASTableViewInternal.h index 8d43beafb6..02eb062d46 100644 --- a/AsyncDisplayKit/ASTableViewInternal.h +++ b/AsyncDisplayKit/ASTableViewInternal.h @@ -26,6 +26,6 @@ * * @param asyncDataFetchingEnabled This option is reserved for future use, and currently a no-op. */ -- (instancetype)initWithFrame:(CGRect)frame style:(UITableViewStyle)style dataControllerClass:(Class)dataControllerClass asyncDataFetching:(BOOL)asyncDataFetchingEnabled; +- (instancetype)_initWithFrame:(CGRect)frame style:(UITableViewStyle)style dataControllerClass:(Class)dataControllerClass ownedByNode:(BOOL)ownedByNode; @end diff --git a/AsyncDisplayKit/Layout/ASLayoutOptions.h b/AsyncDisplayKit/Layout/ASLayoutOptions.h index 88036d1b49..7ce0f404e2 100644 --- a/AsyncDisplayKit/Layout/ASLayoutOptions.h +++ b/AsyncDisplayKit/Layout/ASLayoutOptions.h @@ -55,7 +55,7 @@ NS_ASSUME_NONNULL_BEGIN * * @return a new instance of ASLayoutOptions */ -- (instancetype)initWithLayoutable:(nullable id)layoutable NS_DESIGNATED_INITIALIZER; +- (instancetype)initWithLayoutable:(id)layoutable; /** * Copies the values of layoutOptions into self. This is useful when placing a layoutable inside of another. Consider diff --git a/AsyncDisplayKit/Layout/ASLayoutOptions.mm b/AsyncDisplayKit/Layout/ASLayoutOptions.mm index a40eea2bd5..ad503014bf 100644 --- a/AsyncDisplayKit/Layout/ASLayoutOptions.mm +++ b/AsyncDisplayKit/Layout/ASLayoutOptions.mm @@ -56,10 +56,10 @@ static Class gDefaultLayoutOptionsClass = nil; - (instancetype)init { - return [self initWithLayoutable:nil]; + return nil; } -- (instancetype)initWithLayoutable:(id)layoutable; +- (instancetype)initWithLayoutable:(id)layoutable { self = [super init]; if (self) { diff --git a/AsyncDisplayKit/Private/ASDisplayNodeInternal.h b/AsyncDisplayKit/Private/ASDisplayNodeInternal.h index ca5a7ee1ca..1636e5a175 100644 --- a/AsyncDisplayKit/Private/ASDisplayNodeInternal.h +++ b/AsyncDisplayKit/Private/ASDisplayNodeInternal.h @@ -160,6 +160,20 @@ typedef NS_OPTIONS(NSUInteger, ASDisplayNodeMethodOverrides) @property (nonatomic, assign) CGFloat contentsScaleForDisplay; +/** + * // TODO: NOT YET IMPLEMENTED + * + * @abstract Prevents interface state changes from affecting the node, until disabled. + * + * @discussion Useful to avoid flashing after removing a node from the hierarchy and re-adding it. + * Removing a node from the hierarchy will cause it to exit the Display state, clearing its contents. + * For some animations, it's desirable to be able to remove a node without causing it to re-display. + * Once re-enabled, the interface state will be updated to the same value it would have been. + * + * @see ASInterfaceState + */ +@property (nonatomic, assign) BOOL interfaceStateSuspended; + /** * This method has proven helpful in a few rare scenarios, similar to a category extension on UIView, * but it's considered private API for now and its use should not be encouraged. diff --git a/AsyncDisplayKitTests/ASTableViewTests.m b/AsyncDisplayKitTests/ASTableViewTests.m index a382b6be8c..676634e106 100644 --- a/AsyncDisplayKitTests/ASTableViewTests.m +++ b/AsyncDisplayKitTests/ASTableViewTests.m @@ -37,9 +37,9 @@ @implementation ASTestTableView -- (instancetype)initWithFrame:(CGRect)frame style:(UITableViewStyle)style asyncDataFetching:(BOOL)asyncDataFetchingEnabled +- (instancetype)__initWithFrame:(CGRect)frame style:(UITableViewStyle)style { - return [super initWithFrame:frame style:style dataControllerClass:[ASTestDataController class] asyncDataFetching:asyncDataFetchingEnabled]; + return [super _initWithFrame:frame style:style dataControllerClass:[ASTestDataController class] ownedByNode:NO]; } - (ASTestDataController *)testDataController @@ -124,6 +124,7 @@ @end @interface ASTableViewTests : XCTestCase +@property (atomic, retain) ASTableView *testTableView; @end @implementation ASTableViewTests @@ -131,7 +132,7 @@ // TODO: Convert this to ARC. - (void)DISABLED_testTableViewDoesNotRetainItselfAndDelegate { - ASTestTableView *tableView = [[ASTestTableView alloc] initWithFrame:CGRectZero style:UITableViewStylePlain]; + ASTestTableView *tableView = [[ASTestTableView alloc] __initWithFrame:CGRectZero style:UITableViewStylePlain]; __block BOOL tableViewDidDealloc = NO; tableView.willDeallocBlock = ^(ASTableView *v){ @@ -185,9 +186,8 @@ - (void)testReloadData { // Keep the viewport moderately sized so that new cells are loaded on scrolling - ASTableView *tableView = [[ASTableView alloc] initWithFrame:CGRectMake(0, 0, 100, 500) - style:UITableViewStylePlain - asyncDataFetching:YES]; + ASTableView *tableView = [[ASTestTableView alloc] __initWithFrame:CGRectMake(0, 0, 100, 500) + style:UITableViewStylePlain]; ASTableViewFilledDataSource *dataSource = [ASTableViewFilledDataSource new]; @@ -250,9 +250,8 @@ // Any subsequence size change must trigger a relayout. CGSize tableViewFinalSize = CGSizeMake(100, 500); // Width and height are swapped so that a later size change will simulate a rotation - ASTestTableView *tableView = [[ASTestTableView alloc] initWithFrame:CGRectMake(0, 0, tableViewFinalSize.height, tableViewFinalSize.width) - style:UITableViewStylePlain - asyncDataFetching:YES]; + ASTestTableView *tableView = [[ASTestTableView alloc] __initWithFrame:CGRectMake(0, 0, tableViewFinalSize.height, tableViewFinalSize.width) + style:UITableViewStylePlain]; ASTableViewFilledDataSource *dataSource = [ASTableViewFilledDataSource new]; @@ -270,9 +269,8 @@ // Initial width of the table view is 0. The first size change is part of the initial config. // Any subsequence size change after that must trigger a relayout. CGSize tableViewFinalSize = CGSizeMake(100, 500); - ASTestTableView *tableView = [[ASTestTableView alloc] initWithFrame:CGRectZero - style:UITableViewStylePlain - asyncDataFetching:YES]; + ASTestTableView *tableView = [[ASTestTableView alloc] __initWithFrame:CGRectZero + style:UITableViewStylePlain]; ASTableViewFilledDataSource *dataSource = [ASTableViewFilledDataSource new]; tableView.asyncDelegate = dataSource; @@ -292,9 +290,8 @@ - (void)testRelayoutVisibleRowsWhenEditingModeIsChanged { CGSize tableViewSize = CGSizeMake(100, 500); - ASTestTableView *tableView = [[ASTestTableView alloc] initWithFrame:CGRectMake(0, 0, tableViewSize.width, tableViewSize.height) - style:UITableViewStylePlain - asyncDataFetching:YES]; + ASTestTableView *tableView = [[ASTestTableView alloc] __initWithFrame:CGRectMake(0, 0, tableViewSize.width, tableViewSize.height) + style:UITableViewStylePlain]; ASTableViewFilledDataSource *dataSource = [ASTableViewFilledDataSource new]; tableView.asyncDelegate = dataSource; @@ -361,9 +358,8 @@ - (void)DISABLED_testRelayoutRowsAfterEditingModeIsChangedAndTheyBecomeVisible { CGSize tableViewSize = CGSizeMake(100, 500); - ASTestTableView *tableView = [[ASTestTableView alloc] initWithFrame:CGRectMake(0, 0, tableViewSize.width, tableViewSize.height) - style:UITableViewStylePlain - asyncDataFetching:YES]; + ASTestTableView *tableView = [[ASTestTableView alloc] __initWithFrame:CGRectMake(0, 0, tableViewSize.width, tableViewSize.height) + style:UITableViewStylePlain]; ASTableViewFilledDataSource *dataSource = [ASTableViewFilledDataSource new]; tableView.asyncDelegate = dataSource; @@ -398,9 +394,6 @@ style:UITableViewStylePlain asyncDataFetching:YES]; ASTableViewFilledDataSource *dataSource = [ASTableViewFilledDataSource new]; -#if ! __has_feature(objc_arc) -#error This file must be compiled with ARC. Use -fobjc-arc flag (or convert project to ARC). -#endif tableView.asyncDelegate = dataSource; tableView.asyncDataSource = dataSource; @@ -414,6 +407,7 @@ XCTAssertEqual(indexPath.row, reportedIndexPath.row); } } + self.testTableView = nil; }]; }