Implement node-backing for ASTableView and ASCollectionView, with a strong back-pointer in these cases.

This commit is contained in:
Scott Goodson 2015-12-26 23:04:16 -08:00
parent 02ab9e230f
commit 44feece701
12 changed files with 124 additions and 62 deletions

View File

@ -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]) {

View File

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

View File

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

View File

@ -205,6 +205,7 @@ NS_ASSUME_NONNULL_BEGIN
*/
@property (nonatomic, readonly) ASInterfaceState interfaceState;
/** @name Managing dimensions */
/**

View File

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

View File

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

View File

@ -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 () <ASRangeControllerDataSource, ASRangeControllerDelegate, ASDataControllerSource, _ASTableViewCellDelegate, ASCellNodeLayoutDelegate, ASDelegateProxyInterceptor> {
@ -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;
}

View File

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

View File

@ -55,7 +55,7 @@ NS_ASSUME_NONNULL_BEGIN
*
* @return a new instance of ASLayoutOptions
*/
- (instancetype)initWithLayoutable:(nullable id<ASLayoutable>)layoutable NS_DESIGNATED_INITIALIZER;
- (instancetype)initWithLayoutable:(id<ASLayoutable>)layoutable;
/**
* Copies the values of layoutOptions into self. This is useful when placing a layoutable inside of another. Consider

View File

@ -56,10 +56,10 @@ static Class gDefaultLayoutOptionsClass = nil;
- (instancetype)init
{
return [self initWithLayoutable:nil];
return nil;
}
- (instancetype)initWithLayoutable:(id<ASLayoutable>)layoutable;
- (instancetype)initWithLayoutable:(id<ASLayoutable>)layoutable
{
self = [super init];
if (self) {

View File

@ -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.

View File

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