From 09ade3dd00b37668e61dece7d9918beae42a8629 Mon Sep 17 00:00:00 2001 From: Ryan Nystrom Date: Wed, 4 Feb 2015 11:44:17 -0800 Subject: [PATCH] ASTableView batch API and context object --- AsyncDisplayKit.xcodeproj/project.pbxproj | 10 ++++ AsyncDisplayKit/ASTableView.h | 35 +++++++++++++ AsyncDisplayKit/ASTableView.mm | 55 ++++++++++++++++++++- AsyncDisplayKit/Details/ASBatchContext.h | 52 ++++++++++++++++++++ AsyncDisplayKit/Details/ASBatchContext.m | 60 +++++++++++++++++++++++ examples/Kittens/Sample/ViewController.m | 55 ++++++++++++++++++--- 6 files changed, 258 insertions(+), 9 deletions(-) create mode 100644 AsyncDisplayKit/Details/ASBatchContext.h create mode 100644 AsyncDisplayKit/Details/ASBatchContext.m diff --git a/AsyncDisplayKit.xcodeproj/project.pbxproj b/AsyncDisplayKit.xcodeproj/project.pbxproj index 3571f15925..7937fa669e 100644 --- a/AsyncDisplayKit.xcodeproj/project.pbxproj +++ b/AsyncDisplayKit.xcodeproj/project.pbxproj @@ -138,12 +138,15 @@ 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 */; }; 292C59A21A956527007E5DD6 /* ASRangeHandler.h in Headers */ = {isa = PBXBuildFile; fileRef = 292C599C1A956527007E5DD6 /* ASRangeHandler.h */; settings = {ATTRIBUTES = (Public, ); }; }; 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 */; }; + 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, ); }; }; 464052211A3F83C40061C0BA /* ASDataController.mm in Sources */ = {isa = PBXBuildFile; fileRef = 4640521A1A3F83C40061C0BA /* ASDataController.mm */; }; @@ -290,12 +293,15 @@ 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 = ""; }; 292C599C1A956527007E5DD6 /* ASRangeHandler.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = ASRangeHandler.h; sourceTree = ""; }; 292C599D1A956527007E5DD6 /* ASRangeHandlerRender.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = ASRangeHandlerRender.h; sourceTree = ""; }; 292C599E1A956527007E5DD6 /* ASRangeHandlerRender.mm */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.cpp.objcpp; path = ASRangeHandlerRender.mm; sourceTree = ""; }; + 299DA1A71A828D2900162D41 /* ASBatchContext.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = ASBatchContext.h; sourceTree = ""; }; + 299DA1A81A828D2900162D41 /* ASBatchContext.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = ASBatchContext.m; sourceTree = ""; }; 3C9C128419E616EF00E942A0 /* ASTableViewTests.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = ASTableViewTests.m; sourceTree = ""; }; 464052191A3F83C40061C0BA /* ASDataController.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = ASDataController.h; sourceTree = ""; }; 4640521A1A3F83C40061C0BA /* ASDataController.mm */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.cpp.objcpp; path = ASDataController.mm; sourceTree = ""; }; @@ -465,6 +471,8 @@ 058D09E5195D050800B7D73C /* _ASDisplayView.mm */, 054963471A1EA066000F8E56 /* ASBasicImageDownloader.h */, 054963481A1EA066000F8E56 /* ASBasicImageDownloader.mm */, + 299DA1A71A828D2900162D41 /* ASBatchContext.h */, + 299DA1A81A828D2900162D41 /* ASBatchContext.m */, 464052191A3F83C40061C0BA /* ASDataController.h */, 4640521A1A3F83C40061C0BA /* ASDataController.mm */, 05A6D05819D0EB64002DD95E /* ASDealloc2MainObject.h */, @@ -619,6 +627,7 @@ 292C599F1A956527007E5DD6 /* ASLayoutRangeType.h in Headers */, 464052251A3F83C40061C0BA /* ASMultidimensionalArrayUtils.h in Headers */, 058D0A64195D05DC00B7D73C /* ASTextNodeWordKerner.h in Headers */, + 299DA1A91A828D2900162D41 /* ASBatchContext.h in Headers */, 058D0A65195D05DC00B7D73C /* ASTextNodeWordKerner.m in Headers */, 058D0A66195D05DC00B7D73C /* NSMutableAttributedString+TextKitAdditions.h in Headers */, 058D0A67195D05DC00B7D73C /* NSMutableAttributedString+TextKitAdditions.m in Headers */, @@ -789,6 +798,7 @@ 058D0A18195D050800B7D73C /* _ASDisplayLayer.mm in Sources */, 058D0A2C195D050800B7D73C /* ASSentinel.m in Sources */, 464052211A3F83C40061C0BA /* ASDataController.mm in Sources */, + 299DA1AA1A828D2900162D41 /* ASBatchContext.m in Sources */, 058D0A15195D050800B7D73C /* ASDisplayNodeExtras.mm in Sources */, 058D0A1F195D050800B7D73C /* ASTextNodeTextKitHelpers.mm in Sources */, 055F1A3519ABD3E3004DAFF1 /* ASTableView.mm in Sources */, diff --git a/AsyncDisplayKit/ASTableView.h b/AsyncDisplayKit/ASTableView.h index 9b4118c8c7..cf251518a1 100644 --- a/AsyncDisplayKit/ASTableView.h +++ b/AsyncDisplayKit/ASTableView.h @@ -11,6 +11,7 @@ #import #import #import +#import @class ASCellNode; @@ -62,6 +63,13 @@ */ - (instancetype)initWithFrame:(CGRect)frame style:(UITableViewStyle)style asyncDataFetching:(BOOL)asyncDataFetchingEnabled; +/** + * The number of screens left to scroll before the delegate -tableView:shouldBeginBatchFetchingWithContext: is called. + * + * Defaults to one screenful. + */ +@property (nonatomic, assign) CGFloat leadingScreensForBatching; + /** * Reload everything from scratch, destroying the working range and all cached nodes. * @@ -171,6 +179,33 @@ - (void)tableView:(ASTableView *)tableView willDisplayNodeForRowAtIndexPath:(NSIndexPath *)indexPath; - (void)tableView:(ASTableView *)tableView didEndDisplayingNodeForRowAtIndexPath:(NSIndexPath*)indexPath; +/** + * Tell the tableView if batch fetching should begin. + * + * @param tableView 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 tableView assumes that it should notify its asyncDelegate when batch fetching + * should occur. + */ +- (BOOL)shouldBatchFetchForTableView:(UITableView *)tableView; + +/** + * Receive a message that the tableView 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. + * + * ASTableView currently only supports batch events for tail loads. If you require a head load, consider implementing a + * UIRefreshControl. + */ +- (void)tableView:(UITableView *)tableView beginBatchFetchingWithContext:(ASBatchContext *)context; + @end @interface ASTableView (Deprecated) diff --git a/AsyncDisplayKit/ASTableView.mm b/AsyncDisplayKit/ASTableView.mm index 46f23b8cd8..cbef98027e 100644 --- a/AsyncDisplayKit/ASTableView.mm +++ b/AsyncDisplayKit/ASTableView.mm @@ -38,7 +38,10 @@ static BOOL _isInterceptedSelector(SEL sel) // used for ASRangeController visibility updates sel == @selector(tableView:willDisplayCell:forRowAtIndexPath:) || - sel == @selector(tableView:didEndDisplayingCell:forRowAtIndexPath:) + sel == @selector(tableView:didEndDisplayingCell:forRowAtIndexPath:) || + + // used for batch fetching API + sel == @selector(scrollViewWillEndDragging:withVelocity:targetContentOffset:) ); } @@ -112,6 +115,8 @@ static BOOL _isInterceptedSelector(SEL sel) ASRangeController *_rangeController; BOOL _asyncDataFetchingEnabled; + + ASBatchContext *_batchContext; } @property (atomic, assign) BOOL asyncDataSourceLocked; @@ -150,6 +155,9 @@ static BOOL _isInterceptedSelector(SEL sel) _asyncDataFetchingEnabled = asyncDataFetchingEnabled; _asyncDataSourceLocked = NO; + _leadingScreensForBatching = 1.0; + _batchContext = [[ASBatchContext alloc] init]; + return self; } @@ -370,6 +378,51 @@ 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(shouldBatchFetchForTableView:)]) { + return [self.asyncDelegate shouldBatchFetchForTableView:self]; + } else { + return YES; + } +} + +- (void)handleBatchFetchScrollingToOffset:(CGPoint)targetOffset +{ + // 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:)] || + ![self shouldFetchBatch]) { + return; + } + + CGFloat viewHeight = CGRectGetHeight(self.bounds); + CGFloat triggerDistance = viewHeight * _leadingScreensForBatching; + CGFloat offset = targetOffset.y; + CGFloat contentHeight = self.contentSize.height; + + // Determine if the offset that we are headed to is within the number of screens we have defined + // ASTableView supports tail loading only currently, hence the check against ASScrollDirectionUp + if ([self scrollDirection] == ASScrollDirectionUp && + contentHeight - (viewHeight + offset) <= triggerDistance) { + [_batchContext beginBatchFetching]; + [self.asyncDelegate tableView:self beginBatchFetchingWithContext:_batchContext]; + } +} + + #pragma mark - #pragma mark ASRangeControllerDelegate diff --git a/AsyncDisplayKit/Details/ASBatchContext.h b/AsyncDisplayKit/Details/ASBatchContext.h new file mode 100644 index 0000000000..baceb5074a --- /dev/null +++ b/AsyncDisplayKit/Details/ASBatchContext.h @@ -0,0 +1,52 @@ +/* Copyright (c) 2014-present, Facebook, Inc. + * All rights reserved. + * + * This source code is licensed under the BSD-style license found in the + * LICENSE file in the root directory of this source tree. An additional grant + * of patent rights can be found in the PATENTS file in the same directory. + */ + +/** + * @abstract A context object to notify when batch fetches are finished or cancelled. + */ +@interface ASBatchContext : NSObject + +/** + * Retreive the state of the current batch process. + * + * @returns A boolean reflecting if the owner of the context object is fetching another batch. + */ +- (BOOL)isFetching; + +/** + * Let the context object know that a batch fetch was completed. + * + * @param didComplete A boolean that states whether or not the batch fetch completed. + * + * @discussion Only by passing YES will the owner of the context know to attempt another batch update when necessary. + * For instance, when a table has reached the end of its data, a batch fetch will be attempted unless the context + * object thinks that it is still fetching. + */ +- (void)completeBatchFetching:(BOOL)didComplete; + +- (void)beginBatchFetching; + +/** + * Ask the context object if the batch fetching process was cancelled by the context owner. + * + * @discussion If an error occurs in the context owner, the batch fetching may become out of sync and need to be + * cancelled. For best practices, pass the return value of -batchWasCancelled to -completeBatchFetch:. + * + * @returns A boolean reflecting if the context object owner had to cancel the batch process. + */ +- (BOOL)batchFetchingWasCancelled; + +/** + * Notify the context object that something has interupted the batch fetching process. + * + * @discussion Call this method only when something has corrupted the batch fetching process. Calling this method should + * be left to the owner of the batch process unless there is a specific purpose. + */ +- (void)cancelBatchFetching; + +@end diff --git a/AsyncDisplayKit/Details/ASBatchContext.m b/AsyncDisplayKit/Details/ASBatchContext.m new file mode 100644 index 0000000000..f3cbb0f7b9 --- /dev/null +++ b/AsyncDisplayKit/Details/ASBatchContext.m @@ -0,0 +1,60 @@ +/* Copyright (c) 2014-present, Facebook, Inc. + * All rights reserved. + * + * This source code is licensed under the BSD-style license found in the + * LICENSE file in the root directory of this source tree. An additional grant + * of patent rights can be found in the PATENTS file in the same directory. + */ + +#import "ASBatchContext.h" + +typedef NS_ENUM(NSInteger, ASBatchContextState) { + ASBatchContextStateFetching, + ASBatchContextStateCancelled, + ASBatchContextStateCompleted +}; + +@interface ASBatchContext () +{ + ASBatchContextState _state; +} +@end + +@implementation ASBatchContext + +- (instancetype)init +{ + if (self = [super init]) { + _state = ASBatchContextStateCompleted; + } + return self; +} + +- (BOOL)isFetching +{ + return _state == ASBatchContextStateFetching; +} + +- (BOOL)batchFetchingWasCancelled +{ + return _state == ASBatchContextStateCancelled; +} + +- (void)completeBatchFetching:(BOOL)didComplete +{ + if (didComplete) { + _state = ASBatchContextStateCompleted; + } +} + +- (void)beginBatchFetching +{ + _state = ASBatchContextStateFetching; +} + +- (void)cancelBatchFetching +{ + _state = ASBatchContextStateCancelled; +} + +@end diff --git a/examples/Kittens/Sample/ViewController.m b/examples/Kittens/Sample/ViewController.m index c29978186f..5433b4a01d 100644 --- a/examples/Kittens/Sample/ViewController.m +++ b/examples/Kittens/Sample/ViewController.m @@ -17,8 +17,10 @@ #import "BlurbNode.h" #import "KittenNode.h" -static const NSInteger kLitterSize = 200; +static const NSInteger kLitterSize = 20; +static const NSInteger kLitterBatchSize = 10; +static const NSInteger kMaxLitterSize = 100; @interface ViewController () { @@ -52,17 +54,23 @@ static const NSInteger kLitterSize = 200; _tableView.asyncDelegate = self; // populate our "data source" with some random kittens - NSMutableArray *kittenDataSource = [NSMutableArray arrayWithCapacity:kLitterSize]; + + _kittenDataSource = [self createLitterWithSize:kLitterSize];; + + return self; +} + +- (NSArray *)createLitterWithSize:(NSInteger)litterSize +{ + NSMutableArray *kittens = [NSMutableArray arrayWithCapacity:litterSize]; for (NSInteger i = 0; i < kLitterSize; i++) { u_int32_t deltaX = arc4random_uniform(10) - 5; u_int32_t deltaY = arc4random_uniform(10) - 5; CGSize size = CGSizeMake(350 + 2 * deltaX, 350 + 4 * deltaY); - [kittenDataSource addObject:[NSValue valueWithCGSize:size]]; + [kittens addObject:[NSValue valueWithCGSize:size]]; } - _kittenDataSource = kittenDataSource; - - return self; + return kittens; } - (void)setKittenDataSource:(NSArray *)kittenDataSource { @@ -117,12 +125,43 @@ static const NSInteger kLitterSize = 200; return NO; } -- (void)tableViewLockDataSource:(ASTableView *)tableView { +- (void)tableViewLockDataSource:(ASTableView *)tableView +{ self.dataSourceLocked = YES; } -- (void)tableViewUnlockDataSource:(ASTableView *)tableView { +- (void)tableViewUnlockDataSource:(ASTableView *)tableView +{ self.dataSourceLocked = NO; } +- (BOOL)shouldBatchFetchForTableView:(UITableView *)tableView +{ + return _kittenDataSource.count < kMaxLitterSize; +} + +- (void)tableView:(UITableView *)tableView beginBatchFetchingWithContext:(ASBatchContext *)context +{ + NSLog(@"adding kitties"); + dispatch_async(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0), ^{ + sleep(1); + dispatch_async(dispatch_get_main_queue(), ^{ + NSArray *moarKittens = [self createLitterWithSize:kLitterBatchSize]; + + NSMutableArray *indexPaths = [[NSMutableArray alloc] init]; + NSInteger existingKittens = _kittenDataSource.count; + for (NSInteger i = 0; i < moarKittens.count; i++) { + [indexPaths addObject:[NSIndexPath indexPathForRow:existingKittens + i inSection:0]]; + } + + _kittenDataSource = [_kittenDataSource arrayByAddingObjectsFromArray:moarKittens]; + [tableView insertRowsAtIndexPaths:indexPaths withRowAnimation:UITableViewRowAnimationFade]; + + [context completeBatchFetching:YES]; + + NSLog(@"kittens added"); + }); + }); +} + @end