From 09ade3dd00b37668e61dece7d9918beae42a8629 Mon Sep 17 00:00:00 2001 From: Ryan Nystrom Date: Wed, 4 Feb 2015 11:44:17 -0800 Subject: [PATCH 1/5] 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 3571f159..7937fa66 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 9b4118c8..cf251518 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 46f23b8c..cbef9802 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 00000000..baceb507 --- /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 00000000..f3cbb0f7 --- /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 c2997818..5433b4a0 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 From 1f8001a10418e972396f3ba10502ca534130d073 Mon Sep 17 00:00:00 2001 From: Ryan Nystrom Date: Sat, 7 Feb 2015 16:10:20 -0500 Subject: [PATCH 2/5] 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 7937fa66..9f0afe7a 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 34e5f100..8ef63cfb 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 6d5bf11e..f5b673c9 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 cf251518..eb4b7968 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 cbef9802..81ab4485 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 895e8170..7b8bfcda 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 From b3ae15858a9d263379ebfd2fd6a496c9066ec091 Mon Sep 17 00:00:00 2001 From: Ryan Nystrom Date: Thu, 12 Feb 2015 16:09:50 -0800 Subject: [PATCH 3/5] Unit tests for batch fetching --- AsyncDisplayKit.xcodeproj/project.pbxproj | 14 +++ AsyncDisplayKit/ASCollectionView.h | 4 +- AsyncDisplayKit/ASCollectionView.mm | 31 ++---- AsyncDisplayKit/ASTableView.h | 4 +- AsyncDisplayKit/ASTableView.mm | 20 +--- AsyncDisplayKit/Details/ASBatchContext.h | 12 ++- AsyncDisplayKit/Details/ASBatchFetching.h | 36 +++++++ AsyncDisplayKit/Details/ASBatchFetching.m | 51 ++++++++++ AsyncDisplayKit/Details/ASLayoutController.h | 10 +- AsyncDisplayKit/Details/ASScrollDirection.h | 17 ++++ AsyncDisplayKitTests/ASBatchFetchingTests.m | 102 +++++++++++++++++++ 11 files changed, 248 insertions(+), 53 deletions(-) create mode 100644 AsyncDisplayKit/Details/ASBatchFetching.h create mode 100644 AsyncDisplayKit/Details/ASBatchFetching.m create mode 100644 AsyncDisplayKit/Details/ASScrollDirection.h create mode 100644 AsyncDisplayKitTests/ASBatchFetchingTests.m diff --git a/AsyncDisplayKit.xcodeproj/project.pbxproj b/AsyncDisplayKit.xcodeproj/project.pbxproj index 9f0afe7a..0f18515d 100644 --- a/AsyncDisplayKit.xcodeproj/project.pbxproj +++ b/AsyncDisplayKit.xcodeproj/project.pbxproj @@ -145,6 +145,9 @@ 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 */; }; + 296A0A2E1A9516B2005ACEAA /* ASBatchFetching.h in Headers */ = {isa = PBXBuildFile; fileRef = 296A0A2C1A9516B2005ACEAA /* ASBatchFetching.h */; settings = {ATTRIBUTES = (Public, ); }; }; + 296A0A2F1A9516B2005ACEAA /* ASBatchFetching.m in Sources */ = {isa = PBXBuildFile; fileRef = 296A0A2D1A9516B2005ACEAA /* ASBatchFetching.m */; }; + 296A0A351A951ABF005ACEAA /* ASBatchFetchingTests.m in Sources */ = {isa = PBXBuildFile; fileRef = 296A0A341A951ABF005ACEAA /* ASBatchFetchingTests.m */; }; 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"; }; }; @@ -299,6 +302,10 @@ 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 = ""; }; + 296A0A2C1A9516B2005ACEAA /* ASBatchFetching.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = ASBatchFetching.h; sourceTree = ""; }; + 296A0A2D1A9516B2005ACEAA /* ASBatchFetching.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = ASBatchFetching.m; sourceTree = ""; }; + 296A0A311A951715005ACEAA /* ASScrollDirection.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; name = ASScrollDirection.h; path = AsyncDisplayKit/Details/ASScrollDirection.h; sourceTree = SOURCE_ROOT; }; + 296A0A341A951ABF005ACEAA /* ASBatchFetchingTests.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = ASBatchFetchingTests.m; 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 = ""; }; @@ -432,6 +439,7 @@ 058D09C5195D04C000B7D73C /* AsyncDisplayKitTests */ = { isa = PBXGroup; children = ( + 296A0A341A951ABF005ACEAA /* ASBatchFetchingTests.m */, 2911485B1A77147A005D0878 /* ASControlNodeTests.m */, 058D0A2D195D057000B7D73C /* ASDisplayLayerTests.m */, 058D0A2E195D057000B7D73C /* ASDisplayNodeAppearanceTests.m */, @@ -472,6 +480,8 @@ 054963481A1EA066000F8E56 /* ASBasicImageDownloader.mm */, 299DA1A71A828D2900162D41 /* ASBatchContext.h */, 299DA1A81A828D2900162D41 /* ASBatchContext.m */, + 296A0A2C1A9516B2005ACEAA /* ASBatchFetching.h */, + 296A0A2D1A9516B2005ACEAA /* ASBatchFetching.m */, 464052191A3F83C40061C0BA /* ASDataController.h */, 4640521A1A3F83C40061C0BA /* ASDataController.mm */, 05A6D05819D0EB64002DD95E /* ASDealloc2MainObject.h */, @@ -497,6 +507,7 @@ 292C599C1A956527007E5DD6 /* ASRangeHandler.h */, 292C599D1A956527007E5DD6 /* ASRangeHandlerRender.h */, 292C599E1A956527007E5DD6 /* ASRangeHandlerRender.mm */, + 296A0A311A951715005ACEAA /* ASScrollDirection.h */, 058D09EA195D050800B7D73C /* ASTextNodeCoreTextAdditions.h */, 058D09EB195D050800B7D73C /* ASTextNodeCoreTextAdditions.m */, 058D09EC195D050800B7D73C /* ASTextNodeRenderer.h */, @@ -600,6 +611,7 @@ 058D0A4F195D05CB00B7D73C /* ASImageNode.h in Headers */, 058D0A50195D05CB00B7D73C /* ASImageNode.mm in Headers */, 058D0A51195D05CB00B7D73C /* ASTextNode.h in Headers */, + 296A0A2E1A9516B2005ACEAA /* ASBatchFetching.h in Headers */, 058D0A52195D05CB00B7D73C /* ASTextNode.mm in Headers */, 055F1A3819ABD413004DAFF1 /* ASRangeController.h in Headers */, 292C59A31A956527007E5DD6 /* ASRangeHandlerRender.h in Headers */, @@ -832,6 +844,7 @@ 058D0A29195D050800B7D73C /* ASDisplayNode+DebugTiming.mm in Sources */, 058D0A22195D050800B7D73C /* _ASAsyncTransaction.m in Sources */, 055F1A3919ABD413004DAFF1 /* ASRangeController.mm in Sources */, + 296A0A2F1A9516B2005ACEAA /* ASBatchFetching.m in Sources */, D785F6631A74327E00291744 /* ASScrollNode.m in Sources */, ); runOnlyForDeploymentPostprocessing = 0; @@ -841,6 +854,7 @@ buildActionMask = 2147483647; files = ( 2911485C1A77147A005D0878 /* ASControlNodeTests.m in Sources */, + 296A0A351A951ABF005ACEAA /* ASBatchFetchingTests.m in Sources */, 058D0A3E195D057000B7D73C /* ASTextNodeRendererTests.m in Sources */, 058D0A3D195D057000B7D73C /* ASTextNodeCoreTextAdditionsTests.m in Sources */, 058D0A3C195D057000B7D73C /* ASMutableAttributedStringBuilderTests.m in Sources */, diff --git a/AsyncDisplayKit/ASCollectionView.h b/AsyncDisplayKit/ASCollectionView.h index 8ef63cfb..da640c61 100644 --- a/AsyncDisplayKit/ASCollectionView.h +++ b/AsyncDisplayKit/ASCollectionView.h @@ -185,7 +185,7 @@ * If not implemented, the collectionView assumes that it should notify its asyncDelegate when batch fetching * should occur. */ -- (BOOL)shouldBatchFetchForCollectionView:(UICollectionView *)collectionView; +- (BOOL)shouldBatchFetchForCollectionView:(ASCollectionView *)collectionView; /** * Receive a message that the collectionView is near the end of its data set and more data should be fetched if @@ -200,7 +200,7 @@ * 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; +- (void)collectionView:(ASCollectionView *)collectionView beginBatchFetchingWithContext:(ASBatchContext *)context; @end diff --git a/AsyncDisplayKit/ASCollectionView.mm b/AsyncDisplayKit/ASCollectionView.mm index f5b673c9..60ac8b1a 100644 --- a/AsyncDisplayKit/ASCollectionView.mm +++ b/AsyncDisplayKit/ASCollectionView.mm @@ -13,6 +13,7 @@ #import "ASRangeController.h" #import "ASDataController.h" #import "ASDisplayNodeInternal.h" +#import "ASBatchFetching.h" const static NSUInteger kASCollectionViewAnimationNone = 0; @@ -144,6 +145,8 @@ static BOOL _isInterceptedSelector(SEL sel) _batchContext = [[ASBatchContext alloc] init]; + _leadingScreensForBatching = 1.0; + _proxyDelegate = [[_ASCollectionViewProxy alloc] initWithTarget:nil interceptor:self]; super.delegate = (id)_proxyDelegate; @@ -386,7 +389,8 @@ static BOOL _isInterceptedSelector(SEL sel) if ([self.asyncDelegate respondsToSelector:@selector(shouldBatchFetchForCollectionView:)]) { return [self.asyncDelegate shouldBatchFetchForCollectionView:self]; } else { - return YES; + // if the delegate does not respond to this method, there is no point in starting to fetch + return [self.asyncDelegate respondsToSelector:@selector(collectionView:beginBatchFetchingWithContext:)]; } } @@ -394,32 +398,11 @@ static BOOL _isInterceptedSelector(SEL sel) { 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]) { + if (![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) { + if (ASDisplayShouldFetchBatchForContext(_batchContext, [self scrollDirection], self.bounds, self.contentSize, targetOffset, _leadingScreensForBatching)) { [_batchContext beginBatchFetching]; [self.asyncDelegate collectionView:self beginBatchFetchingWithContext:_batchContext]; } diff --git a/AsyncDisplayKit/ASTableView.h b/AsyncDisplayKit/ASTableView.h index eb4b7968..08ac6be7 100644 --- a/AsyncDisplayKit/ASTableView.h +++ b/AsyncDisplayKit/ASTableView.h @@ -190,7 +190,7 @@ * If not implemented, the tableView assumes that it should notify its asyncDelegate when batch fetching * should occur. */ -- (BOOL)shouldBatchFetchForTableView:(UITableView *)tableView; +- (BOOL)shouldBatchFetchForTableView:(ASTableView *)tableView; /** * Receive a message that the tableView is near the end of its data set and more data should be fetched if necessary. @@ -204,7 +204,7 @@ * 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; +- (void)tableView:(ASTableView *)tableView beginBatchFetchingWithContext:(ASBatchContext *)context; @end diff --git a/AsyncDisplayKit/ASTableView.mm b/AsyncDisplayKit/ASTableView.mm index 81ab4485..a1fbfa75 100644 --- a/AsyncDisplayKit/ASTableView.mm +++ b/AsyncDisplayKit/ASTableView.mm @@ -14,7 +14,7 @@ #import "ASLayoutController.h" #import "ASRangeController.h" #import "ASDisplayNodeInternal.h" - +#import "ASBatchFetching.h" #pragma mark - @@ -395,7 +395,8 @@ static BOOL _isInterceptedSelector(SEL sel) if ([self.asyncDelegate respondsToSelector:@selector(shouldBatchFetchForTableView:)]) { return [self.asyncDelegate shouldBatchFetchForTableView:self]; } else { - return YES; + // if the delegate does not respond to this method, there is no point in starting to fetch + return [self.asyncDelegate respondsToSelector:@selector(tableView:beginBatchFetchingWithContext:)]; } } @@ -403,22 +404,11 @@ static BOOL _isInterceptedSelector(SEL sel) { 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:)] || - ![self shouldFetchBatch]) { + if (![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) { + if (ASDisplayShouldFetchBatchForContext(_batchContext, [self scrollDirection], self.bounds, self.contentSize, targetOffset, _leadingScreensForBatching)) { [_batchContext beginBatchFetching]; [self.asyncDelegate tableView:self beginBatchFetchingWithContext:_batchContext]; } diff --git a/AsyncDisplayKit/Details/ASBatchContext.h b/AsyncDisplayKit/Details/ASBatchContext.h index baceb507..2d8f2bb2 100644 --- a/AsyncDisplayKit/Details/ASBatchContext.h +++ b/AsyncDisplayKit/Details/ASBatchContext.h @@ -6,6 +6,8 @@ * of patent rights can be found in the PATENTS file in the same directory. */ +#import + /** * @abstract A context object to notify when batch fetches are finished or cancelled. */ @@ -29,8 +31,6 @@ */ - (void)completeBatchFetching:(BOOL)didComplete; -- (void)beginBatchFetching; - /** * Ask the context object if the batch fetching process was cancelled by the context owner. * @@ -49,4 +49,12 @@ */ - (void)cancelBatchFetching; +/** + * Notify the context object that fetching has started. + * + * @discussion Call this method only when you are beginning a fetch process. This should really only be called by the + * context object's owner. Calling this method should be complimented with -completeBatchFetching:. + */ +- (void)beginBatchFetching; + @end diff --git a/AsyncDisplayKit/Details/ASBatchFetching.h b/AsyncDisplayKit/Details/ASBatchFetching.h new file mode 100644 index 00000000..9aeea5ad --- /dev/null +++ b/AsyncDisplayKit/Details/ASBatchFetching.h @@ -0,0 +1,36 @@ +/* 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 + +#import "ASBatchContext.h" +#import "ASScrollDirection.h" +#import "ASBaseDefines.h" + +ASDISPLAYNODE_EXTERN_C_BEGIN + +/** + @abstract Determine if batch fetching should begin based on the state of the parameters. + @param context The batch fetching context that contains knowledge about in-flight fetches. + @param scrollDirection The current scrolling direction of the scroll view. + @param bounds The bounds of the scrollview. + @param contentSize The content size of the scrollview. + @param targetOffset The offset that the scrollview will scroll to. + @param leadingScreens How many screens in the remaining distance will trigger batch fetching. + @return Whether or not the current state should proceed with batch fetching. + @discussion This method is broken into a category for unit testing purposes and should be used with the ASTableView and + * ASCollectionView batch fetching API. + */ +extern BOOL ASDisplayShouldFetchBatchForContext(ASBatchContext *context, + ASScrollDirection scrollDirection, + CGRect bounds, + CGSize contentSize, + CGPoint targetOffset, + CGFloat leadingScreens); + +ASDISPLAYNODE_EXTERN_C_END diff --git a/AsyncDisplayKit/Details/ASBatchFetching.m b/AsyncDisplayKit/Details/ASBatchFetching.m new file mode 100644 index 00000000..c0233cc6 --- /dev/null +++ b/AsyncDisplayKit/Details/ASBatchFetching.m @@ -0,0 +1,51 @@ +/* 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 "ASBatchFetching.h" + +BOOL ASDisplayShouldFetchBatchForContext(ASBatchContext *context, + ASScrollDirection scrollDirection, + CGRect bounds, + CGSize contentSize, + CGPoint targetOffset, + CGFloat leadingScreens) { + // do not allow fetching if a batch is already in-flight and hasn't been completed or cancelled + if ([context isFetching]) { + return NO; + } + + // no fetching for null states + if (leadingScreens <= 0.0 || + CGPointEqualToPoint(targetOffset, CGPointZero) || + CGSizeEqualToSize(contentSize, CGSizeZero) || + CGRectEqualToRect(bounds, CGRectZero)) { + return NO; + } + + // only Up and Left scrolls are currently supported (tail loading) + if (scrollDirection != ASScrollDirectionUp && scrollDirection != ASScrollDirectionLeft) { + return NO; + } + + CGFloat viewLength, offset, contentLength; + + if (scrollDirection == ASScrollDirectionUp) { + viewLength = bounds.size.height; + offset = targetOffset.y; + contentLength = contentSize.height; + } else { // horizontal + viewLength = bounds.size.width; + offset = targetOffset.x; + contentLength = contentSize.width; + } + + CGFloat triggerDistance = viewLength * leadingScreens; + CGFloat remainingDistance = contentLength - viewLength - offset; + + return remainingDistance <= triggerDistance; +} diff --git a/AsyncDisplayKit/Details/ASLayoutController.h b/AsyncDisplayKit/Details/ASLayoutController.h index 60482922..51a3c62a 100644 --- a/AsyncDisplayKit/Details/ASLayoutController.h +++ b/AsyncDisplayKit/Details/ASLayoutController.h @@ -10,20 +10,14 @@ #import #import +#import "ASScrollDirection.h" + typedef struct { CGFloat leadingBufferScreenfuls; CGFloat trailingBufferScreenfuls; } ASRangeTuningParameters; -typedef NS_ENUM(NSInteger, ASScrollDirection) { - ASScrollDirectionNone, - ASScrollDirectionRight, - ASScrollDirectionLeft, - ASScrollDirectionUp, - ASScrollDirectionDown, -}; - @protocol ASLayoutController /** diff --git a/AsyncDisplayKit/Details/ASScrollDirection.h b/AsyncDisplayKit/Details/ASScrollDirection.h new file mode 100644 index 00000000..5459feff --- /dev/null +++ b/AsyncDisplayKit/Details/ASScrollDirection.h @@ -0,0 +1,17 @@ +/* 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 + +typedef NS_ENUM(NSInteger, ASScrollDirection) { + ASScrollDirectionNone, + ASScrollDirectionRight, + ASScrollDirectionLeft, + ASScrollDirectionUp, + ASScrollDirectionDown, +}; diff --git a/AsyncDisplayKitTests/ASBatchFetchingTests.m b/AsyncDisplayKitTests/ASBatchFetchingTests.m new file mode 100644 index 00000000..48f367e2 --- /dev/null +++ b/AsyncDisplayKitTests/ASBatchFetchingTests.m @@ -0,0 +1,102 @@ +/* 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 + +#import "ASBatchFetching.h" + +@interface ASBatchFetchingTests : XCTestCase + +@end + +@implementation ASBatchFetchingTests + +#define PASSING_RECT (CGRect){0,0,1,1} +#define PASSING_SIZE (CGSize){1,1} +#define PASSING_POINT (CGPoint){1,1} +#define VERTICAL_RECT(h) (CGRect){0,0,0,h} +#define VERTICAL_SIZE(h) (CGSize){0,h} +#define VERTICAL_OFFSET(y) (CGPoint){0,y} +#define HORIZONTAL_RECT(w) (CGRect){0,0,w,0} +#define HORIZONTAL_SIZE(w) (CGSize){w,0} +#define HORIZONTAL_OFFSET(x) (CGPoint){x,0} + +- (void)testBatchNullState { + ASBatchContext *context = [[ASBatchContext alloc] init]; + BOOL shouldFetch = ASDisplayShouldFetchBatchForContext(context, ASScrollDirectionUp, CGRectZero, CGSizeZero, CGPointZero, 0.0); + XCTAssert(shouldFetch == NO, @"Should not fetch in the null state"); +} + +- (void)testBatchAlreadyFetching { + ASBatchContext *context = [[ASBatchContext alloc] init]; + [context beginBatchFetching]; + BOOL shouldFetch = ASDisplayShouldFetchBatchForContext(context, ASScrollDirectionUp, PASSING_RECT, PASSING_SIZE, PASSING_POINT, 1.0); + XCTAssert(shouldFetch == NO, @"Should not fetch when context is already fetching"); +} + +- (void)testUnsupportedScrollDirections { + ASBatchContext *context = [[ASBatchContext alloc] init]; + BOOL fetchRight = ASDisplayShouldFetchBatchForContext(context, ASScrollDirectionRight, PASSING_RECT, PASSING_SIZE, PASSING_POINT, 1.0); + XCTAssert(fetchRight == NO, @"Should not fetch for scrolling right"); + BOOL fetchDown = ASDisplayShouldFetchBatchForContext(context, ASScrollDirectionDown, PASSING_RECT, PASSING_SIZE, PASSING_POINT, 1.0); + XCTAssert(fetchDown == NO, @"Should not fetch for scrolling down"); + BOOL fetchUp = ASDisplayShouldFetchBatchForContext(context, ASScrollDirectionUp, PASSING_RECT, PASSING_SIZE, PASSING_POINT, 1.0); + XCTAssert(fetchUp == YES, @"Should fetch for scrolling up"); + BOOL fetchLeft = ASDisplayShouldFetchBatchForContext(context, ASScrollDirectionLeft, PASSING_RECT, PASSING_SIZE, PASSING_POINT, 1.0); + XCTAssert(fetchLeft == YES, @"Should fetch for scrolling left"); +} + +- (void)testVerticalScrollToExactLeading { + CGFloat screen = 1.0; + ASBatchContext *context = [[ASBatchContext alloc] init]; + // scroll to 1-screen top offset, height is 1 screen, so bottom is 1 screen away from end of content + BOOL shouldFetch = ASDisplayShouldFetchBatchForContext(context, ASScrollDirectionUp, VERTICAL_RECT(screen), VERTICAL_SIZE(screen * 3.0), VERTICAL_OFFSET(screen * 1.0), 1.0); + XCTAssert(shouldFetch == YES, @"Fetch should begin when vertically scrolling to exactly 1 leading screen away"); +} + +- (void)testVerticalScrollToLessThanLeading { + CGFloat screen = 1.0; + ASBatchContext *context = [[ASBatchContext alloc] init]; + // 3 screens of content, scroll only 1/2 of one screen + BOOL shouldFetch = ASDisplayShouldFetchBatchForContext(context, ASScrollDirectionUp, VERTICAL_RECT(screen), VERTICAL_SIZE(screen * 3.0), VERTICAL_OFFSET(screen * 0.5), 1.0); + XCTAssert(shouldFetch == NO, @"Fetch should not begin when vertically scrolling less than the leading distance away"); +} + +- (void)testVerticalScrollingPastContentSize { + CGFloat screen = 1.0; + ASBatchContext *context = [[ASBatchContext alloc] init]; + // 3 screens of content, top offset to 3-screens, height 1 screen, so its 1 screen past the leading + BOOL shouldFetch = ASDisplayShouldFetchBatchForContext(context, ASScrollDirectionUp, VERTICAL_RECT(screen), VERTICAL_SIZE(screen * 3.0), VERTICAL_OFFSET(screen * 3.0), 1.0); + XCTAssert(shouldFetch == YES, @"Fetch should begin when vertically scrolling past the content size"); +} + +- (void)testHorizontalScrollToExactLeading { + CGFloat screen = 1.0; + ASBatchContext *context = [[ASBatchContext alloc] init]; + // scroll to 1-screen left offset, width is 1 screen, so right is 1 screen away from end of content + BOOL shouldFetch = ASDisplayShouldFetchBatchForContext(context, ASScrollDirectionLeft, HORIZONTAL_RECT(screen), HORIZONTAL_SIZE(screen * 3.0), HORIZONTAL_OFFSET(screen * 1.0), 1.0); + XCTAssert(shouldFetch == YES, @"Fetch should begin when horizontally scrolling to exactly 1 leading screen away"); +} + +- (void)testHorizontalScrollToLessThanLeading { + CGFloat screen = 1.0; + ASBatchContext *context = [[ASBatchContext alloc] init]; + // 3 screens of content, scroll only 1/2 of one screen + BOOL shouldFetch = ASDisplayShouldFetchBatchForContext(context, ASScrollDirectionRight, HORIZONTAL_RECT(screen), HORIZONTAL_SIZE(screen * 3.0), HORIZONTAL_OFFSET(screen * 0.5), 1.0); + XCTAssert(shouldFetch == NO, @"Fetch should not begin when horizontally scrolling less than the leading distance away"); +} + +- (void)testHorizontalScrollingPastContentSize { + CGFloat screen = 1.0; + ASBatchContext *context = [[ASBatchContext alloc] init]; + // 3 screens of content, left offset to 3-screens, width 1 screen, so its 1 screen past the leading + BOOL shouldFetch = ASDisplayShouldFetchBatchForContext(context, ASScrollDirectionUp, HORIZONTAL_RECT(screen), HORIZONTAL_SIZE(screen * 3.0), HORIZONTAL_OFFSET(screen * 3.0), 1.0); + XCTAssert(shouldFetch == YES, @"Fetch should begin when vertically scrolling past the content size"); +} + +@end From 8d7fe334e258646d4bc4a2f2fdd635d78757084f Mon Sep 17 00:00:00 2001 From: Ryan Nystrom Date: Tue, 24 Feb 2015 16:55:12 -0800 Subject: [PATCH 4/5] Revisions based on feedback --- AsyncDisplayKit.xcodeproj/project.pbxproj | 20 ++++++------- AsyncDisplayKit/ASCollectionView.h | 30 +++++++++---------- AsyncDisplayKit/ASCollectionView.mm | 14 +++++---- AsyncDisplayKit/ASTableView.h | 30 +++++++++---------- AsyncDisplayKit/ASTableView.mm | 14 +++++---- AsyncDisplayKit/Details/ASBatchContext.h | 2 +- .../{ASBatchContext.m => ASBatchContext.mm} | 8 +++++ 7 files changed, 65 insertions(+), 53 deletions(-) rename AsyncDisplayKit/Details/{ASBatchContext.m => ASBatchContext.mm} (81%) diff --git a/AsyncDisplayKit.xcodeproj/project.pbxproj b/AsyncDisplayKit.xcodeproj/project.pbxproj index 0f18515d..98863648 100644 --- a/AsyncDisplayKit.xcodeproj/project.pbxproj +++ b/AsyncDisplayKit.xcodeproj/project.pbxproj @@ -144,12 +144,11 @@ 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 */; }; - 296A0A2E1A9516B2005ACEAA /* ASBatchFetching.h in Headers */ = {isa = PBXBuildFile; fileRef = 296A0A2C1A9516B2005ACEAA /* ASBatchFetching.h */; settings = {ATTRIBUTES = (Public, ); }; }; + 296A0A2E1A9516B2005ACEAA /* ASBatchFetching.h in Headers */ = {isa = PBXBuildFile; fileRef = 296A0A2C1A9516B2005ACEAA /* ASBatchFetching.h */; settings = {ATTRIBUTES = (Private, ); }; }; 296A0A2F1A9516B2005ACEAA /* ASBatchFetching.m in Sources */ = {isa = PBXBuildFile; fileRef = 296A0A2D1A9516B2005ACEAA /* ASBatchFetching.m */; }; 296A0A351A951ABF005ACEAA /* ASBatchFetchingTests.m in Sources */ = {isa = PBXBuildFile; fileRef = 296A0A341A951ABF005ACEAA /* ASBatchFetchingTests.m */; }; 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 */; }; + 299DA1AA1A828D2900162D41 /* ASBatchContext.mm in Sources */ = {isa = PBXBuildFile; fileRef = 299DA1A81A828D2900162D41 /* ASBatchContext.mm */; }; 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 */; }; @@ -302,12 +301,12 @@ 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 = ""; }; - 296A0A2C1A9516B2005ACEAA /* ASBatchFetching.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = ASBatchFetching.h; sourceTree = ""; }; - 296A0A2D1A9516B2005ACEAA /* ASBatchFetching.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = ASBatchFetching.m; sourceTree = ""; }; + 296A0A2C1A9516B2005ACEAA /* ASBatchFetching.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; name = ASBatchFetching.h; path = ../Details/ASBatchFetching.h; sourceTree = ""; }; + 296A0A2D1A9516B2005ACEAA /* ASBatchFetching.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; name = ASBatchFetching.m; path = ../Details/ASBatchFetching.m; sourceTree = ""; }; 296A0A311A951715005ACEAA /* ASScrollDirection.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; name = ASScrollDirection.h; path = AsyncDisplayKit/Details/ASScrollDirection.h; sourceTree = SOURCE_ROOT; }; 296A0A341A951ABF005ACEAA /* ASBatchFetchingTests.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = ASBatchFetchingTests.m; 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 = ""; }; + 299DA1A81A828D2900162D41 /* ASBatchContext.mm */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.cpp.objcpp; path = ASBatchContext.mm; 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 = ""; }; @@ -479,9 +478,7 @@ 054963471A1EA066000F8E56 /* ASBasicImageDownloader.h */, 054963481A1EA066000F8E56 /* ASBasicImageDownloader.mm */, 299DA1A71A828D2900162D41 /* ASBatchContext.h */, - 299DA1A81A828D2900162D41 /* ASBatchContext.m */, - 296A0A2C1A9516B2005ACEAA /* ASBatchFetching.h */, - 296A0A2D1A9516B2005ACEAA /* ASBatchFetching.m */, + 299DA1A81A828D2900162D41 /* ASBatchContext.mm */, 464052191A3F83C40061C0BA /* ASDataController.h */, 4640521A1A3F83C40061C0BA /* ASDataController.mm */, 05A6D05819D0EB64002DD95E /* ASDealloc2MainObject.h */, @@ -546,6 +543,8 @@ 058D0A01195D050800B7D73C /* Private */ = { isa = PBXGroup; children = ( + 296A0A2C1A9516B2005ACEAA /* ASBatchFetching.h */, + 296A0A2D1A9516B2005ACEAA /* ASBatchFetching.m */, 058D0A02195D050800B7D73C /* _AS-objc-internal.h */, 058D0A03195D050800B7D73C /* _ASCoreAnimationExtras.h */, 058D0A04195D050800B7D73C /* _ASCoreAnimationExtras.mm */, @@ -809,7 +808,7 @@ 058D0A18195D050800B7D73C /* _ASDisplayLayer.mm in Sources */, 058D0A2C195D050800B7D73C /* ASSentinel.m in Sources */, 464052211A3F83C40061C0BA /* ASDataController.mm in Sources */, - 299DA1AA1A828D2900162D41 /* ASBatchContext.m in Sources */, + 299DA1AA1A828D2900162D41 /* ASBatchContext.mm in Sources */, 058D0A15195D050800B7D73C /* ASDisplayNodeExtras.mm in Sources */, 058D0A1F195D050800B7D73C /* ASTextNodeTextKitHelpers.mm in Sources */, 055F1A3519ABD3E3004DAFF1 /* ASTableView.mm in Sources */, @@ -862,6 +861,7 @@ 058D0A3B195D057000B7D73C /* ASDisplayNodeTestsHelper.m in Sources */, 058D0A3A195D057000B7D73C /* ASDisplayNodeTests.m in Sources */, 052EE0661A159FEF002C6279 /* ASMultiplexImageNodeTests.m in Sources */, + 291D92411A9D537B008286B8 /* ASBatchFetching.m in Sources */, 058D0A39195D057000B7D73C /* ASDisplayNodeAppearanceTests.m in Sources */, 058D0A41195D057000B7D73C /* ASTextNodeWordKernerTests.mm in Sources */, 058D0A40195D057000B7D73C /* ASTextNodeTests.m in Sources */, diff --git a/AsyncDisplayKit/ASCollectionView.h b/AsyncDisplayKit/ASCollectionView.h index da640c61..fb28fa5e 100644 --- a/AsyncDisplayKit/ASCollectionView.h +++ b/AsyncDisplayKit/ASCollectionView.h @@ -174,6 +174,21 @@ - (void)collectionView:(ASCollectionView *)collectionView willDisplayNodeForItemAtIndexPath:(NSIndexPath *)indexPath; - (void)collectionView:(ASCollectionView *)collectionView didEndDisplayingNodeForItemAtIndexPath:(NSIndexPath*)indexPath; +/** + * 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. This method is called on a background queue. + * + * UICollectionView currently only supports batch events for tail loads. If you require a head load, consider + * implementing a UIRefreshControl. + */ +- (void)collectionView:(ASCollectionView *)collectionView willBeginBatchFetchWithContext:(ASBatchContext *)context; + /** * Tell the collectionView if batch fetching should begin. * @@ -187,21 +202,6 @@ */ - (BOOL)shouldBatchFetchForCollectionView:(ASCollectionView *)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:(ASCollectionView *)collectionView beginBatchFetchingWithContext:(ASBatchContext *)context; - @end @interface ASCollectionView (Deprecated) diff --git a/AsyncDisplayKit/ASCollectionView.mm b/AsyncDisplayKit/ASCollectionView.mm index 60ac8b1a..4463ae60 100644 --- a/AsyncDisplayKit/ASCollectionView.mm +++ b/AsyncDisplayKit/ASCollectionView.mm @@ -384,13 +384,13 @@ static BOOL _isInterceptedSelector(SEL sel) } } -- (BOOL)shouldFetchBatch +- (BOOL)shouldBatchFetch { - if ([self.asyncDelegate respondsToSelector:@selector(shouldBatchFetchForCollectionView:)]) { - return [self.asyncDelegate shouldBatchFetchForCollectionView:self]; + if ([_asyncDelegate respondsToSelector:@selector(shouldBatchFetchForCollectionView:)]) { + return [_asyncDelegate shouldBatchFetchForCollectionView:self]; } else { // if the delegate does not respond to this method, there is no point in starting to fetch - return [self.asyncDelegate respondsToSelector:@selector(collectionView:beginBatchFetchingWithContext:)]; + return [_asyncDelegate respondsToSelector:@selector(collectionView:willBeginBatchFetchWithContext:)]; } } @@ -398,13 +398,15 @@ static BOOL _isInterceptedSelector(SEL sel) { ASDisplayNodeAssert(_batchContext != nil, @"Batch context should exist"); - if (![self shouldFetchBatch]) { + if (![self shouldBatchFetch]) { return; } if (ASDisplayShouldFetchBatchForContext(_batchContext, [self scrollDirection], self.bounds, self.contentSize, targetOffset, _leadingScreensForBatching)) { [_batchContext beginBatchFetching]; - [self.asyncDelegate collectionView:self beginBatchFetchingWithContext:_batchContext]; + dispatch_async(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0), ^{ + [_asyncDelegate collectionView:self willBeginBatchFetchWithContext:_batchContext]; + }); } } diff --git a/AsyncDisplayKit/ASTableView.h b/AsyncDisplayKit/ASTableView.h index 08ac6be7..930afbd8 100644 --- a/AsyncDisplayKit/ASTableView.h +++ b/AsyncDisplayKit/ASTableView.h @@ -179,19 +179,6 @@ - (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:(ASTableView *)tableView; - /** * Receive a message that the tableView is near the end of its data set and more data should be fetched if necessary. * @@ -199,12 +186,25 @@ * @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. + * notifications to do batch fetches. This method is called on a background queue. * * ASTableView currently only supports batch events for tail loads. If you require a head load, consider implementing a * UIRefreshControl. */ -- (void)tableView:(ASTableView *)tableView beginBatchFetchingWithContext:(ASBatchContext *)context; +- (void)tableView:(ASTableView *)tableView willBeginBatchFetchWithContext:(ASBatchContext *)context; + +/** + * 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:(ASTableView *)tableView; @end diff --git a/AsyncDisplayKit/ASTableView.mm b/AsyncDisplayKit/ASTableView.mm index a1fbfa75..6ccf51fd 100644 --- a/AsyncDisplayKit/ASTableView.mm +++ b/AsyncDisplayKit/ASTableView.mm @@ -390,13 +390,13 @@ static BOOL _isInterceptedSelector(SEL sel) } } -- (BOOL)shouldFetchBatch +- (BOOL)shouldBatchFetch { - if ([self.asyncDelegate respondsToSelector:@selector(shouldBatchFetchForTableView:)]) { - return [self.asyncDelegate shouldBatchFetchForTableView:self]; + if ([_asyncDelegate respondsToSelector:@selector(shouldBatchFetchForTableView:)]) { + return [_asyncDelegate shouldBatchFetchForTableView:self]; } else { // if the delegate does not respond to this method, there is no point in starting to fetch - return [self.asyncDelegate respondsToSelector:@selector(tableView:beginBatchFetchingWithContext:)]; + return [_asyncDelegate respondsToSelector:@selector(tableView:willBeginBatchFetchWithContext:)]; } } @@ -404,13 +404,15 @@ static BOOL _isInterceptedSelector(SEL sel) { ASDisplayNodeAssert(_batchContext != nil, @"Batch context should exist"); - if (![self shouldFetchBatch]) { + if (![self shouldBatchFetch]) { return; } if (ASDisplayShouldFetchBatchForContext(_batchContext, [self scrollDirection], self.bounds, self.contentSize, targetOffset, _leadingScreensForBatching)) { [_batchContext beginBatchFetching]; - [self.asyncDelegate tableView:self beginBatchFetchingWithContext:_batchContext]; + dispatch_async(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0), ^{ + [_asyncDelegate tableView:self willBeginBatchFetchWithContext:_batchContext]; + }); } } diff --git a/AsyncDisplayKit/Details/ASBatchContext.h b/AsyncDisplayKit/Details/ASBatchContext.h index 2d8f2bb2..94c42293 100644 --- a/AsyncDisplayKit/Details/ASBatchContext.h +++ b/AsyncDisplayKit/Details/ASBatchContext.h @@ -53,7 +53,7 @@ * Notify the context object that fetching has started. * * @discussion Call this method only when you are beginning a fetch process. This should really only be called by the - * context object's owner. Calling this method should be complimented with -completeBatchFetching:. + * context object's owner. Calling this method should be paired with -completeBatchFetching:. */ - (void)beginBatchFetching; diff --git a/AsyncDisplayKit/Details/ASBatchContext.m b/AsyncDisplayKit/Details/ASBatchContext.mm similarity index 81% rename from AsyncDisplayKit/Details/ASBatchContext.m rename to AsyncDisplayKit/Details/ASBatchContext.mm index f3cbb0f7..4833cbd8 100644 --- a/AsyncDisplayKit/Details/ASBatchContext.m +++ b/AsyncDisplayKit/Details/ASBatchContext.mm @@ -8,6 +8,8 @@ #import "ASBatchContext.h" +#import "ASThread.h" + typedef NS_ENUM(NSInteger, ASBatchContextState) { ASBatchContextStateFetching, ASBatchContextStateCancelled, @@ -17,6 +19,7 @@ typedef NS_ENUM(NSInteger, ASBatchContextState) { @interface ASBatchContext () { ASBatchContextState _state; + ASDN::RecursiveMutex _propertyLock; } @end @@ -32,28 +35,33 @@ typedef NS_ENUM(NSInteger, ASBatchContextState) { - (BOOL)isFetching { + ASDN::MutexLocker l(_propertyLock); return _state == ASBatchContextStateFetching; } - (BOOL)batchFetchingWasCancelled { + ASDN::MutexLocker l(_propertyLock); return _state == ASBatchContextStateCancelled; } - (void)completeBatchFetching:(BOOL)didComplete { if (didComplete) { + ASDN::MutexLocker l(_propertyLock); _state = ASBatchContextStateCompleted; } } - (void)beginBatchFetching { + ASDN::MutexLocker l(_propertyLock); _state = ASBatchContextStateFetching; } - (void)cancelBatchFetching { + ASDN::MutexLocker l(_propertyLock); _state = ASBatchContextStateCancelled; } From c657411a2e99e3a2195a03b1382c40815ff986a2 Mon Sep 17 00:00:00 2001 From: Ryan Nystrom Date: Thu, 26 Feb 2015 15:40:06 -0800 Subject: [PATCH 5/5] Batch fetch with small data sets --- AsyncDisplayKit/ASCollectionView.mm | 7 ++++--- AsyncDisplayKit/ASTableView.mm | 7 ++++--- AsyncDisplayKit/Details/ASBatchFetching.m | 17 +++++++++-------- AsyncDisplayKitTests/ASBatchFetchingTests.m | 16 ++++++++++++++++ .../ASCollectionView/Sample/ViewController.m | 2 +- examples/Kittens/Sample/ViewController.m | 6 +++--- 6 files changed, 37 insertions(+), 18 deletions(-) diff --git a/AsyncDisplayKit/ASCollectionView.mm b/AsyncDisplayKit/ASCollectionView.mm index 4463ae60..256ae110 100644 --- a/AsyncDisplayKit/ASCollectionView.mm +++ b/AsyncDisplayKit/ASCollectionView.mm @@ -386,11 +386,12 @@ static BOOL _isInterceptedSelector(SEL sel) - (BOOL)shouldBatchFetch { - if ([_asyncDelegate respondsToSelector:@selector(shouldBatchFetchForCollectionView:)]) { + // if the delegate does not respond to this method, there is no point in starting to fetch + BOOL canFetch = [_asyncDelegate respondsToSelector:@selector(collectionView:willBeginBatchFetchWithContext:)]; + if (canFetch && [_asyncDelegate respondsToSelector:@selector(shouldBatchFetchForCollectionView:)]) { return [_asyncDelegate shouldBatchFetchForCollectionView:self]; } else { - // if the delegate does not respond to this method, there is no point in starting to fetch - return [_asyncDelegate respondsToSelector:@selector(collectionView:willBeginBatchFetchWithContext:)]; + return canFetch; } } diff --git a/AsyncDisplayKit/ASTableView.mm b/AsyncDisplayKit/ASTableView.mm index 6ccf51fd..d3488657 100644 --- a/AsyncDisplayKit/ASTableView.mm +++ b/AsyncDisplayKit/ASTableView.mm @@ -392,11 +392,12 @@ static BOOL _isInterceptedSelector(SEL sel) - (BOOL)shouldBatchFetch { - if ([_asyncDelegate respondsToSelector:@selector(shouldBatchFetchForTableView:)]) { + // if the delegate does not respond to this method, there is no point in starting to fetch + BOOL canFetch = [_asyncDelegate respondsToSelector:@selector(tableView:willBeginBatchFetchWithContext:)]; + if (canFetch && [_asyncDelegate respondsToSelector:@selector(shouldBatchFetchForTableView:)]) { return [_asyncDelegate shouldBatchFetchForTableView:self]; } else { - // if the delegate does not respond to this method, there is no point in starting to fetch - return [_asyncDelegate respondsToSelector:@selector(tableView:willBeginBatchFetchWithContext:)]; + return canFetch; } } diff --git a/AsyncDisplayKit/Details/ASBatchFetching.m b/AsyncDisplayKit/Details/ASBatchFetching.m index c0233cc6..c4027a70 100644 --- a/AsyncDisplayKit/Details/ASBatchFetching.m +++ b/AsyncDisplayKit/Details/ASBatchFetching.m @@ -19,16 +19,14 @@ BOOL ASDisplayShouldFetchBatchForContext(ASBatchContext *context, return NO; } - // no fetching for null states - if (leadingScreens <= 0.0 || - CGPointEqualToPoint(targetOffset, CGPointZero) || - CGSizeEqualToSize(contentSize, CGSizeZero) || - CGRectEqualToRect(bounds, CGRectZero)) { + // only Up and Left scrolls are currently supported (tail loading) + if (scrollDirection != ASScrollDirectionUp && scrollDirection != ASScrollDirectionLeft) { return NO; } - // only Up and Left scrolls are currently supported (tail loading) - if (scrollDirection != ASScrollDirectionUp && scrollDirection != ASScrollDirectionLeft) { + // no fetching for null states + if (leadingScreens <= 0.0 || + CGRectEqualToRect(bounds, CGRectZero)) { return NO; } @@ -44,8 +42,11 @@ BOOL ASDisplayShouldFetchBatchForContext(ASBatchContext *context, contentLength = contentSize.width; } + // target offset will always be 0 if the content size is smaller than the viewport + BOOL hasSmallContent = offset == 0.0 && contentLength < viewLength; + CGFloat triggerDistance = viewLength * leadingScreens; CGFloat remainingDistance = contentLength - viewLength - offset; - return remainingDistance <= triggerDistance; + return hasSmallContent || remainingDistance <= triggerDistance; } diff --git a/AsyncDisplayKitTests/ASBatchFetchingTests.m b/AsyncDisplayKitTests/ASBatchFetchingTests.m index 48f367e2..3b009cdd 100644 --- a/AsyncDisplayKitTests/ASBatchFetchingTests.m +++ b/AsyncDisplayKitTests/ASBatchFetchingTests.m @@ -99,4 +99,20 @@ XCTAssert(shouldFetch == YES, @"Fetch should begin when vertically scrolling past the content size"); } +- (void)testVerticalScrollingSmallContentSize { + CGFloat screen = 1.0; + ASBatchContext *context = [[ASBatchContext alloc] init]; + // when the content size is < screen size, the target offset will always be 0 + BOOL shouldFetch = ASDisplayShouldFetchBatchForContext(context, ASScrollDirectionUp, VERTICAL_RECT(screen), VERTICAL_SIZE(screen * 0.5), VERTICAL_OFFSET(0.0), 1.0); + XCTAssert(shouldFetch == YES, @"Fetch should begin when the target is 0 and the content size is smaller than the scree"); +} + +- (void)testHorizontalScrollingSmallContentSize { + CGFloat screen = 1.0; + ASBatchContext *context = [[ASBatchContext alloc] init]; + // when the content size is < screen size, the target offset will always be 0 + BOOL shouldFetch = ASDisplayShouldFetchBatchForContext(context, ASScrollDirectionLeft, HORIZONTAL_RECT(screen), HORIZONTAL_SIZE(screen * 0.5), HORIZONTAL_OFFSET(0.0), 1.0); + XCTAssert(shouldFetch == YES, @"Fetch should begin when the target is 0 and the content size is smaller than the scree"); +} + @end diff --git a/examples/ASCollectionView/Sample/ViewController.m b/examples/ASCollectionView/Sample/ViewController.m index 7b8bfcda..5e2ab429 100644 --- a/examples/ASCollectionView/Sample/ViewController.m +++ b/examples/ASCollectionView/Sample/ViewController.m @@ -89,7 +89,7 @@ // unlock the data source to enable data source updating. } -- (void)collectionView:(UICollectionView *)collectionView beginBatchFetchingWithContext:(ASBatchContext *)context +- (void)collectionView:(UICollectionView *)collectionView willBeginBatchFetchWithContext:(ASBatchContext *)context { NSLog(@"fetch additional content"); [context completeBatchFetching:YES]; diff --git a/examples/Kittens/Sample/ViewController.m b/examples/Kittens/Sample/ViewController.m index 5433b4a0..f9c88384 100644 --- a/examples/Kittens/Sample/ViewController.m +++ b/examples/Kittens/Sample/ViewController.m @@ -55,7 +55,7 @@ static const NSInteger kMaxLitterSize = 100; // populate our "data source" with some random kittens - _kittenDataSource = [self createLitterWithSize:kLitterSize];; + _kittenDataSource = [self createLitterWithSize:kLitterSize]; return self; } @@ -63,7 +63,7 @@ static const NSInteger kMaxLitterSize = 100; - (NSArray *)createLitterWithSize:(NSInteger)litterSize { NSMutableArray *kittens = [NSMutableArray arrayWithCapacity:litterSize]; - for (NSInteger i = 0; i < kLitterSize; i++) { + for (NSInteger i = 0; i < litterSize; 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); @@ -140,7 +140,7 @@ static const NSInteger kMaxLitterSize = 100; return _kittenDataSource.count < kMaxLitterSize; } -- (void)tableView:(UITableView *)tableView beginBatchFetchingWithContext:(ASBatchContext *)context +- (void)tableView:(UITableView *)tableView willBeginBatchFetchWithContext:(ASBatchContext *)context { NSLog(@"adding kitties"); dispatch_async(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0), ^{