diff --git a/AsyncDisplayKit/ASCollectionView.m b/AsyncDisplayKit/ASCollectionView.m index 1bd7c5bf7c..4b31d3ce17 100644 --- a/AsyncDisplayKit/ASCollectionView.m +++ b/AsyncDisplayKit/ASCollectionView.m @@ -8,6 +8,327 @@ #import "ASCollectionView.h" -@implementation ASCollectionView +#import "ASAssert.h" +#import "ASRangeController.h" + + +#pragma mark - +#pragma mark Proxying. + +/** + * ASCollectionView intercepts and/or overrides a few of UICollectionView's critical data source and delegate methods. + * + * Any selector included in this function *MUST* be implemented by ASCollectionView. + */ +static BOOL _isInterceptedSelector(SEL sel) +{ + return ( + // handled by ASCollectionView node<->cell machinery + sel == @selector(collectionView:cellForItemAtIndexPath:) || + sel == @selector(collectionView:layout:sizeForItemAtIndexPath:) || + + // handled by ASRangeController + sel == @selector(numberOfSectionsInCollectionView:) || + sel == @selector(collectionView:numberOfItemsInSection:) || + + // used for ASRangeController visibility updates + sel == @selector(collectionView:willDisplayCell:forItemAtIndexPath:) || + sel == @selector(collectionView:didEndDisplayingCell:forItemAtIndexPath:) + ); +} + + +/** + * Stand-in for UICollectionViewDataSource and UICollectionViewDelegate. Any method calls we intercept are routed to ASCollectionView; + * everything else leaves AsyncDisplayKit safely and arrives at the original intended data source and delegate. + */ +@interface _ASCollectionViewProxy : NSProxy +- (instancetype)initWithTarget:(id)target interceptor:(ASCollectionView *)interceptor; +@end + +@implementation _ASCollectionViewProxy { + id __weak _target; + ASCollectionView * __weak _interceptor; +} + +- (instancetype)initWithTarget:(id)target interceptor:(ASCollectionView *)interceptor +{ + // -[NSProxy init] is undefined + if (!self) { + return nil; + } + + ASDisplayNodeAssert(target, @"target must not be nil"); + ASDisplayNodeAssert(interceptor, @"interceptor must not be nil"); + + _target = target; + _interceptor = interceptor; + + return self; +} + +- (BOOL)respondsToSelector:(SEL)aSelector +{ + return (_isInterceptedSelector(aSelector) || [_target respondsToSelector:aSelector]); +} + +- (id)forwardingTargetForSelector:(SEL)aSelector +{ + if (_isInterceptedSelector(aSelector)) { + return _interceptor; + } + + return [_target respondsToSelector:aSelector] ? _target : nil; +} + +@end + + +#pragma mark - +#pragma mark ASCollectionView. + +@interface ASCollectionView () { + _ASCollectionViewProxy *_proxyDataSource; + _ASCollectionViewProxy *_proxyDelegate; + + ASRangeController *_rangeController; +} + +@end + +@implementation ASCollectionView + +#pragma mark - +#pragma mark Lifecycle. + +- (instancetype)initWithFrame:(CGRect)frame collectionViewLayout:(UICollectionViewLayout *)layout +{ + if (!(self = [super initWithFrame:frame collectionViewLayout:layout])) + return nil; + + _rangeController = [[ASRangeController alloc] init]; + _rangeController.delegate = self; + + [self registerClass:[UICollectionViewCell class] forCellWithReuseIdentifier:@"_ASCollectionViewCell"]; + + return self; +} + + +#pragma mark - +#pragma mark Overrides. + +- (void)reloadData +{ + [_rangeController rebuildData]; + [super reloadData]; +} + +- (void)setDataSource:(id)dataSource +{ + ASDisplayNodeAssert(NO, @"ASCollectionView uses asyncDataSource, not UICollectionView's dataSource property."); +} + +- (void)setDelegate:(id)delegate +{ + // Our UIScrollView superclass sets its delegate to nil on dealloc. Only assert if we get a non-nil value here. + ASDisplayNodeAssert(delegate == nil, @"ASCollectionView uses asyncDelegate, not UICollectionView's delegate property."); +} + +- (void)setAsyncDataSource:(id)asyncDataSource +{ + if (_asyncDataSource == asyncDataSource) + return; + + _asyncDataSource = asyncDataSource; + _proxyDataSource = [[_ASCollectionViewProxy alloc] initWithTarget:_asyncDataSource interceptor:self]; + super.dataSource = (id)_proxyDataSource; +} + +- (void)setAsyncDelegate:(id)asyncDelegate +{ + if (_asyncDelegate == asyncDelegate) + return; + + _asyncDelegate = asyncDelegate; + _proxyDelegate = [[_ASCollectionViewProxy alloc] initWithTarget:_asyncDelegate interceptor:self]; + super.delegate = (id)_proxyDelegate; +} + +- (ASRangeTuningParameters)rangeTuningParameters +{ + return _rangeController.tuningParameters; +} + +- (void)setRangeTuningParameters:(ASRangeTuningParameters)tuningParameters +{ + _rangeController.tuningParameters = tuningParameters; +} + +- (void)appendNodesWithIndexPaths:(NSArray *)indexPaths +{ + [_rangeController appendNodesWithIndexPaths:indexPaths]; +} + +#pragma mark Assertions. + +- (void)throwUnimplementedException +{ + [[NSException exceptionWithName:@"UnimplementedException" + reason:@"ASCollectionView's update/editing support is not yet implemented. Please see ASCollectionView.h." + userInfo:nil] raise]; +} + +- (void)insertSections:(NSIndexSet *)sections +{ + [self throwUnimplementedException]; +} + +- (void)deleteSections:(NSIndexSet *)sections +{ + [self throwUnimplementedException]; +} + +- (void)reloadSections:(NSIndexSet *)sections +{ + [self throwUnimplementedException]; +} + +- (void)moveSection:(NSInteger)section toSection:(NSInteger)newSection +{ + [self throwUnimplementedException]; +} + +- (void)insertItemsAtIndexPaths:(NSArray *)indexPaths +{ + [self throwUnimplementedException]; +} + +- (void)deleteItemsAtIndexPaths:(NSArray *)indexPaths +{ + [self throwUnimplementedException]; +} + +- (void)reloadItemsAtIndexPaths:(NSArray *)indexPaths +{ + [self throwUnimplementedException]; +} + +- (void)moveItemAtIndexPath:(NSIndexPath *)indexPath toIndexPath:(NSIndexPath *)newIndexPath +{ + [self throwUnimplementedException]; +} + + +#pragma mark - +#pragma mark Intercepted selectors. + +- (UICollectionViewCell *)collectionView:(UICollectionView *)collectionView cellForItemAtIndexPath:(NSIndexPath *)indexPath +{ + static NSString *reuseIdentifier = @"_ASCollectionViewCell"; + + UICollectionViewCell *cell = [self dequeueReusableCellWithReuseIdentifier:reuseIdentifier forIndexPath:indexPath]; + + [_rangeController configureContentView:cell.contentView forIndexPath:indexPath]; + + return cell; +} + +- (CGSize)collectionView:(UICollectionView *)collectionView layout:(UICollectionViewLayout *)collectionViewLayout sizeForItemAtIndexPath:(NSIndexPath *)indexPath +{ + return [_rangeController calculatedSizeForNodeAtIndexPath:indexPath]; +} + +- (NSInteger)numberOfSectionsInCollectionView:(UICollectionView *)collectionView +{ + return [_rangeController numberOfSizedSections]; +} + +- (NSInteger)collectionView:(UICollectionView *)collectionView numberOfItemsInSection:(NSInteger)section +{ + return [_rangeController numberOfSizedRowsInSection:section]; +} + +- (void)collectionView:(UICollectionView *)collectionView willDisplayCell:(UICollectionViewCell *)cell forItemAtIndexPath:(NSIndexPath *)indexPath +{ + [_rangeController visibleNodeIndexPathsDidChange]; + + if ([_asyncDelegate respondsToSelector:@selector(collectionView:willDisplayNodeForItemAtIndexPath:)]) { + [_asyncDelegate collectionView:self willDisplayNodeForItemAtIndexPath:indexPath]; + } +} + +- (void)collectionView:(UICollectionView *)collectionView didEndDisplayingCell:(UICollectionViewCell *)cell forItemAtIndexPath:(NSIndexPath *)indexPath +{ + [_rangeController visibleNodeIndexPathsDidChange]; + + if ([_asyncDelegate respondsToSelector:@selector(collectionView:didEndDisplayingNodeForItemAtIndexPath:)]) { + [_asyncDelegate collectionView:self didEndDisplayingNodeForItemAtIndexPath:indexPath]; + } +} + + +#pragma mark - +#pragma mark ASRangeControllerDelegate. + +- (NSArray *)rangeControllerVisibleNodeIndexPaths:(ASRangeController *)rangeController +{ + ASDisplayNodeAssertMainThread(); + return [self indexPathsForVisibleItems]; +} + +- (CGSize)rangeControllerViewportSize:(ASRangeController *)rangeController +{ + ASDisplayNodeAssertMainThread(); + return self.bounds.size; +} + +- (NSInteger)rangeControllerSections:(ASRangeController *)rangeController +{ + ASDisplayNodeAssertMainThread(); + if ([_asyncDataSource respondsToSelector:@selector(numberOfSectionsInCollectionView:)]) { + return [_asyncDataSource numberOfSectionsInCollectionView:self]; + } else { + return 1; + } +} + +- (NSInteger)rangeController:(ASRangeController *)rangeController rowsInSection:(NSInteger)section +{ + ASDisplayNodeAssertMainThread(); + return [_asyncDataSource collectionView:self numberOfItemsInSection:section]; +} + +- (ASCellNode *)rangeController:(ASRangeController *)rangeController nodeForIndexPath:(NSIndexPath *)indexPath +{ + ASDisplayNodeAssertNotMainThread(); + return [_asyncDataSource collectionView:self nodeForItemAtIndexPath:indexPath]; +} + +- (CGSize)rangeController:(ASRangeController *)rangeController constrainedSizeForNodeAtIndexPath:(NSIndexPath *)indexPath +{ + ASDisplayNodeAssertNotMainThread(); + //FIXME: Assuming vertical scroll + return CGSizeMake(self.bounds.size.width, FLT_MAX); +} + +- (void)rangeController:(ASRangeController *)rangeController didSizeNodesWithIndexPaths:(NSArray *)indexPaths +{ + ASDisplayNodeAssertMainThread(); + [UIView performWithoutAnimation:^{ + [self performBatchUpdates:^{ + // -insertItemsAtIndexPaths: is insufficient; UICollectionView also needs to be notified of section changes + NSInteger sectionCount = [super numberOfSections]; + NSInteger newSectionCount = [_rangeController numberOfSizedSections]; + if (newSectionCount > sectionCount) { + NSRange range = NSMakeRange(sectionCount, newSectionCount - sectionCount); + NSIndexSet *sections = [NSIndexSet indexSetWithIndexesInRange:range]; + [super insertSections:sections]; + } + + [super insertItemsAtIndexPaths:indexPaths]; + } completion:nil]; + }]; +} @end diff --git a/AsyncDisplayKit/AsyncDisplayKit.h b/AsyncDisplayKit/AsyncDisplayKit.h index fbc44409fa..bbecc933ce 100644 --- a/AsyncDisplayKit/AsyncDisplayKit.h +++ b/AsyncDisplayKit/AsyncDisplayKit.h @@ -14,4 +14,5 @@ #import #import +#import #import