From 1f8001a10418e972396f3ba10502ca534130d073 Mon Sep 17 00:00:00 2001 From: Ryan Nystrom Date: Sat, 7 Feb 2015 16:10:20 -0500 Subject: [PATCH] ASCollectionView batch API --- AsyncDisplayKit.xcodeproj/project.pbxproj | 3 +- AsyncDisplayKit/ASCollectionView.h | 36 ++++++++++ AsyncDisplayKit/ASCollectionView.mm | 66 ++++++++++++++++++- AsyncDisplayKit/ASTableView.h | 2 +- AsyncDisplayKit/ASTableView.mm | 2 + .../ASCollectionView/Sample/ViewController.m | 14 +++- 6 files changed, 116 insertions(+), 7 deletions(-) diff --git a/AsyncDisplayKit.xcodeproj/project.pbxproj b/AsyncDisplayKit.xcodeproj/project.pbxproj index 7937fa669e..9f0afe7a2e 100644 --- a/AsyncDisplayKit.xcodeproj/project.pbxproj +++ b/AsyncDisplayKit.xcodeproj/project.pbxproj @@ -138,7 +138,6 @@ 05F20AA41A15733C00DCA68A /* ASImageProtocols.h in Headers */ = {isa = PBXBuildFile; fileRef = 05F20AA31A15733C00DCA68A /* ASImageProtocols.h */; settings = {ATTRIBUTES = (Public, ); }; }; 1950C4491A3BB5C1005C8279 /* ASEqualityHelpers.h in Headers */ = {isa = PBXBuildFile; fileRef = 1950C4481A3BB5C1005C8279 /* ASEqualityHelpers.h */; }; 2911485C1A77147A005D0878 /* ASControlNodeTests.m in Sources */ = {isa = PBXBuildFile; fileRef = 2911485B1A77147A005D0878 /* ASControlNodeTests.m */; }; -<<<<<<< HEAD 292C599F1A956527007E5DD6 /* ASLayoutRangeType.h in Headers */ = {isa = PBXBuildFile; fileRef = 292C59991A956527007E5DD6 /* ASLayoutRangeType.h */; settings = {ATTRIBUTES = (Public, ); }; }; 292C59A01A956527007E5DD6 /* ASRangeHandlerPreload.h in Headers */ = {isa = PBXBuildFile; fileRef = 292C599A1A956527007E5DD6 /* ASRangeHandlerPreload.h */; settings = {ATTRIBUTES = (Public, ); }; }; 292C59A11A956527007E5DD6 /* ASRangeHandlerPreload.mm in Sources */ = {isa = PBXBuildFile; fileRef = 292C599B1A956527007E5DD6 /* ASRangeHandlerPreload.mm */; }; @@ -146,6 +145,7 @@ 292C59A31A956527007E5DD6 /* ASRangeHandlerRender.h in Headers */ = {isa = PBXBuildFile; fileRef = 292C599D1A956527007E5DD6 /* ASRangeHandlerRender.h */; settings = {ATTRIBUTES = (Public, ); }; }; 292C59A41A956527007E5DD6 /* ASRangeHandlerRender.mm in Sources */ = {isa = PBXBuildFile; fileRef = 292C599E1A956527007E5DD6 /* ASRangeHandlerRender.mm */; }; 299DA1A91A828D2900162D41 /* ASBatchContext.h in Headers */ = {isa = PBXBuildFile; fileRef = 299DA1A71A828D2900162D41 /* ASBatchContext.h */; }; + 299DA1A91A828D2900162D41 /* ASBatchContext.h in Headers */ = {isa = PBXBuildFile; fileRef = 299DA1A71A828D2900162D41 /* ASBatchContext.h */; settings = {ATTRIBUTES = (Public, ); }; }; 299DA1AA1A828D2900162D41 /* ASBatchContext.m in Sources */ = {isa = PBXBuildFile; fileRef = 299DA1A81A828D2900162D41 /* ASBatchContext.m */; }; 3C9C128519E616EF00E942A0 /* ASTableViewTests.m in Sources */ = {isa = PBXBuildFile; fileRef = 3C9C128419E616EF00E942A0 /* ASTableViewTests.m */; settings = {COMPILER_FLAGS = "-fno-objc-arc"; }; }; 464052201A3F83C40061C0BA /* ASDataController.h in Headers */ = {isa = PBXBuildFile; fileRef = 464052191A3F83C40061C0BA /* ASDataController.h */; settings = {ATTRIBUTES = (Public, ); }; }; @@ -293,7 +293,6 @@ 05F20AA31A15733C00DCA68A /* ASImageProtocols.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = ASImageProtocols.h; sourceTree = ""; }; 1950C4481A3BB5C1005C8279 /* ASEqualityHelpers.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = ASEqualityHelpers.h; sourceTree = ""; }; 2911485B1A77147A005D0878 /* ASControlNodeTests.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = ASControlNodeTests.m; sourceTree = ""; }; -<<<<<<< HEAD 292C59991A956527007E5DD6 /* ASLayoutRangeType.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = ASLayoutRangeType.h; sourceTree = ""; }; 292C599A1A956527007E5DD6 /* ASRangeHandlerPreload.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = ASRangeHandlerPreload.h; sourceTree = ""; }; 292C599B1A956527007E5DD6 /* ASRangeHandlerPreload.mm */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.cpp.objcpp; path = ASRangeHandlerPreload.mm; sourceTree = ""; }; diff --git a/AsyncDisplayKit/ASCollectionView.h b/AsyncDisplayKit/ASCollectionView.h index 34e5f10039..8ef63cfbc7 100644 --- a/AsyncDisplayKit/ASCollectionView.h +++ b/AsyncDisplayKit/ASCollectionView.h @@ -11,6 +11,7 @@ #import #import #import +#import @class ASCellNode; @@ -62,6 +63,13 @@ */ - (instancetype)initWithFrame:(CGRect)frame collectionViewLayout:(UICollectionViewLayout *)layout asyncDataFetching:(BOOL)asyncDataFetchingEnabled; +/** + * The number of screens left to scroll before the delegate -collectionView:beginBatchFetchingWithContext: is called. + * + * Defaults to one screenful. + */ +@property (nonatomic, assign) CGFloat leadingScreensForBatching; + /** * Reload everything from scratch, destroying the working range and all cached nodes. * @@ -166,6 +174,34 @@ - (void)collectionView:(ASCollectionView *)collectionView willDisplayNodeForItemAtIndexPath:(NSIndexPath *)indexPath; - (void)collectionView:(ASCollectionView *)collectionView didEndDisplayingNodeForItemAtIndexPath:(NSIndexPath*)indexPath; +/** + * Tell the collectionView if batch fetching should begin. + * + * @param collectionView The sender. + * + * @discussion Use this method to conditionally fetch batches. Example use cases are: limiting the total number of + * objects that can be fetched or no network connection. + * + * If not implemented, the collectionView assumes that it should notify its asyncDelegate when batch fetching + * should occur. + */ +- (BOOL)shouldBatchFetchForCollectionView:(UICollectionView *)collectionView; + +/** + * Receive a message that the collectionView is near the end of its data set and more data should be fetched if + * necessary. + * + * @param tableView The sender. + * @param context A context object that must be notified when the batch fetch is completed. + * + * @discussion You must eventually call -completeBatchFetching: with an argument of YES in order to receive future + * notifications to do batch fetches. + * + * UICollectionView currently only supports batch events for tail loads. If you require a head load, consider + * implementing a UIRefreshControl. + */ +- (void)collectionView:(UICollectionView *)collectionView beginBatchFetchingWithContext:(ASBatchContext *)context; + @end @interface ASCollectionView (Deprecated) diff --git a/AsyncDisplayKit/ASCollectionView.mm b/AsyncDisplayKit/ASCollectionView.mm index 6d5bf11e87..f5b673c989 100644 --- a/AsyncDisplayKit/ASCollectionView.mm +++ b/AsyncDisplayKit/ASCollectionView.mm @@ -38,7 +38,10 @@ static BOOL _isInterceptedSelector(SEL sel) // used for ASRangeController visibility updates sel == @selector(collectionView:willDisplayCell:forItemAtIndexPath:) || - sel == @selector(collectionView:didEndDisplayingCell:forItemAtIndexPath:) + sel == @selector(collectionView:didEndDisplayingCell:forItemAtIndexPath:) || + + // used for batch fetching API + sel == @selector(scrollViewWillEndDragging:withVelocity:targetContentOffset:) ); } @@ -103,6 +106,8 @@ static BOOL _isInterceptedSelector(SEL sel) NSMutableArray *_batchUpdateBlocks; BOOL _asyncDataFetchingEnabled; + + ASBatchContext *_batchContext; } @property (atomic, assign) BOOL asyncDataSourceLocked; @@ -137,6 +142,8 @@ static BOOL _isInterceptedSelector(SEL sel) _dataController.delegate = _rangeController; _dataController.dataSource = self; + _batchContext = [[ASBatchContext alloc] init]; + _proxyDelegate = [[_ASCollectionViewProxy alloc] initWithTarget:nil interceptor:self]; super.delegate = (id)_proxyDelegate; @@ -362,6 +369,63 @@ static BOOL _isInterceptedSelector(SEL sel) } +#pragma mark - +#pragma mark Batch Fetching + +- (void)scrollViewWillEndDragging:(UIScrollView *)scrollView withVelocity:(CGPoint)velocity targetContentOffset:(inout CGPoint *)targetContentOffset +{ + [self handleBatchFetchScrollingToOffset:*targetContentOffset]; + + if ([_asyncDelegate respondsToSelector:@selector(scrollViewWillEndDragging:withVelocity:targetContentOffset:)]) { + [_asyncDelegate scrollViewWillEndDragging:scrollView withVelocity:velocity targetContentOffset:targetContentOffset]; + } +} + +- (BOOL)shouldFetchBatch +{ + if ([self.asyncDelegate respondsToSelector:@selector(shouldBatchFetchForCollectionView:)]) { + return [self.asyncDelegate shouldBatchFetchForCollectionView:self]; + } else { + return YES; + } +} + +- (void)handleBatchFetchScrollingToOffset:(CGPoint)targetOffset +{ + ASDisplayNodeAssert(_batchContext != nil, @"Batch context should exist"); + + // Bail if we are already fetching, the delegate doesn't care, or we're told not to fetch + if ([_batchContext isFetching] || + ![self.asyncDelegate respondsToSelector:@selector(collectionView:beginBatchFetchingWithContext:)] || + ![self shouldFetchBatch]) { + return; + } + + ASScrollDirection scrollDirection = [self scrollDirection]; + CGFloat viewSize, offset, contentSize; + + if (scrollDirection == ASScrollDirectionUp) { + viewSize = CGRectGetHeight(self.bounds); + offset = targetOffset.y; + contentSize = self.contentSize.height; + } else { // horizontal + viewSize = CGRectGetWidth(self.bounds); + offset = targetOffset.x; + contentSize = self.contentSize.width; + } + + CGFloat triggerDistance = viewSize * _leadingScreensForBatching; + + // Determine if the offset that we are headed to is within the number of screens we have defined + // ASCollectionView supports tail loading only currently, hence the check against Up and Left + BOOL supportedBatchScrollDirection = scrollDirection == ASScrollDirectionUp || ASScrollDirectionLeft; + if (supportedBatchScrollDirection && contentSize - (viewSize + offset) <= triggerDistance) { + [_batchContext beginBatchFetching]; + [self.asyncDelegate collectionView:self beginBatchFetchingWithContext:_batchContext]; + } +} + + #pragma mark - ASDataControllerSource - (ASCellNode *)dataController:(ASDataController *)dataController nodeAtIndexPath:(NSIndexPath *)indexPath diff --git a/AsyncDisplayKit/ASTableView.h b/AsyncDisplayKit/ASTableView.h index cf251518a1..eb4b7968e2 100644 --- a/AsyncDisplayKit/ASTableView.h +++ b/AsyncDisplayKit/ASTableView.h @@ -64,7 +64,7 @@ - (instancetype)initWithFrame:(CGRect)frame style:(UITableViewStyle)style asyncDataFetching:(BOOL)asyncDataFetchingEnabled; /** - * The number of screens left to scroll before the delegate -tableView:shouldBeginBatchFetchingWithContext: is called. + * The number of screens left to scroll before the delegate -tableView:beginBatchFetchingWithContext: is called. * * Defaults to one screenful. */ diff --git a/AsyncDisplayKit/ASTableView.mm b/AsyncDisplayKit/ASTableView.mm index cbef98027e..81ab448535 100644 --- a/AsyncDisplayKit/ASTableView.mm +++ b/AsyncDisplayKit/ASTableView.mm @@ -401,6 +401,8 @@ static BOOL _isInterceptedSelector(SEL sel) - (void)handleBatchFetchScrollingToOffset:(CGPoint)targetOffset { + ASDisplayNodeAssert(_batchContext != nil, @"Batch context should exist"); + // Bail if we are already fetching, the delegate doesn't care, or we're told not to fetch if ([_batchContext isFetching] || ![self.asyncDelegate respondsToSelector:@selector(tableView:beginBatchFetchingWithContext:)] || diff --git a/examples/ASCollectionView/Sample/ViewController.m b/examples/ASCollectionView/Sample/ViewController.m index 895e817021..7b8bfcdaf9 100644 --- a/examples/ASCollectionView/Sample/ViewController.m +++ b/examples/ASCollectionView/Sample/ViewController.m @@ -65,7 +65,7 @@ - (ASCellNode *)collectionView:(ASCollectionView *)collectionView nodeForItemAtIndexPath:(NSIndexPath *)indexPath { - NSString *text = [NSString stringWithFormat:@"[%ld.%ld] says hi", indexPath.section, indexPath.item]; + NSString *text = [NSString stringWithFormat:@"[%zd.%zd] says hi", indexPath.section, indexPath.item]; ASTextCellNode *node = [[ASTextCellNode alloc] init]; node.text = text; node.backgroundColor = [UIColor lightGrayColor]; @@ -78,13 +78,21 @@ return 300; } -- (void)collectionViewLockDataSource:(ASCollectionView *)collectionView { +- (void)collectionViewLockDataSource:(ASCollectionView *)collectionView +{ // lock the data source // The data source should not be change until it is unlocked. } -- (void)collectionViewUnlockDataSource:(ASCollectionView *)collectionView { +- (void)collectionViewUnlockDataSource:(ASCollectionView *)collectionView +{ // unlock the data source to enable data source updating. } +- (void)collectionView:(UICollectionView *)collectionView beginBatchFetchingWithContext:(ASBatchContext *)context +{ + NSLog(@"fetch additional content"); + [context completeBatchFetching:YES]; +} + @end