diff --git a/AsyncDisplayKit.xcodeproj/project.pbxproj b/AsyncDisplayKit.xcodeproj/project.pbxproj index 9fdb19ca14..ff21572756 100644 --- a/AsyncDisplayKit.xcodeproj/project.pbxproj +++ b/AsyncDisplayKit.xcodeproj/project.pbxproj @@ -17,6 +17,7 @@ 051943151A1575670030A7D0 /* Photos.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 051943141A1575670030A7D0 /* Photos.framework */; settings = {ATTRIBUTES = (Weak, ); }; }; 052EE0661A159FEF002C6279 /* ASMultiplexImageNodeTests.m in Sources */ = {isa = PBXBuildFile; fileRef = 052EE0651A159FEF002C6279 /* ASMultiplexImageNodeTests.m */; settings = {COMPILER_FLAGS = "-fno-objc-arc"; }; }; 052EE06B1A15A0D8002C6279 /* TestResources in Resources */ = {isa = PBXBuildFile; fileRef = 052EE06A1A15A0D8002C6279 /* TestResources */; }; + 0540322B1A37B69C00001D02 /* ASEqualityHelpers.h in Headers */ = {isa = PBXBuildFile; fileRef = 0540322A1A37B69C00001D02 /* ASEqualityHelpers.h */; settings = {ATTRIBUTES = (Public, ); }; }; 054963491A1EA066000F8E56 /* ASBasicImageDownloader.h in Headers */ = {isa = PBXBuildFile; fileRef = 054963471A1EA066000F8E56 /* ASBasicImageDownloader.h */; settings = {ATTRIBUTES = (Public, ); }; }; 0549634A1A1EA066000F8E56 /* ASBasicImageDownloader.mm in Sources */ = {isa = PBXBuildFile; fileRef = 054963481A1EA066000F8E56 /* ASBasicImageDownloader.mm */; }; 055B9FA81A1C154B00035D6D /* ASNetworkImageNode.h in Headers */ = {isa = PBXBuildFile; fileRef = 055B9FA61A1C154B00035D6D /* ASNetworkImageNode.h */; settings = {ATTRIBUTES = (Public, ); }; }; @@ -134,8 +135,14 @@ 05A6D05A19D0EB64002DD95E /* ASDealloc2MainObject.h in Headers */ = {isa = PBXBuildFile; fileRef = 05A6D05819D0EB64002DD95E /* ASDealloc2MainObject.h */; settings = {ATTRIBUTES = (Public, ); }; }; 05A6D05B19D0EB64002DD95E /* ASDealloc2MainObject.m in Sources */ = {isa = PBXBuildFile; fileRef = 05A6D05919D0EB64002DD95E /* ASDealloc2MainObject.m */; settings = {COMPILER_FLAGS = "-fno-objc-arc"; }; }; 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 */; }; 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 */; }; + 464052221A3F83C40061C0BA /* ASFlowLayoutController.h in Headers */ = {isa = PBXBuildFile; fileRef = 4640521B1A3F83C40061C0BA /* ASFlowLayoutController.h */; settings = {ATTRIBUTES = (Public, ); }; }; + 464052231A3F83C40061C0BA /* ASFlowLayoutController.mm in Sources */ = {isa = PBXBuildFile; fileRef = 4640521C1A3F83C40061C0BA /* ASFlowLayoutController.mm */; }; + 464052241A3F83C40061C0BA /* ASLayoutController.h in Headers */ = {isa = PBXBuildFile; fileRef = 4640521D1A3F83C40061C0BA /* ASLayoutController.h */; settings = {ATTRIBUTES = (Public, ); }; }; + 464052251A3F83C40061C0BA /* ASMultidimensionalArrayUtils.h in Headers */ = {isa = PBXBuildFile; fileRef = 4640521E1A3F83C40061C0BA /* ASMultidimensionalArrayUtils.h */; }; + 464052261A3F83C40061C0BA /* ASMultidimensionalArrayUtils.mm in Sources */ = {isa = PBXBuildFile; fileRef = 4640521F1A3F83C40061C0BA /* ASMultidimensionalArrayUtils.mm */; }; 6BDC61F61979037800E50D21 /* AsyncDisplayKit.h in Headers */ = {isa = PBXBuildFile; fileRef = 6BDC61F51978FEA400E50D21 /* AsyncDisplayKit.h */; settings = {ATTRIBUTES = (Public, ); }; }; AC3C4A511A1139C100143C57 /* ASCollectionView.h in Headers */ = {isa = PBXBuildFile; fileRef = AC3C4A4F1A1139C100143C57 /* ASCollectionView.h */; settings = {ATTRIBUTES = (Public, ); }; }; AC3C4A521A1139C100143C57 /* ASCollectionView.m in Sources */ = {isa = PBXBuildFile; fileRef = AC3C4A501A1139C100143C57 /* ASCollectionView.m */; }; @@ -175,6 +182,7 @@ 052EE0651A159FEF002C6279 /* ASMultiplexImageNodeTests.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = ASMultiplexImageNodeTests.m; sourceTree = ""; }; 052EE06A1A15A0D8002C6279 /* TestResources */ = {isa = PBXFileReference; lastKnownFileType = folder; path = TestResources; sourceTree = ""; }; 053011A719B9882B00A9F2D0 /* ASRangeControllerInternal.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = ASRangeControllerInternal.h; sourceTree = ""; }; + 0540322A1A37B69C00001D02 /* ASEqualityHelpers.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = ASEqualityHelpers.h; sourceTree = ""; }; 054963471A1EA066000F8E56 /* ASBasicImageDownloader.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = ASBasicImageDownloader.h; sourceTree = ""; }; 054963481A1EA066000F8E56 /* ASBasicImageDownloader.mm */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.cpp.objcpp; path = ASBasicImageDownloader.mm; sourceTree = ""; }; 055B9FA61A1C154B00035D6D /* ASNetworkImageNode.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = ASNetworkImageNode.h; sourceTree = ""; }; @@ -269,8 +277,14 @@ 05A6D05819D0EB64002DD95E /* ASDealloc2MainObject.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; name = ASDealloc2MainObject.h; path = ../Details/ASDealloc2MainObject.h; sourceTree = ""; }; 05A6D05919D0EB64002DD95E /* ASDealloc2MainObject.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; name = ASDealloc2MainObject.m; path = ../Details/ASDealloc2MainObject.m; sourceTree = ""; }; 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 = ""; }; 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 = ""; }; + 4640521B1A3F83C40061C0BA /* ASFlowLayoutController.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = ASFlowLayoutController.h; sourceTree = ""; }; + 4640521C1A3F83C40061C0BA /* ASFlowLayoutController.mm */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.cpp.objcpp; path = ASFlowLayoutController.mm; sourceTree = ""; }; + 4640521D1A3F83C40061C0BA /* ASLayoutController.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = ASLayoutController.h; sourceTree = ""; }; + 4640521E1A3F83C40061C0BA /* ASMultidimensionalArrayUtils.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = ASMultidimensionalArrayUtils.h; sourceTree = ""; }; + 4640521F1A3F83C40061C0BA /* ASMultidimensionalArrayUtils.mm */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.cpp.objcpp; path = ASMultidimensionalArrayUtils.mm; sourceTree = ""; }; 6BDC61F51978FEA400E50D21 /* AsyncDisplayKit.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = AsyncDisplayKit.h; sourceTree = ""; }; AC3C4A4F1A1139C100143C57 /* ASCollectionView.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = ASCollectionView.h; sourceTree = ""; }; AC3C4A501A1139C100143C57 /* ASCollectionView.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = ASCollectionView.m; sourceTree = ""; }; @@ -419,6 +433,13 @@ 058D09E1195D050800B7D73C /* Details */ = { isa = PBXGroup; children = ( + 464052191A3F83C40061C0BA /* ASDataController.h */, + 4640521A1A3F83C40061C0BA /* ASDataController.mm */, + 4640521B1A3F83C40061C0BA /* ASFlowLayoutController.h */, + 4640521C1A3F83C40061C0BA /* ASFlowLayoutController.mm */, + 4640521D1A3F83C40061C0BA /* ASLayoutController.h */, + 4640521E1A3F83C40061C0BA /* ASMultidimensionalArrayUtils.h */, + 4640521F1A3F83C40061C0BA /* ASMultidimensionalArrayUtils.mm */, 05F20AA31A15733C00DCA68A /* ASImageProtocols.h */, 058D09E2195D050800B7D73C /* _ASDisplayLayer.h */, 058D09E3195D050800B7D73C /* _ASDisplayLayer.mm */, @@ -499,7 +520,7 @@ 0516FA3A1A15563400B4EBED /* ASAvailability.h */, 058D0A44195D058D00B7D73C /* ASBaseDefines.h */, 058D0A45195D058D00B7D73C /* ASDisplayNodeExtraIvars.h */, - 1950C4481A3BB5C1005C8279 /* ASEqualityHelpers.h */, + 0540322A1A37B69C00001D02 /* ASEqualityHelpers.h */, 0516FA3B1A15563400B4EBED /* ASLog.h */, ); path = Base; @@ -521,13 +542,15 @@ isa = PBXHeadersBuildPhase; buildActionMask = 2147483647; files = ( + 464052221A3F83C40061C0BA /* ASFlowLayoutController.h in Headers */, + 464052241A3F83C40061C0BA /* ASLayoutController.h in Headers */, + 464052201A3F83C40061C0BA /* ASDataController.h in Headers */, 05A6D05A19D0EB64002DD95E /* ASDealloc2MainObject.h in Headers */, 0516FA401A1563D200B4EBED /* ASMultiplexImageNode.h in Headers */, 058D0A47195D05CB00B7D73C /* ASControlNode.h in Headers */, 058D0A48195D05CB00B7D73C /* ASControlNode.m in Headers */, 058D0A49195D05CB00B7D73C /* ASControlNode+Subclasses.h in Headers */, 058D0A4A195D05CB00B7D73C /* ASDisplayNode.h in Headers */, - 1950C4491A3BB5C1005C8279 /* ASEqualityHelpers.h in Headers */, 058D0A4B195D05CB00B7D73C /* ASDisplayNode.mm in Headers */, 058D0A4C195D05CB00B7D73C /* ASDisplayNode+Subclasses.h in Headers */, 058D0A4D195D05CB00B7D73C /* ASDisplayNodeExtras.h in Headers */, @@ -557,6 +580,7 @@ 058D0A61195D05DC00B7D73C /* ASTextNodeTextKitHelpers.h in Headers */, 058D0A62195D05DC00B7D73C /* ASTextNodeTextKitHelpers.mm in Headers */, 058D0A63195D05DC00B7D73C /* ASTextNodeTypes.h in Headers */, + 464052251A3F83C40061C0BA /* ASMultidimensionalArrayUtils.h in Headers */, 058D0A64195D05DC00B7D73C /* ASTextNodeWordKerner.h in Headers */, 058D0A65195D05DC00B7D73C /* ASTextNodeWordKerner.m in Headers */, 058D0A66195D05DC00B7D73C /* NSMutableAttributedString+TextKitAdditions.h in Headers */, @@ -580,6 +604,7 @@ 055B9FA81A1C154B00035D6D /* ASNetworkImageNode.h in Headers */, 054963491A1EA066000F8E56 /* ASBasicImageDownloader.h in Headers */, AC3C4A541A113EEC00143C57 /* ASCollectionViewProtocols.h in Headers */, + 0540322B1A37B69C00001D02 /* ASEqualityHelpers.h in Headers */, 05F20AA41A15733C00DCA68A /* ASImageProtocols.h in Headers */, 058D0A71195D05F800B7D73C /* _AS-objc-internal.h in Headers */, 058D0A72195D05F800B7D73C /* _ASCoreAnimationExtras.h in Headers */, @@ -723,15 +748,18 @@ 058D0A1E195D050800B7D73C /* ASTextNodeShadower.m in Sources */, 058D0A18195D050800B7D73C /* _ASDisplayLayer.mm in Sources */, 058D0A2C195D050800B7D73C /* ASSentinel.m in Sources */, + 464052211A3F83C40061C0BA /* ASDataController.mm in Sources */, 058D0A15195D050800B7D73C /* ASDisplayNodeExtras.mm in Sources */, 058D0A1F195D050800B7D73C /* ASTextNodeTextKitHelpers.mm in Sources */, 055F1A3519ABD3E3004DAFF1 /* ASTableView.m in Sources */, + 464052261A3F83C40061C0BA /* ASMultidimensionalArrayUtils.mm in Sources */, 055B9FA91A1C154B00035D6D /* ASNetworkImageNode.mm in Sources */, 058D0A1D195D050800B7D73C /* ASTextNodeRenderer.mm in Sources */, 058D0A2A195D050800B7D73C /* ASDisplayNode+UIViewBridge.mm in Sources */, AC3C4A521A1139C100143C57 /* ASCollectionView.m in Sources */, 058D0A20195D050800B7D73C /* ASTextNodeWordKerner.m in Sources */, 058D0A1A195D050800B7D73C /* ASHighlightOverlayLayer.mm in Sources */, + 464052231A3F83C40061C0BA /* ASFlowLayoutController.mm in Sources */, 058D0A28195D050800B7D73C /* ASDisplayNode+AsyncDisplay.mm in Sources */, 058D0A21195D050800B7D73C /* NSMutableAttributedString+TextKitAdditions.m in Sources */, 058D0A25195D050800B7D73C /* UIView+ASConvenience.m in Sources */, diff --git a/AsyncDisplayKit/ASCellNode.m b/AsyncDisplayKit/ASCellNode.m index 1c37c831c9..6efe37045a 100644 --- a/AsyncDisplayKit/ASCellNode.m +++ b/AsyncDisplayKit/ASCellNode.m @@ -9,21 +9,12 @@ #import "ASCellNode.h" #import "ASDisplayNode+Subclasses.h" -#import "ASRangeControllerInternal.h" #import "ASTextNode.h" #pragma mark - #pragma mark ASCellNode -@interface ASCellNode () { - // used by ASRangeController machinery - NSIndexPath *_asyncdisplaykit_indexPath; -} - -@end - - @implementation ASCellNode - (instancetype)init @@ -43,20 +34,6 @@ ASDisplayNodeAssert(!layerBacked, @"ASCellNode does not support layer-backing."); } -// TODO consider making this property an associated object in ASRangeController.mm -- (NSIndexPath *)asyncdisplaykit_indexPath -{ - return _asyncdisplaykit_indexPath; -} - -- (void)setAsyncdisplaykit_indexPath:(NSIndexPath *)asyncdisplaykit_indexPath -{ - if (_asyncdisplaykit_indexPath == asyncdisplaykit_indexPath) - return; - - _asyncdisplaykit_indexPath = [asyncdisplaykit_indexPath copy]; -} - @end diff --git a/AsyncDisplayKit/ASCollectionView.h b/AsyncDisplayKit/ASCollectionView.h index f5e320fd3b..9713f267b3 100644 --- a/AsyncDisplayKit/ASCollectionView.h +++ b/AsyncDisplayKit/ASCollectionView.h @@ -34,13 +34,6 @@ */ @property (nonatomic, assign) ASRangeTuningParameters rangeTuningParameters; -/** - * @abstract An optional block which can perform custom calculation for working range. - * - * @discussion Can be used to provide custom working range logic for custom layouts. - */ -@property (nonatomic, assign) asrangecontroller_working_range_calculation_block_t workingRangeCalculationBlock; - /** * Reload everything from scratch, destroying the working range and all cached nodes. * @@ -49,30 +42,26 @@ - (void)reloadData; /** - * WARNING: ASCollectionView's update/editing support is not yet implemented. Use of these methods will fire an assertion. + * Section updating. * - * This initial version of ASCollectionView only supports appending nodes (see below). If you'd like to see full-fledged - * support for data source updates and interactive editing, please file a GitHub issue -- AsyncDisplayKit can do it, - * we just haven't built it out yet. :] + * All operations are asynchronous and thread safe. You can call it from background thread (it is recommendated) and the UI table + * view will be updated asynchronously. The asyncDataSource must be updated to reflect the changes before these methods are called. */ -//- (void)insertSections:(NSIndexSet *)sections; -//- (void)deleteSections:(NSIndexSet *)sections; -//- (void)reloadSections:(NSIndexSet *)sections; -//- (void)moveSection:(NSInteger)section toSection:(NSInteger)newSection; -// -//- (void)insertItemsAtIndexPaths:(NSArray *)indexPaths; -//- (void)deleteItemsAtIndexPaths:(NSArray *)indexPaths; -//- (void)reloadItemsAtIndexPaths:(NSArray *)indexPaths; -//- (void)moveItemAtIndexPath:(NSIndexPath *)indexPath toIndexPath:(NSIndexPath *)newIndexPath; +- (void)insertSections:(NSIndexSet *)sections; +- (void)deleteSections:(NSIndexSet *)sections; +- (void)reloadSections:(NSIndexSet *)sections; +- (void)moveSection:(NSInteger)section toSection:(NSInteger)newSection; /** - * Append nodes. + * Items updating. * - * As with UICollectionView, the asyncDataSource must be updated to reflect the new nodes before this method is called. - * - * @param indexPaths Ordered array of index paths corresponding to the nodes to be added. + * All operations are asynchronous and thread safe. You can call it from background thread (it is recommendated) and the UI table + * view will be updated asynchronously. The asyncDataSource must be updated to reflect the changes before these methods are called. */ -- (void)appendNodesWithIndexPaths:(NSArray *)indexPaths; +- (void)insertItemsAtIndexPaths:(NSArray *)indexPaths; +- (void)deleteItemsAtIndexPaths:(NSArray *)indexPaths; +- (void)reloadItemsAtIndexPaths:(NSArray *)indexPaths; +- (void)moveItemAtIndexPath:(NSIndexPath *)indexPath toIndexPath:(NSIndexPath *)newIndexPath; /** * Query the sized node at `indexPath` for its calculatedSize. diff --git a/AsyncDisplayKit/ASCollectionView.m b/AsyncDisplayKit/ASCollectionView.m index cfba00839e..0de221a25d 100644 --- a/AsyncDisplayKit/ASCollectionView.m +++ b/AsyncDisplayKit/ASCollectionView.m @@ -9,7 +9,9 @@ #import "ASCollectionView.h" #import "ASAssert.h" +#import "ASFlowLayoutController.h" #import "ASRangeController.h" +#import "ASDataController.h" #pragma mark - @@ -87,11 +89,13 @@ static BOOL _isInterceptedSelector(SEL sel) #pragma mark - #pragma mark ASCollectionView. -@interface ASCollectionView () { +@interface ASCollectionView () { _ASCollectionViewProxy *_proxyDataSource; _ASCollectionViewProxy *_proxyDelegate; - + + ASDataController *_dataController; ASRangeController *_rangeController; + ASFlowLayoutController *_layoutController; } @end @@ -105,22 +109,33 @@ static BOOL _isInterceptedSelector(SEL sel) { if (!(self = [super initWithFrame:frame collectionViewLayout:layout])) return nil; - + + ASDisplayNodeAssert([layout isKindOfClass:UICollectionViewFlowLayout.class], @"only flow layouts are currently supported"); + + ASFlowLayoutDirection direction = (((UICollectionViewFlowLayout *)layout).scrollDirection == UICollectionViewScrollDirectionHorizontal) ? ASFlowLayoutDirectionHorizontal : ASFlowLayoutDirectionVertical; + _layoutController = [[ASFlowLayoutController alloc] initWithScrollOption:direction]; + _rangeController = [[ASRangeController alloc] init]; _rangeController.delegate = self; + _rangeController.layoutController = _layoutController; + + _dataController = [[ASDataController alloc] init]; + _dataController.delegate = _rangeController; + _dataController.dataSource = self; [self registerClass:[UICollectionViewCell class] forCellWithReuseIdentifier:@"_ASCollectionViewCell"]; return self; } - #pragma mark - #pragma mark Overrides. - (void)reloadData { - [_rangeController rebuildData]; + [_dataController reloadData]; + + // It is required [super reloadData]; } @@ -169,81 +184,59 @@ static BOOL _isInterceptedSelector(SEL sel) - (ASRangeTuningParameters)rangeTuningParameters { - return _rangeController.tuningParameters; + return _layoutController.tuningParameters; } - (void)setRangeTuningParameters:(ASRangeTuningParameters)tuningParameters { - _rangeController.tuningParameters = tuningParameters; -} - -- (asrangecontroller_working_range_calculation_block_t)workingRangeCalculationBlock -{ - return _rangeController.workingRangeCalculationBlock; -} - -- (void)setWorkingRangeCalculationBlock:(asrangecontroller_working_range_calculation_block_t)workingRangeCalculationBlock -{ - _rangeController.workingRangeCalculationBlock = workingRangeCalculationBlock; -} - -- (void)appendNodesWithIndexPaths:(NSArray *)indexPaths -{ - [_rangeController appendNodesWithIndexPaths:indexPaths]; + _layoutController.tuningParameters = tuningParameters; } - (CGSize)calculatedSizeForNodeAtIndexPath:(NSIndexPath *)indexPath { - return [_rangeController calculatedSizeForNodeAtIndexPath:indexPath]; + return [[_dataController nodeAtIndexPath:indexPath] calculatedSize]; } #pragma mark Assertions. -- (void)throwUnimplementedException -{ - [[NSException exceptionWithName:@"UnimplementedException" - reason:@"ASCollectionView's update/editing support is not yet implemented. Please see ASCollectionView.h." - userInfo:nil] raise]; -} - - (void)insertSections:(NSIndexSet *)sections { - [self throwUnimplementedException]; + [_dataController insertSections:sections]; } - (void)deleteSections:(NSIndexSet *)sections { - [self throwUnimplementedException]; + [_dataController deleteSections:sections]; } - (void)reloadSections:(NSIndexSet *)sections { - [self throwUnimplementedException]; + [_dataController reloadSections:sections]; } - (void)moveSection:(NSInteger)section toSection:(NSInteger)newSection { - [self throwUnimplementedException]; + [_dataController moveSection:section toSection:newSection]; } - (void)insertItemsAtIndexPaths:(NSArray *)indexPaths { - [self throwUnimplementedException]; + [_dataController insertRowsAtIndexPaths:indexPaths]; } - (void)deleteItemsAtIndexPaths:(NSArray *)indexPaths { - [self throwUnimplementedException]; + [_dataController deleteRowsAtIndexPaths:indexPaths]; } - (void)reloadItemsAtIndexPaths:(NSArray *)indexPaths { - [self throwUnimplementedException]; + [_dataController reloadRowsAtIndexPaths:indexPaths]; } - (void)moveItemAtIndexPath:(NSIndexPath *)indexPath toIndexPath:(NSIndexPath *)newIndexPath { - [self throwUnimplementedException]; + [_dataController moveRowAtIndexPath:indexPath toIndexPath:newIndexPath]; } @@ -255,30 +248,53 @@ static BOOL _isInterceptedSelector(SEL sel) static NSString *reuseIdentifier = @"_ASCollectionViewCell"; UICollectionViewCell *cell = [self dequeueReusableCellWithReuseIdentifier:reuseIdentifier forIndexPath:indexPath]; + + ASCellNode *node = [_dataController nodeAtIndexPath:indexPath]; - [_rangeController configureContentView:cell.contentView forIndexPath:indexPath]; + [_rangeController configureContentView:cell.contentView forCellNode:node]; return cell; } - (CGSize)collectionView:(UICollectionView *)collectionView layout:(UICollectionViewLayout *)collectionViewLayout sizeForItemAtIndexPath:(NSIndexPath *)indexPath { - return [_rangeController calculatedSizeForNodeAtIndexPath:indexPath]; + return [[_dataController nodeAtIndexPath:indexPath] calculatedSize]; } - (NSInteger)numberOfSectionsInCollectionView:(UICollectionView *)collectionView { - return [_rangeController numberOfSizedSections]; + return [_dataController numberOfSections]; } - (NSInteger)collectionView:(UICollectionView *)collectionView numberOfItemsInSection:(NSInteger)section { - return [_rangeController numberOfSizedRowsInSection:section]; + return [_dataController numberOfRowsInSection:section]; +} + +- (ASScrollDirection)scrollDirection +{ + CGPoint scrollVelocity = [self.panGestureRecognizer velocityInView:self.superview]; + ASScrollDirection direction = ASScrollDirectionNone; + if (_layoutController.layoutDirection == ASFlowLayoutDirectionHorizontal) { + if (scrollVelocity.x > 0) { + direction = ASScrollDirectionRight; + } else if (scrollVelocity.x < 0) { + direction = ASScrollDirectionLeft; + } + } else { + if (scrollVelocity.y > 0) { + direction = ASScrollDirectionDown; + } else { + direction = ASScrollDirectionUp; + } + } + + return direction; } - (void)collectionView:(UICollectionView *)collectionView willDisplayCell:(UICollectionViewCell *)cell forItemAtIndexPath:(NSIndexPath *)indexPath { - [_rangeController visibleNodeIndexPathsDidChange]; + [_rangeController visibleNodeIndexPathsDidChangeWithScrollDirection:self.scrollDirection]; if ([_asyncDelegate respondsToSelector:@selector(collectionView:willDisplayNodeForItemAtIndexPath:)]) { [_asyncDelegate collectionView:self willDisplayNodeForItemAtIndexPath:indexPath]; @@ -287,7 +303,7 @@ static BOOL _isInterceptedSelector(SEL sel) - (void)collectionView:(UICollectionView *)collectionView didEndDisplayingCell:(UICollectionViewCell *)cell forItemAtIndexPath:(NSIndexPath *)indexPath { - [_rangeController visibleNodeIndexPathsDidChange]; + [_rangeController visibleNodeIndexPathsDidChangeWithScrollDirection:self.scrollDirection]; if ([_asyncDelegate respondsToSelector:@selector(collectionView:didEndDisplayingNodeForItemAtIndexPath:)]) { [_asyncDelegate collectionView:self didEndDisplayingNodeForItemAtIndexPath:indexPath]; @@ -295,13 +311,48 @@ static BOOL _isInterceptedSelector(SEL sel) } +#pragma mark - ASDataControllerSource + +- (ASCellNode *)dataController:(ASDataController *)dataController nodeAtIndexPath:(NSIndexPath *)indexPath +{ + return [_asyncDataSource collectionView:self nodeForItemAtIndexPath:indexPath]; +} + +- (CGSize)dataController:(ASDataController *)dataController constrainedSizeForNodeAtIndexPath:(NSIndexPath *)indexPath +{ + CGSize restrainedSize = self.bounds.size; + + if (_layoutController.layoutDirection == ASFlowLayoutDirectionHorizontal) { + restrainedSize.width = FLT_MAX; + } else { + restrainedSize.height = FLT_MAX; + } + + return restrainedSize; +} + +- (NSUInteger)dataController:(ASDataController *)dataController rowsInSection:(NSUInteger)section +{ + return [_asyncDataSource collectionView:self numberOfItemsInSection:section]; +} + +- (NSUInteger)dataControllerNumberOfSections:(ASDataController *)dataController { + if ([_asyncDataSource respondsToSelector:@selector(numberOfSectionsInCollectionView:)]) { + return [_asyncDataSource numberOfSectionsInCollectionView:self]; + } else { + return 1; + } +} + + #pragma mark - #pragma mark ASRangeControllerDelegate. + - (NSArray *)rangeControllerVisibleNodeIndexPaths:(ASRangeController *)rangeController { ASDisplayNodeAssertMainThread(); - return [[self indexPathsForVisibleItems] sortedArrayUsingSelector:@selector(compare:)]; + return [self indexPathsForVisibleItems]; } - (CGSize)rangeControllerViewportSize:(ASRangeController *)rangeController @@ -310,55 +361,44 @@ static BOOL _isInterceptedSelector(SEL sel) return self.bounds.size; } -- (NSInteger)rangeControllerSections:(ASRangeController *)rangeController -{ - ASDisplayNodeAssertMainThread(); - if ([_asyncDataSource respondsToSelector:@selector(numberOfSectionsInCollectionView:)]) { - return [_asyncDataSource numberOfSectionsInCollectionView:self]; - } else { - return 1; - } +- (NSArray *)rangeController:(ASRangeController *)rangeController nodesAtIndexPaths:(NSArray *)indexPaths { + return [_dataController nodesAtIndexPaths:indexPaths]; } -- (NSInteger)rangeController:(ASRangeController *)rangeController rowsInSection:(NSInteger)section -{ - ASDisplayNodeAssertMainThread(); - return [_asyncDataSource collectionView:self numberOfItemsInSection:section]; -} - -- (ASCellNode *)rangeController:(ASRangeController *)rangeController nodeForIndexPath:(NSIndexPath *)indexPath -{ - ASDisplayNodeAssertNotMainThread(); - return [_asyncDataSource collectionView:self nodeForItemAtIndexPath:indexPath]; -} - -- (CGSize)rangeController:(ASRangeController *)rangeController constrainedSizeForNodeAtIndexPath:(NSIndexPath *)indexPath -{ - ASDisplayNodeAssertNotMainThread(); - CGSize contentSize = [self.collectionViewLayout collectionViewContentSize]; - CGSize viewSize = self.bounds.size; - CGFloat constrainedWidth = viewSize.width == contentSize.width ? viewSize.width : FLT_MAX; - CGFloat constrainedHeight = viewSize.height == contentSize.height ? viewSize.height : FLT_MAX; - return CGSizeMake(constrainedWidth, constrainedHeight); -} - -- (void)rangeController:(ASRangeController *)rangeController didSizeNodesWithIndexPaths:(NSArray *)indexPaths -{ +- (void)rangeController:(ASRangeController *)rangeController didInsertNodesAtIndexPaths:(NSArray *)indexPaths { ASDisplayNodeAssertMainThread(); [UIView performWithoutAnimation:^{ [self performBatchUpdates:^{ - // -insertItemsAtIndexPaths: is insufficient; UICollectionView also needs to be notified of section changes - NSInteger sectionCount = [super numberOfSections]; - NSInteger newSectionCount = [_rangeController numberOfSizedSections]; - if (newSectionCount > sectionCount) { - NSRange range = NSMakeRange(sectionCount, newSectionCount - sectionCount); - NSIndexSet *sections = [NSIndexSet indexSetWithIndexesInRange:range]; - [super insertSections:sections]; - } - [super insertItemsAtIndexPaths:indexPaths]; } completion:nil]; }]; } +- (void)rangeController:(ASRangeController *)rangeController didDeleteNodesAtIndexPaths:(NSArray *)indexPaths { + ASDisplayNodeAssertMainThread(); + [UIView performWithoutAnimation:^{ + [self performBatchUpdates:^{ + [super deleteItemsAtIndexPaths:indexPaths]; + } completion:nil]; + }]; +} + +- (void)rangeController:(ASRangeController *)rangeController didInsertSectionsAtIndexSet:(NSIndexSet *)indexSet { + ASDisplayNodeAssertMainThread(); + [UIView performWithoutAnimation:^{ + [self performBatchUpdates:^{ + [super insertSections:indexSet]; + } completion:nil]; + }]; +} + +- (void)rangeController:(ASRangeController *)rangeController didDeleteSectionsAtIndexSet:(NSIndexSet *)indexSet { + ASDisplayNodeAssertMainThread(); + [UIView performWithoutAnimation:^{ + [self performBatchUpdates:^{ + [super deleteSections:indexSet]; + } completion:nil]; + }]; +} + @end diff --git a/AsyncDisplayKit/ASDisplayNode.mm b/AsyncDisplayKit/ASDisplayNode.mm index 3880474ad5..8cd313598d 100644 --- a/AsyncDisplayKit/ASDisplayNode.mm +++ b/AsyncDisplayKit/ASDisplayNode.mm @@ -13,8 +13,8 @@ #import #import "_ASAsyncTransaction.h" -#import "_ASPendingState.h" #import "_ASDisplayView.h" +#import "_ASPendingState.h" #import "_ASScopeTimer.h" #import "ASDisplayNodeExtras.h" diff --git a/AsyncDisplayKit/ASImageNode.mm b/AsyncDisplayKit/ASImageNode.mm index 7f19f17567..5f532aadea 100644 --- a/AsyncDisplayKit/ASImageNode.mm +++ b/AsyncDisplayKit/ASImageNode.mm @@ -181,8 +181,7 @@ || alphaInfo == kCGImageAlphaPremultipliedLast; BOOL contentModeSupported = contentMode == UIViewContentModeScaleAspectFill - || contentMode == UIViewContentModeScaleAspectFit - || contentMode == UIViewContentModeCenter; + || contentMode == UIViewContentModeScaleAspectFit; CGSize backingSize; CGRect imageDrawRect; diff --git a/AsyncDisplayKit/ASTableView.h b/AsyncDisplayKit/ASTableView.h index e8815e953e..0b0d11bab7 100644 --- a/AsyncDisplayKit/ASTableView.h +++ b/AsyncDisplayKit/ASTableView.h @@ -41,37 +41,36 @@ */ - (void)reloadData; -/** - * WARNING: ASTableView's update/editing support is not yet implemented. Use of these methods will fire an assertion. - * - * This initial version of ASTableView only supports appending nodes (see below). If you'd like to see full-fledged - * support for data source updates and interactive editing, please file a GitHub issue -- AsyncDisplayKit can do it, - * we just haven't built it out yet. :] - */ -//- (void)beginUpdates; -//- (void)endUpdates; -// -//- (void)insertSections:(NSIndexSet *)sections withRowAnimation:(UITableViewRowAnimation)animation; -//- (void)deleteSections:(NSIndexSet *)sections withRowAnimation:(UITableViewRowAnimation)animation; -//- (void)reloadSections:(NSIndexSet *)sections withRowAnimation:(UITableViewRowAnimation)animation; -//- (void)moveSection:(NSInteger)section toSection:(NSInteger)newSection; -// -//- (void)insertRowsAtIndexPaths:(NSArray *)indexPaths withRowAnimation:(UITableViewRowAnimation)animation; -//- (void)deleteRowsAtIndexPaths:(NSArray *)indexPaths withRowAnimation:(UITableViewRowAnimation)animation; -//- (void)reloadRowsAtIndexPaths:(NSArray *)indexPaths withRowAnimation:(UITableViewRowAnimation)animation; -//- (void)moveRowAtIndexPath:(NSIndexPath *)indexPath toIndexPath:(NSIndexPath *)newIndexPath; -// -//- (void)setEditing:(BOOL)editing; -//- (void)setEditing:(BOOL)editing animated:(BOOL)animated; /** - * Append nodes. + * We don't support the these methods for animation yet. * - * As with UITableView, the asyncDataSource must be updated to reflect the new nodes before this method is called. - * - * @param indexPaths Ordered array of index paths corresponding to the nodes to be added. + * TODO: support animations. */ -- (void)appendNodesWithIndexPaths:(NSArray *)indexPaths; +- (void)beginUpdates; +- (void)endUpdates; + +/** + * Section updating. + * + * All operations are asynchronous and thread safe. You can call it from background thread (it is recommendated) and the UI collection + * view will be updated asynchronously. The asyncDataSource must be updated to reflect the changes before these methods are called. + */ +- (void)insertSections:(NSIndexSet *)sections withRowAnimation:(UITableViewRowAnimation)animation; +- (void)deleteSections:(NSIndexSet *)sections withRowAnimation:(UITableViewRowAnimation)animation; +- (void)reloadSections:(NSIndexSet *)sections withRowAnimation:(UITableViewRowAnimation)animation; +- (void)moveSection:(NSInteger)section toSection:(NSInteger)newSection; + +/** + * Row updating. + * + * All operations are asynchronous and thread safe. You can call it from background thread (it is recommendated) and the UI collection + * view will be updated asynchronously. The asyncDataSource must be updated to reflect the changes before these methods are called. + */ +- (void)insertRowsAtIndexPaths:(NSArray *)indexPaths withRowAnimation:(UITableViewRowAnimation)animation; +- (void)deleteRowsAtIndexPaths:(NSArray *)indexPaths withRowAnimation:(UITableViewRowAnimation)animation; +- (void)reloadRowsAtIndexPaths:(NSArray *)indexPaths withRowAnimation:(UITableViewRowAnimation)animation; +- (void)moveRowAtIndexPath:(NSIndexPath *)indexPath toIndexPath:(NSIndexPath *)newIndexPath; @end diff --git a/AsyncDisplayKit/ASTableView.m b/AsyncDisplayKit/ASTableView.m index 22932faef7..919b43a2bb 100644 --- a/AsyncDisplayKit/ASTableView.m +++ b/AsyncDisplayKit/ASTableView.m @@ -9,6 +9,9 @@ #import "ASTableView.h" #import "ASAssert.h" +#import "ASDataController.h" +#import "ASFlowLayoutController.h" +#import "ASLayoutController.h" #import "ASRangeController.h" @@ -98,10 +101,13 @@ static BOOL _isInterceptedSelector(SEL sel) #pragma mark - #pragma mark ASTableView. -@interface ASTableView () { +@interface ASTableView () { _ASTableViewProxy *_proxyDataSource; _ASTableViewProxy *_proxyDelegate; + ASDataController *_dataController; + ASFlowLayoutController *_layoutController; + ASRangeController *_rangeController; } @@ -117,22 +123,22 @@ static BOOL _isInterceptedSelector(SEL sel) if (!(self = [super initWithFrame:frame style:style])) return nil; + _layoutController = [[ASFlowLayoutController alloc] initWithScrollOption:ASFlowLayoutDirectionVertical]; + _rangeController = [[ASRangeController alloc] init]; + _rangeController.layoutController = _layoutController; _rangeController.delegate = self; + _dataController = [[ASDataController alloc] init]; + _dataController.dataSource = self; + _dataController.delegate = _rangeController; + return self; } - #pragma mark - #pragma mark Overrides. -- (void)reloadData -{ - [_rangeController rebuildData]; - [super reloadData]; -} - - (void)setDataSource:(id)dataSource { ASDisplayNodeAssert(NO, @"ASTableView uses asyncDataSource, not UITableView's dataSource property."); @@ -176,19 +182,22 @@ static BOOL _isInterceptedSelector(SEL sel) } } +- (void)reloadData +{ + [_dataController reloadData]; + + // It is required + [super reloadData]; +} + - (ASRangeTuningParameters)rangeTuningParameters { - return _rangeController.tuningParameters; + return _layoutController.tuningParameters; } - (void)setRangeTuningParameters:(ASRangeTuningParameters)tuningParameters { - _rangeController.tuningParameters = tuningParameters; -} - -- (void)appendNodesWithIndexPaths:(NSArray *)indexPaths -{ - [_rangeController appendNodesWithIndexPaths:indexPaths]; + _layoutController.tuningParameters = tuningParameters; } #pragma mark Assertions. @@ -196,7 +205,7 @@ static BOOL _isInterceptedSelector(SEL sel) - (void)throwUnimplementedException { [[NSException exceptionWithName:@"UnimplementedException" - reason:@"ASTableView's update/editing support is not yet implemented. Please see ASTableView.h." + reason:@"ASTableView's grouped updates aren't currently supported yet, please call the insert/delete function directly." userInfo:nil] raise]; } @@ -212,55 +221,44 @@ static BOOL _isInterceptedSelector(SEL sel) - (void)insertSections:(NSIndexSet *)sections withRowAnimation:(UITableViewRowAnimation)animation { - [self throwUnimplementedException]; + [_dataController insertSections:sections]; } - (void)deleteSections:(NSIndexSet *)sections withRowAnimation:(UITableViewRowAnimation)animation { - [self throwUnimplementedException]; + [_dataController deleteSections:sections]; } - (void)reloadSections:(NSIndexSet *)sections withRowAnimation:(UITableViewRowAnimation)animation { - [self throwUnimplementedException]; + [_dataController reloadSections:sections]; } - (void)moveSection:(NSInteger)section toSection:(NSInteger)newSection { - [self throwUnimplementedException]; + [_dataController moveSection:section toSection:newSection]; } - (void)insertRowsAtIndexPaths:(NSArray *)indexPaths withRowAnimation:(UITableViewRowAnimation)animation { - [self throwUnimplementedException]; + [_dataController insertRowsAtIndexPaths:indexPaths]; } - (void)deleteRowsAtIndexPaths:(NSArray *)indexPaths withRowAnimation:(UITableViewRowAnimation)animation { - [self throwUnimplementedException]; + [_dataController deleteRowsAtIndexPaths:indexPaths]; } - (void)reloadRowsAtIndexPaths:(NSArray *)indexPaths withRowAnimation:(UITableViewRowAnimation)animation { - [self throwUnimplementedException]; + [_dataController reloadRowsAtIndexPaths:indexPaths]; } - (void)moveRowAtIndexPath:(NSIndexPath *)indexPath toIndexPath:(NSIndexPath *)newIndexPath { - [self throwUnimplementedException]; + [_dataController moveRowAtIndexPath:indexPath toIndexPath:newIndexPath]; } -- (void)setEditing:(BOOL)editing -{ - [self throwUnimplementedException]; -} - -- (void)setEditing:(BOOL)editing animated:(BOOL)animated -{ - [self throwUnimplementedException]; -} - - #pragma mark - #pragma mark Intercepted selectors. @@ -273,29 +271,56 @@ static BOOL _isInterceptedSelector(SEL sel) cell = [[_ASTableViewCell alloc] initWithStyle:UITableViewCellStyleDefault reuseIdentifier:reuseIdentifier]; } - [_rangeController configureTableViewCell:cell forIndexPath:indexPath]; + ASCellNode *node = [_dataController nodeAtIndexPath:indexPath]; + [_rangeController configureContentView:cell.contentView forCellNode:node]; + + cell.backgroundColor = node.backgroundColor; + cell.selectionStyle = node.selectionStyle; return cell; } - (CGFloat)tableView:(UITableView *)tableView heightForRowAtIndexPath:(NSIndexPath *)indexPath { - return [_rangeController calculatedSizeForNodeAtIndexPath:indexPath].height; + ASCellNode *node = [_dataController nodeAtIndexPath:indexPath]; + return node.calculatedSize.height; } - (NSInteger)numberOfSectionsInTableView:(UITableView *)tableView { - return [_rangeController numberOfSizedSections]; + return [_dataController numberOfSections]; } - (NSInteger)tableView:(UITableView *)tableView numberOfRowsInSection:(NSInteger)section { - return [_rangeController numberOfSizedRowsInSection:section]; + return [_dataController numberOfRowsInSection:section]; +} + + +- (ASScrollDirection)scrollDirection +{ + CGPoint scrollVelocity = [self.panGestureRecognizer velocityInView:self.superview]; + ASScrollDirection direction = ASScrollDirectionNone; + if (_layoutController.layoutDirection == ASFlowLayoutDirectionHorizontal) { + if (scrollVelocity.x > 0) { + direction = ASScrollDirectionRight; + } else if (scrollVelocity.x < 0) { + direction = ASScrollDirectionLeft; + } + } else { + if (scrollVelocity.y > 0) { + direction = ASScrollDirectionDown; + } else { + direction = ASScrollDirectionUp; + } + } + + return direction; } - (void)tableView:(UITableView *)tableView willDisplayCell:(UITableViewCell *)cell forRowAtIndexPath:(NSIndexPath *)indexPath { - [_rangeController visibleNodeIndexPathsDidChange]; + [_rangeController visibleNodeIndexPathsDidChangeWithScrollDirection:self.scrollDirection]; if ([_asyncDelegate respondsToSelector:@selector(tableView:willDisplayNodeForRowAtIndexPath:)]) { [_asyncDelegate tableView:self willDisplayNodeForRowAtIndexPath:indexPath]; @@ -304,7 +329,7 @@ static BOOL _isInterceptedSelector(SEL sel) - (void)tableView:(UITableView *)tableView didEndDisplayingCell:(UITableViewCell *)cell forRowAtIndexPath:(NSIndexPath*)indexPath { - [_rangeController visibleNodeIndexPathsDidChange]; + [_rangeController visibleNodeIndexPathsDidChangeWithScrollDirection:self.scrollDirection]; if ([_asyncDelegate respondsToSelector:@selector(tableView:didEndDisplayingNodeForRowAtIndexPath:)]) { [_asyncDelegate tableView:self didEndDisplayingNodeForRowAtIndexPath:indexPath]; @@ -321,60 +346,76 @@ static BOOL _isInterceptedSelector(SEL sel) return [self indexPathsForVisibleRows]; } +- (NSArray *)rangeController:(ASRangeController *)rangeController nodesAtIndexPaths:(NSArray *)indexPaths { + return [_dataController nodesAtIndexPaths:indexPaths]; +} + - (CGSize)rangeControllerViewportSize:(ASRangeController *)rangeController { ASDisplayNodeAssertMainThread(); return self.bounds.size; } -- (NSInteger)rangeControllerSections:(ASRangeController *)rangeController -{ +- (void)rangeController:(ASRangeController *)rangeController didInsertNodesAtIndexPaths:(NSArray *)indexPaths { ASDisplayNodeAssertMainThread(); - if ([_asyncDataSource respondsToSelector:@selector(numberOfSectionsInTableView:)]) { - return [_asyncDataSource numberOfSectionsInTableView:self]; - } else { - return 1; - } -} -- (NSInteger)rangeController:(ASRangeController *)rangeController rowsInSection:(NSInteger)section -{ - ASDisplayNodeAssertMainThread(); - return [_asyncDataSource tableView:self numberOfRowsInSection:section]; -} - -- (ASCellNode *)rangeController:(ASRangeController *)rangeController nodeForIndexPath:(NSIndexPath *)indexPath -{ - ASDisplayNodeAssertNotMainThread(); - return [_asyncDataSource tableView:self nodeForRowAtIndexPath:indexPath]; -} - -- (CGSize)rangeController:(ASRangeController *)rangeController constrainedSizeForNodeAtIndexPath:(NSIndexPath *)indexPath -{ - ASDisplayNodeAssertNotMainThread(); - return CGSizeMake(self.bounds.size.width, FLT_MAX); -} - -- (void)rangeController:(ASRangeController *)rangeController didSizeNodesWithIndexPaths:(NSArray *)indexPaths -{ - ASDisplayNodeAssertMainThread(); [UIView performWithoutAnimation:^{ [super beginUpdates]; - - // -insertRowsAtIndexPaths:: is insufficient; UITableView also needs to be notified of section changes - NSInteger sectionCount = [super numberOfSections]; - NSInteger newSectionCount = [_rangeController numberOfSizedSections]; - if (newSectionCount > sectionCount) { - NSRange range = NSMakeRange(sectionCount, newSectionCount - sectionCount); - NSIndexSet *sections = [NSIndexSet indexSetWithIndexesInRange:range]; - [super insertSections:sections withRowAnimation:UITableViewRowAnimationNone]; - } - [super insertRowsAtIndexPaths:indexPaths withRowAnimation:UITableViewRowAnimationNone]; - [super endUpdates]; }]; } +- (void)rangeController:(ASRangeController *)rangeController didDeleteNodesAtIndexPaths:(NSArray *)indexPaths { + ASDisplayNodeAssertMainThread(); + + [UIView performWithoutAnimation:^{ + [super beginUpdates]; + [super deleteRowsAtIndexPaths:indexPaths withRowAnimation:UITableViewRowAnimationNone]; + [super endUpdates]; + }]; +} + +- (void)rangeController:(ASRangeController *)rangeController didInsertSectionsAtIndexSet:(NSIndexSet *)indexSet { + ASDisplayNodeAssertMainThread(); + + [UIView performWithoutAnimation:^{ + [super beginUpdates]; + [super insertSections:indexSet withRowAnimation:UITableViewRowAnimationNone]; + [super endUpdates]; + }]; +} + +- (void)rangeController:(ASRangeController *)rangeController didDeleteSectionsAtIndexSet:(NSIndexSet *)indexSet { + ASDisplayNodeAssertMainThread(); + + [UIView performWithoutAnimation:^{ + [super beginUpdates]; + [super deleteSections:indexSet withRowAnimation:UITableViewRowAnimationNone]; + [super endUpdates]; + }]; +} + +#pragma mark - ASDataControllerDelegate + +- (ASCellNode *)dataController:(ASDataController *)dataController nodeAtIndexPath:(NSIndexPath *)indexPath { + return [_asyncDataSource tableView:self nodeForRowAtIndexPath:indexPath]; +} + +- (CGSize)dataController:(ASDataController *)dataController constrainedSizeForNodeAtIndexPath:(NSIndexPath *)indexPath { + return CGSizeMake(self.bounds.size.width, FLT_MAX); +} + +- (NSUInteger)dataController:(ASDataController *)dataControllre rowsInSection:(NSUInteger)section { + return [_asyncDataSource tableView:self numberOfRowsInSection:section]; +} + +- (NSUInteger)dataControllerNumberOfSections:(ASDataController *)dataController { + if ([_asyncDataSource respondsToSelector:@selector(numberOfSectionsInTableView:)]) { + return [_asyncDataSource numberOfSectionsInTableView:self]; + } else { + return 1; // default section number + } +} @end diff --git a/AsyncDisplayKit/ASTextNode.mm b/AsyncDisplayKit/ASTextNode.mm index c017eda023..74574dd7b1 100644 --- a/AsyncDisplayKit/ASTextNode.mm +++ b/AsyncDisplayKit/ASTextNode.mm @@ -867,8 +867,6 @@ ASDISPLAYNODE_INLINE CGFloat ceilPixelValue(CGFloat f) return [[self _renderer] lineCount]; } -#pragma mark - Truncation Message - - (void)_invalidateTruncationString { _composedTruncationString = [self _prepareTruncationStringForDrawing:[self _composedTruncationString]]; diff --git a/AsyncDisplayKit/Details/ASDataController.h b/AsyncDisplayKit/Details/ASDataController.h new file mode 100644 index 0000000000..599060ae1b --- /dev/null +++ b/AsyncDisplayKit/Details/ASDataController.h @@ -0,0 +1,125 @@ +// Copyright 2004-present Facebook. All Rights Reserved. + +#import +#import + +@class ASCellNode; +@class ASDataController; + +/** + Data source for data controller + It will be invoked in the same thread as the api call of ASDataController. + */ +@protocol ASDataControllerSource + +/** + Fetch the ASCellNode for specific index path. + */ +- (ASCellNode *)dataController:(ASDataController *)dataController nodeAtIndexPath:(NSIndexPath *)indexPath; + +/** + The constrained size for layout. + */ +- (CGSize)dataController:(ASDataController *)dataController constrainedSizeForNodeAtIndexPath:(NSIndexPath *)indexPath; + +/** + Fetch the number of rows in specific section. + */ +- (NSUInteger)dataController:(ASDataController *)dataControllre rowsInSection:(NSUInteger)section; + +/** + Fetch the number of sections. + */ +- (NSUInteger)dataControllerNumberOfSections:(ASDataController *)dataController; + +@end + +/** + Delegate for notify the data updating of data controller. + These methods will be invoked from main thread right now, but it may be moved to background thread in the future. + */ +@protocol ASDataControllerDelegate + +@optional + +/** + Called for insertion of elements. + */ +- (void)dataController:(ASDataController *)dataController willInsertNodes:(NSArray *)nodes atIndexPaths:(NSArray *)indexPaths; +- (void)dataController:(ASDataController *)dataController didInsertNodes:(NSArray *)nodes atIndexPaths:(NSArray *)indexPaths; + +/** + Called for deletion of elements. + */ +- (void)dataController:(ASDataController *)dataController willDeleteNodesAtIndexPaths:(NSArray *)indexPaths; +- (void)dataController:(ASDataController *)dataController didDeleteNodesAtIndexPaths:(NSArray *)indexPaths; + +/** + Called for insertion of sections. + */ +- (void)dataController:(ASDataController *)dataController willInsertSectionsAtIndexSet:(NSIndexSet *)indexSet; +- (void)dataController:(ASDataController *)dataController didInsertSectionsAtIndexSet:(NSIndexSet *)indexSet; + +/** + Called for deletion of sections. + */ +- (void)dataController:(ASDataController *)dataController willDeleteSectionsAtIndexSet:(NSIndexSet *)indexSet; +- (void)dataController:(ASDataController *)dataController didDeleteSectionsAtIndexSet:(NSIndexSet *)indexSet; + +@end + + +/** + * Controller to layout data in background, and managed data updating. + * + * All operations are asynchronous and thread safe. You can call it from background thread (it is recommendated) and the data + * will be updated asynchronously. The dataSource must be updated to reflect the changes before these methods has been called. + * For each data updatin, the corresponding methods in delegate will be called. + */ +@interface ASDataController : ASDealloc2MainObject + +/** + Data source for fetching data info. + */ +@property (nonatomic, weak) id dataSource; + +/** + Delegate to notify when data is updated. + */ +@property (nonatomic, weak) id delegate; + +/** @name Initial loading */ + +- (void)initialDataLoading; + +/** @name Data Updating */ + +- (void)insertSections:(NSIndexSet *)sections; + +- (void)deleteSections:(NSIndexSet *)sections; + +- (void)reloadSections:(NSIndexSet *)sections; + +- (void)moveSection:(NSInteger)section toSection:(NSInteger)newSection; + +- (void)insertRowsAtIndexPaths:(NSArray *)indexPaths; + +- (void)deleteRowsAtIndexPaths:(NSArray *)indexPaths; + +- (void)reloadRowsAtIndexPaths:(NSArray *)indexPaths; + +- (void)moveRowAtIndexPath:(NSIndexPath *)indexPath toIndexPath:(NSIndexPath *)newIndexPath; + +- (void)reloadData; + +/** @name Data Querying */ + +- (NSUInteger)numberOfSections; + +- (NSUInteger)numberOfRowsInSection:(NSUInteger)section; + +- (ASCellNode *)nodeAtIndexPath:(NSIndexPath *)indexPath; + +- (NSArray *)nodesAtIndexPaths:(NSArray *)indexPaths; + +@end diff --git a/AsyncDisplayKit/Details/ASDataController.mm b/AsyncDisplayKit/Details/ASDataController.mm new file mode 100644 index 0000000000..24356b1b0e --- /dev/null +++ b/AsyncDisplayKit/Details/ASDataController.mm @@ -0,0 +1,474 @@ +// Copyright 2004-present Facebook. All Rights Reserved. + +#import "ASDataController.h" + +#import + +#import "ASAssert.h" +#import "ASCellNode.h" +#import "ASDisplayNode.h" +#import "ASMultidimensionalArrayUtils.h" + +#define INSERT_NODES(multidimensionalArray, indexPath, elements) \ +{ \ + if ([_delegate respondsToSelector:@selector(dataController:willInsertNodes:atIndexPaths:)]) { \ + [_delegate dataController:self willInsertNodes:elements atIndexPaths:indexPath]; \ + } \ + ASInsertElementsIntoMultidimensionalArrayAtIndexPaths(multidimensionalArray, indexPath, elements); \ + if ([_delegate respondsToSelector:@selector(dataController:didInsertNodes:atIndexPaths:)]) { \ + [_delegate dataController:self didInsertNodes:elements atIndexPaths:indexPath]; \ + } \ +} + +#define DELETE_NODES(multidimensionalArray, indexPath) \ +{ \ + if ([_delegate respondsToSelector:@selector(dataController:willDeleteNodesAtIndexPaths:)]) { \ + [_delegate dataController:self willDeleteNodesAtIndexPaths:indexPath]; \ + } \ + ASDeleteElementsInMultidimensionalArrayAtIndexPaths(multidimensionalArray, indexPath); \ + if ([_delegate respondsToSelector:@selector(dataController:didDeleteNodesAtIndexPaths:)]) { \ + [_delegate dataController:self didDeleteNodesAtIndexPaths:indexPath]; \ + } \ +} + +#define INSERT_SECTIONS(multidimensionalArray, indexSet, sections) \ +{ \ + if ([_delegate respondsToSelector:@selector(dataController:willInsertSectionsAtIndexSet:)]) { \ + [_delegate dataController:self willInsertSectionsAtIndexSet:indexSet]; \ + } \ + [_nodes insertObjects:sections atIndexes:indexSet]; \ + if ([_delegate respondsToSelector:@selector(dataController:didInsertSectionsAtIndexSet:)]) { \ + [_delegate dataController:self didInsertSectionsAtIndexSet:indexSet]; \ + } \ +} + +#define DELETE_SECTIONS(multidimensionalArray, indexSet) \ +{ \ + if ([_delegate respondsToSelector:@selector(dataController:willDeleteSectionsAtIndexSet:)]) { \ + [_delegate dataController:self willDeleteSectionsAtIndexSet:indexSet]; \ + } \ + [_nodes removeObjectsAtIndexes:indexSet]; \ + if ([_delegate respondsToSelector:@selector(dataController:willDeleteSectionsAtIndexSet:)]) { \ + [_delegate dataController:self didDeleteSectionsAtIndexSet:indexSet]; \ + } \ +} + +// +// The background update is not fully supported yet, although it is trivial to fix it. The underline +// problem is we need to do the profiling between the main thread updating and background updating, +// and then decided which way to go. +// +// For background update, we could avoid the multi-dimensinonal array operation (insertion / deletion) +// on main thread. However, the sideback is we need to dispatch_sync to lock main thread for data query, +// although it is running on a concurrent queue and should be fast enough. +// +// For main thread update, we need to do the multi-dimensional operations (insertion / deletion) on +// main thread, but we will gain the performance in data query. Considering data query is much more +// frequent than data updating, so we keep it on main thread for the initial version. +// +// +#define ENABLE_BACKGROUND_UPDATE 0 + +const static NSUInteger kASDataControllerSizingCountPerProcessor = 5; + +static void *kASSizingQueueContext = &kASSizingQueueContext; +static void *kASDataUpdatingQueueContext = &kASDataUpdatingQueueContext; + +@interface ASDataController () { + NSMutableArray *_nodes; +} + +@end + +@implementation ASDataController + +- (instancetype)init { + if (self = [super init]) { + _nodes = [NSMutableArray array]; + } + + return self; +} + +#pragma mark - Utils + ++ (NSUInteger)parallelProcessorCount { + static NSUInteger parallelProcessorCount; + + static dispatch_once_t onceToken; + dispatch_once(&onceToken, ^{ + parallelProcessorCount = [[NSProcessInfo processInfo] processorCount]; + }); + + return parallelProcessorCount; +} + ++ (dispatch_queue_t)sizingQueue +{ + static dispatch_queue_t sizingQueue = NULL; + static dispatch_once_t onceToken; + dispatch_once(&onceToken, ^{ + sizingQueue = dispatch_queue_create("com.facebook.AsyncDisplayKit.ASDataController.sizingQueue", DISPATCH_QUEUE_SERIAL); + dispatch_queue_set_specific(sizingQueue, kASSizingQueueContext, kASSizingQueueContext, NULL); + dispatch_set_target_queue(sizingQueue, dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0)); + }); + + return sizingQueue; +} + ++ (BOOL)isSizingQueue { + return kASSizingQueueContext == dispatch_get_specific(kASSizingQueueContext); +} + +/** + * Concurrent queue for query / updating the cached data. + * The data query is more frequent than the data updating, so we use dispatch_sync for reading, and dispatch_barrier_async for writing. + */ ++ (dispatch_queue_t)dataUpdatingQueue +{ + static dispatch_queue_t dataUpdatingQueue = NULL; + static dispatch_once_t onceToken; + dispatch_once(&onceToken, ^{ + dataUpdatingQueue = dispatch_queue_create("com.facebook.AsyncDisplayKit.ASDataController.dataUpdatingQueue", DISPATCH_QUEUE_CONCURRENT); + dispatch_queue_set_specific(dataUpdatingQueue, kASDataUpdatingQueueContext, kASDataUpdatingQueueContext, NULL); + }); + + return dataUpdatingQueue; +} + ++ (BOOL)isDataUpdatingQueue { + return kASDataUpdatingQueueContext == dispatch_get_specific(kASDataUpdatingQueueContext); +} + +- (void)asyncUpdateDataWithBlock:(dispatch_block_t)block { +#if ENABLE_BACKGROUND_UPDATE + dispatch_barrier_async([ASDataController dataUpdatingQueue], ^{ + block(); + }); +#else + dispatch_async(dispatch_get_main_queue(), ^{ + block(); + }); +#endif +} + +- (void)syncUpdateDataWithBlock:(dispatch_block_t)block { +#if ENABLE_BACKGROUND_UPDATE + dispatch_barrier_sync([ASDataController dataUpdatingQueue], ^{ + block(); + }); +#else + dispatch_sync(dispatch_get_main_queue(), ^{ + block(); + }); +#endif +} + +- (void)queryDataWithBlock:(dispatch_block_t)block { +#if ENABLE_BACKGROUND_UPDATE + if ([ASDataController isDataUpdatingQueue]) { + block(); + } else { + dispatch_sync([ASDataController dataUpdatingQueue], ^{ + block(); + }); + } +#else + ASDisplayNodeAssertMainThread(); + block(); +#endif +} + +#pragma mark - Initial Data Loading + +- (void)initialDataLoading { + NSMutableArray *indexPaths = [NSMutableArray array]; + + NSUInteger sectionNum = [_dataSource dataControllerNumberOfSections:self]; + + // insert sections + [self insertSections:[NSIndexSet indexSetWithIndexesInRange:NSMakeRange(0, sectionNum)]]; + + for (NSUInteger i = 0; i < sectionNum; i++) { + NSIndexPath *indexPath = [[NSIndexPath alloc] initWithIndex:i]; + + NSUInteger rowNum = [_dataSource dataController:self rowsInSection:i]; + for (NSUInteger j = 0; j < rowNum; j++) { + [indexPaths addObject:[indexPath indexPathByAddingIndex:j]]; + } + } + + // insert elements + [self insertRowsAtIndexPaths:indexPaths]; +} + +#pragma mark - Data Update + +- (void)insertSections:(NSIndexSet *)indexSet { + NSMutableArray *sectionArray = [[NSMutableArray alloc] initWithCapacity:indexSet.count]; + for (int i = 0; i < indexSet.count; i++) { + [sectionArray addObject:[[NSMutableArray alloc] init]]; + } + + dispatch_async([[self class] sizingQueue], ^{ + [self asyncUpdateDataWithBlock:^{ + INSERT_SECTIONS(_nodes , indexSet, sectionArray); + }]; + }); +} + +- (void)deleteSections:(NSIndexSet *)indexSet { + dispatch_async([[self class] sizingQueue], ^{ + [self asyncUpdateDataWithBlock:^{ + // remove elements + NSArray *indexPaths = ASIndexPathsForMultidimensionalArrayAtIndexSet(_nodes, indexSet); + + DELETE_NODES(_nodes, indexPaths); + DELETE_SECTIONS(_nodes, indexSet); + }]; + }); +} + +- (void)reloadSections:(NSIndexSet *)sections { + // We need to keep data query on data source in the calling thread. + NSMutableArray *updatedIndexPaths = [[NSMutableArray alloc] init]; + NSMutableArray *updatedNodes = [[NSMutableArray alloc] init]; + + [sections enumerateIndexesUsingBlock:^(NSUInteger idx, BOOL *stop) { + NSUInteger rowNum = [_dataSource dataController:self rowsInSection:sections.count]; + + NSIndexPath *sectionIndex = [[NSIndexPath alloc] initWithIndex:idx]; + for (NSUInteger i = 0; i < rowNum; i++) { + NSIndexPath *indexPath = [sectionIndex indexPathByAddingIndex:i]; + [updatedIndexPaths addObject:indexPath]; + [updatedNodes addObject:[_dataSource dataController:self nodeAtIndexPath:indexPath]]; + } + }]; + + dispatch_async([ASDataController sizingQueue], ^{ + [self syncUpdateDataWithBlock:^{ + // remove elements + NSArray *indexPaths = ASIndexPathsForMultidimensionalArrayAtIndexSet(_nodes, sections); + DELETE_NODES(_nodes, indexPaths); + }]; + + // reinsert the elements + [self _insertNodes:updatedNodes atIndexPaths:updatedIndexPaths]; + }); +} + +- (void)moveSection:(NSInteger)section toSection:(NSInteger)newSection { + dispatch_async([ASDataController sizingQueue], ^{ + [self asyncUpdateDataWithBlock:^{ + // remove elements + NSArray *indexPaths = ASIndexPathsForMultidimensionalArrayAtIndexSet(_nodes, [NSIndexSet indexSetWithIndex:section]); + NSArray *nodes = ASFindElementsInMultidimensionalArrayAtIndexPaths(_nodes, indexPaths); + DELETE_NODES(_nodes, indexPaths); + + // update the section of indexpaths + NSIndexPath *sectionIndexPath = [[NSIndexPath alloc] initWithIndex:newSection]; + NSMutableArray *updatedIndexPaths = [[NSMutableArray alloc] initWithCapacity:indexPaths.count]; + [indexPaths enumerateObjectsUsingBlock:^(NSIndexPath *indexPath, NSUInteger idx, BOOL *stop) { + [updatedIndexPaths addObject:[sectionIndexPath indexPathByAddingIndex:[indexPath indexAtPosition:indexPath.length - 1]]]; + }]; + + // Don't re-calculate size for moving + INSERT_NODES(_nodes, updatedIndexPaths, nodes); + }]; + }); +} + +- (void)_insertNodes:(NSArray *)nodes + atIndexPaths:(NSArray *)indexPaths { + if (!nodes.count) { + return; + } + + dispatch_group_t layoutGroup = dispatch_group_create(); + + for (NSUInteger j = 0; j < nodes.count && j < indexPaths.count; j += kASDataControllerSizingCountPerProcessor) { + NSArray *subIndexPaths = [indexPaths subarrayWithRange:NSMakeRange(j, MIN(kASDataControllerSizingCountPerProcessor, indexPaths.count - j))]; + + NSMutableArray *nodeBoundSizes = [[NSMutableArray alloc] initWithCapacity:kASDataControllerSizingCountPerProcessor]; + [subIndexPaths enumerateObjectsUsingBlock:^(NSIndexPath *indexPath, NSUInteger idx, BOOL *stop) { + [nodeBoundSizes addObject:[NSValue valueWithCGSize:[_dataSource dataController:self constrainedSizeForNodeAtIndexPath:indexPath]]]; + }]; + + dispatch_group_async(layoutGroup, dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0), ^{ + [subIndexPaths enumerateObjectsUsingBlock:^(NSIndexPath *indexPath, NSUInteger idx, BOOL *stop) { + ASCellNode *node = nodes[j + idx]; + [node measure:[nodeBoundSizes[idx] CGSizeValue]]; + node.frame = CGRectMake(0.0f, 0.0f, node.calculatedSize.width, node.calculatedSize.height); + }]; + }); + } + + dispatch_block_t block = ^{ + dispatch_group_wait(layoutGroup, DISPATCH_TIME_FOREVER); + + [self asyncUpdateDataWithBlock:^{ + // updating the cells + INSERT_NODES(_nodes, indexPaths, nodes); + }]; + }; + + if ([ASDataController isSizingQueue]) { + block(); + } else { + dispatch_async([ASDataController sizingQueue], block); + } +} + +- (void)insertRowsAtIndexPaths:(NSArray *)indexPaths { + NSUInteger blockSize = [[ASDataController class] parallelProcessorCount] * kASDataControllerSizingCountPerProcessor; + + // sort indexPath to avoid messing up the index when inserting in several batches + NSArray *sortedIndexPaths = [indexPaths sortedArrayUsingSelector:@selector(compare:)]; + + // Processing in batches + for (NSUInteger i = 0; i < sortedIndexPaths.count; i += blockSize) { + NSArray *batchedIndexPaths = [sortedIndexPaths subarrayWithRange:NSMakeRange(i, MIN(sortedIndexPaths.count - i, blockSize))]; + NSMutableArray *nodes = [NSMutableArray arrayWithCapacity:batchedIndexPaths.count]; + + [batchedIndexPaths enumerateObjectsUsingBlock:^(NSIndexPath *indexPath, NSUInteger idx, BOOL *stop) { + [nodes addObject:[_dataSource dataController:self nodeAtIndexPath:indexPath]]; + }]; + + [self _insertNodes:nodes atIndexPaths:batchedIndexPaths]; + } +} + +- (void)deleteRowsAtIndexPaths:(NSArray *)indexPaths { + // sort indexPath in revse order to avoid messing up the index when deleting + NSArray *sortedIndexPaths = [indexPaths sortedArrayUsingComparator:^NSComparisonResult(NSIndexPath *obj1, NSIndexPath *obj2) { + return [obj2 compare:obj1]; + }]; + + dispatch_async([ASDataController sizingQueue], ^{ + [self asyncUpdateDataWithBlock:^{ + DELETE_NODES(_nodes, sortedIndexPaths); + }]; + }); +} + +- (void)reloadRowsAtIndexPaths:(NSArray *)indexPaths { + // The reloading operation required reloading the data + // Loading data in the calling thread + NSMutableArray *nodes = [[NSMutableArray alloc] initWithCapacity:indexPaths.count]; + [indexPaths enumerateObjectsUsingBlock:^(NSIndexPath *indexPath, NSUInteger idx, BOOL *stop) { + [nodes addObject:[_dataSource dataController:self nodeAtIndexPath:indexPath]]; + }]; + + dispatch_async([ASDataController sizingQueue], ^{ + [self syncUpdateDataWithBlock:^{ + DELETE_NODES(_nodes, nodes); + }]; + + [self _insertNodes:nodes atIndexPaths:indexPaths]; + }); +} + +- (void)moveRowAtIndexPath:(NSIndexPath *)indexPath toIndexPath:(NSIndexPath *)newIndexPath { + dispatch_async([ASDataController sizingQueue], ^{ + [self asyncUpdateDataWithBlock:^{ + NSArray *nodes = ASFindElementsInMultidimensionalArrayAtIndexPaths(_nodes, [NSArray arrayWithObject:indexPath]); + NSArray *indexPaths = [NSArray arrayWithObject:indexPath]; + DELETE_NODES(_nodes, indexPaths); + + // Don't re-calculate size for moving + INSERT_NODES(_nodes, indexPaths, nodes); + }]; + }); +} + +- (void)reloadData { + // Fetching data in calling thread + NSMutableArray *updatedNodes = [[NSMutableArray alloc] init]; + NSMutableArray *updatedIndexPaths = [[NSMutableArray alloc] init]; + + NSUInteger sectionNum = [_dataSource dataControllerNumberOfSections:self]; + for (NSUInteger i = 0; i < sectionNum; i++) { + NSIndexPath *sectionIndexPath = [[NSIndexPath alloc] initWithIndex:i]; + + NSUInteger rowNum = [_dataSource dataController:self rowsInSection:i]; + for (NSUInteger j = 0; j < rowNum; j++) { + NSIndexPath *indexPath = [sectionIndexPath indexPathByAddingIndex:j]; + [updatedIndexPaths addObject:indexPath]; + [updatedNodes addObject:[_dataSource dataController:self nodeAtIndexPath:indexPath]]; + } + } + + dispatch_async([ASDataController sizingQueue], ^{ + [self syncUpdateDataWithBlock:^{ + + NSArray *indexPaths = ASIndexPathsForMultidimensionalArray(_nodes); + + // sort indexPath in reverse order to avoid messing up the index when inserting in several batches + NSArray *sortedIndexPaths = [indexPaths sortedArrayUsingComparator:^NSComparisonResult(NSIndexPath *obj1, NSIndexPath *obj2) { + return [obj2 compare:obj1]; + }]; + + DELETE_NODES(_nodes, sortedIndexPaths); + + NSMutableIndexSet *indexSet = [[NSMutableIndexSet alloc] initWithIndexesInRange:NSMakeRange(0, _nodes.count)]; + DELETE_SECTIONS(_nodes, indexSet); + + + // Insert section + + NSMutableArray *sections = [[NSMutableArray alloc] initWithCapacity:sectionNum]; + for (int i = 0; i < sectionNum; i++) { + [sections addObject:[[NSMutableArray alloc] init]]; + } + + INSERT_SECTIONS(_nodes, [[NSMutableIndexSet alloc] initWithIndexesInRange:NSMakeRange(0, sectionNum)], sections); + + }]; + + [self _insertNodes:updatedNodes atIndexPaths:updatedIndexPaths]; + }); +} + +#pragma mark - Data Querying + +- (NSUInteger)numberOfSections { + __block NSUInteger sectionNum; + + [self queryDataWithBlock:^{ + sectionNum = [_nodes count]; + }]; + + return sectionNum; +} + +- (NSUInteger)numberOfRowsInSection:(NSUInteger)section { + __block NSUInteger rowNum; + + [self queryDataWithBlock:^{ + rowNum = [_nodes[section] count]; + }]; + + return rowNum; +} + +- (ASCellNode *)nodeAtIndexPath:(NSIndexPath *)indexPath { + __block ASCellNode *node; + + [self queryDataWithBlock:^{ + node = _nodes[indexPath.section][indexPath.row]; + }]; + + return node; +} + +- (NSArray *)nodesAtIndexPaths:(NSArray *)indexPaths { + __block NSArray *arr = nil; + + [self queryDataWithBlock:^{ + arr = ASFindElementsInMultidimensionalArrayAtIndexPaths(_nodes, indexPaths); + }]; + + return arr; +} + +@end diff --git a/AsyncDisplayKit/Details/ASFlowLayoutController.h b/AsyncDisplayKit/Details/ASFlowLayoutController.h new file mode 100644 index 0000000000..45fefaa6ad --- /dev/null +++ b/AsyncDisplayKit/Details/ASFlowLayoutController.h @@ -0,0 +1,21 @@ +// Copyright 2004-present Facebook. All Rights Reserved. + +#import + +typedef NS_ENUM(NSUInteger, ASFlowLayoutDirection) { + ASFlowLayoutDirectionVertical, + ASFlowLayoutDirectionHorizontal, +}; + +/** + * The controller for flow layout. + */ +@interface ASFlowLayoutController : NSObject + +@property (nonatomic, assign) ASRangeTuningParameters tuningParameters; + +@property (nonatomic, readonly, assign) ASFlowLayoutDirection layoutDirection; + +- (instancetype)initWithScrollOption:(ASFlowLayoutDirection)layoutDirection; + +@end diff --git a/AsyncDisplayKit/Details/ASFlowLayoutController.mm b/AsyncDisplayKit/Details/ASFlowLayoutController.mm new file mode 100644 index 0000000000..7101a1103b --- /dev/null +++ b/AsyncDisplayKit/Details/ASFlowLayoutController.mm @@ -0,0 +1,200 @@ +// Copyright 2004-present Facebook. All Rights Reserved. + +#import "ASFlowLayoutController.h" + +#include +#include +#include + +#import "ASAssert.h" + +static const CGFloat kASFlowLayoutControllerRefreshingThreshold = 0.3; + +@interface ASFlowLayoutController() { + std::vector > _nodeSizes; + + std::pair _visibleRangeStartPos; + std::pair _visibleRangeEndPos; + + std::pair _workingRangeStartPos; + std::pair _workingRangeEndPos; +} + +@end + +@implementation ASFlowLayoutController + +- (instancetype)initWithScrollOption:(ASFlowLayoutDirection)layoutDirection { + if (!(self = [super init])) { + return nil; + } + + _layoutDirection = layoutDirection; + + _tuningParameters.leadingBufferScreenfuls = 2; + _tuningParameters.trailingBufferScreenfuls = 1; + + return self; +} + +- (void)insertNodesAtIndexPaths:(NSArray *)indexPaths withSizes:(NSArray *)nodeSizes { + ASDisplayNodeAssert(indexPaths.count == nodeSizes.count, @"Inconsistent index paths and node size"); + + [indexPaths enumerateObjectsUsingBlock:^(NSIndexPath *indexPath, NSUInteger idx, BOOL *stop) { + std::vector &v = _nodeSizes[indexPath.section]; + v.insert(v.begin() + indexPath.row, [(NSValue *)nodeSizes[idx] CGSizeValue]); + }]; +} + +- (void)deleteNodesAtIndexPaths:(NSArray *)indexPaths { + [indexPaths enumerateObjectsWithOptions:NSEnumerationReverse usingBlock:^(NSIndexPath *indexPath, NSUInteger idx, BOOL *stop) { + std::vector &v = _nodeSizes[indexPath.section]; + v.erase(v.begin() + indexPath.row); + }]; +} + +- (void)insertSectionsAtIndexSet:(NSIndexSet *)indexSet { + [indexSet enumerateIndexesUsingBlock:^(NSUInteger idx, BOOL *stop) { + _nodeSizes.insert(_nodeSizes.begin() + idx, std::vector()); + }]; +} + +- (void)deleteSectionsAtIndexSet:(NSIndexSet *)indexSet { + [indexSet enumerateIndexesWithOptions:NSEnumerationReverse usingBlock:^(NSUInteger idx, BOOL *stop) { + _nodeSizes.erase(_nodeSizes.begin() +idx); + }]; +} + +- (BOOL)shouldUpdateWorkingRangesForVisibleIndexPath:(NSArray *)indexPaths + viewportSize:(CGSize)viewportSize { + if (!indexPaths.count) { + return NO; + } + + std::pair startPos, endPos; + ASFindIndexPathRange(indexPaths, startPos, endPos); + + if (_workingRangeStartPos >= startPos || _workingRangeEndPos <= endPos) { + return YES; + } + + return ASFlowLayoutDistance(startPos, _visibleRangeStartPos, _nodeSizes) > ASFlowLayoutDistance(_visibleRangeStartPos, _workingRangeStartPos, _nodeSizes) * kASFlowLayoutControllerRefreshingThreshold || + ASFlowLayoutDistance(endPos, _visibleRangeEndPos, _nodeSizes) > ASFlowLayoutDistance(_visibleRangeEndPos, _workingRangeEndPos, _nodeSizes) * kASFlowLayoutControllerRefreshingThreshold; +} + +- (void)setVisibleNodeIndexPaths:(NSArray *)indexPaths { + ASFindIndexPathRange(indexPaths, _visibleRangeStartPos, _visibleRangeEndPos); +} + +/** + * IndexPath array for the element in the working range. + */ +- (NSSet *)workingRangeIndexPathsForScrolling:(enum ASScrollDirection)scrollDirection + viewportSize:(CGSize)viewportSize { + std::pair startIter, endIter; + + if (_layoutDirection == ASFlowLayoutDirectionHorizontal) { + ASDisplayNodeAssert(scrollDirection == ASScrollDirectionNone || scrollDirection == ASScrollDirectionLeft || scrollDirection == ASScrollDirectionRight, @"Invalid scroll direction"); + + CGFloat backScreens = scrollDirection == ASScrollDirectionLeft ? self.tuningParameters.leadingBufferScreenfuls : self.tuningParameters.trailingBufferScreenfuls; + CGFloat frontScreens = scrollDirection == ASScrollDirectionLeft ? self.tuningParameters.trailingBufferScreenfuls : self.tuningParameters.leadingBufferScreenfuls; + + startIter = ASFindIndexForRange(_nodeSizes, _visibleRangeStartPos, - backScreens * viewportSize.width, _layoutDirection); + endIter = ASFindIndexForRange(_nodeSizes, _visibleRangeEndPos, frontScreens * viewportSize.width, _layoutDirection); + } else { + ASDisplayNodeAssert(scrollDirection == ASScrollDirectionNone || scrollDirection == ASScrollDirectionUp || scrollDirection == ASScrollDirectionDown, @"Invalid scroll direction"); + + int backScreens = scrollDirection == ASScrollDirectionUp ? self.tuningParameters.leadingBufferScreenfuls : self.tuningParameters.trailingBufferScreenfuls; + int frontScreens = scrollDirection == ASScrollDirectionUp ? self.tuningParameters.trailingBufferScreenfuls : self.tuningParameters.leadingBufferScreenfuls; + + startIter = ASFindIndexForRange(_nodeSizes, _visibleRangeStartPos, - backScreens * viewportSize.height, _layoutDirection); + endIter = ASFindIndexForRange(_nodeSizes, _visibleRangeEndPos, frontScreens * viewportSize.height, _layoutDirection); + } + + NSMutableSet *indexPathSet = [[NSMutableSet alloc] init]; + + while (startIter != endIter) { + [indexPathSet addObject:[NSIndexPath indexPathForRow:startIter.second inSection:startIter.first]]; + startIter.second++; + + while (startIter.second == _nodeSizes[startIter.first].size() && startIter.first < _nodeSizes.size()) { + startIter.second = 0; + startIter.first++; + } + } + + [indexPathSet addObject:[NSIndexPath indexPathForRow:endIter.second inSection:endIter.first]]; + + return indexPathSet; +} + +static void ASFindIndexPathRange(NSArray *indexPaths, std::pair &startPos, std::pair &endPos) { + NSIndexPath *initialIndexPath = [indexPaths firstObject]; + startPos = endPos = {initialIndexPath.section, initialIndexPath.row}; + [indexPaths enumerateObjectsUsingBlock:^(NSIndexPath *indexPath, NSUInteger idx, BOOL *stop) { + std::pair p(indexPath.section, indexPath.row); + startPos = MIN(startPos, p); + endPos = MAX(endPos, p); + }]; +} + +static const std::pair ASFindIndexForRange(const std::vector> &nodes, + const std::pair &pos, + CGFloat range, + ASFlowLayoutDirection layoutDirection) { + std::pair cur = pos, pre = pos; + + if (range < 0.0 && cur.first >= 0 && cur.second >= 0) { + // search backward + while (range < 0.0 && cur.second >= 0) { + pre = cur; + CGSize size = nodes[cur.first][cur.second]; + range += layoutDirection == ASFlowLayoutDirectionHorizontal ? size.width : size.height; + cur.second--; + while (cur.second < 0 && cur.first > 0) { + cur.second = (int)nodes[--cur.first].size() - 1; + } + } + + if (cur.second < 0) { + cur = pre; + } + } else { + // search forward + while (range > 0.0 && cur.second < nodes[cur.first].size()) { + pre = cur; + CGSize size = nodes[cur.first][cur.second]; + range -= layoutDirection == ASFlowLayoutDirectionHorizontal ? size.width : size.height; + + cur.second++; + while (cur.second == nodes[cur.first].size() && cur.first < (int)nodes.size() - 1) { + cur.second = 0; + cur.first++; + } + } + + if (cur.second == nodes[cur.first].size()) { + cur = pre; + } + } + + return cur; +} + +static int ASFlowLayoutDistance(const std::pair &start, const std::pair &end, const std::vector> &nodes) { + if (start == end) { + return 0; + } else if (start > end) { + return - ASFlowLayoutDistance(end, start, nodes); + } + + int res = 0; + + for (int i = start.first; i <= end.first; i++) { + res += (i == end.first ? end.second + 1 : nodes[i].size()) - (i == start.first ? start.second : 0); + } + + return res; +} + +@end diff --git a/AsyncDisplayKit/Details/ASLayoutController.h b/AsyncDisplayKit/Details/ASLayoutController.h new file mode 100644 index 0000000000..faa7f5c073 --- /dev/null +++ b/AsyncDisplayKit/Details/ASLayoutController.h @@ -0,0 +1,42 @@ +// Copyright 2004-present Facebook. All Rights Reserved. + +#import + +typedef struct { + CGFloat leadingBufferScreenfuls; + CGFloat trailingBufferScreenfuls; +} ASRangeTuningParameters; + +typedef NS_ENUM(NSInteger, ASScrollDirection) { + ASScrollDirectionNone, + ASScrollDirectionRight, + ASScrollDirectionLeft, + ASScrollDirectionUp, + ASScrollDirectionDown, +}; + +@protocol ASLayoutController + +/** + * Tuning parameters for the working range. + * + * Defaults to a trailing buffer of one screenful and a leading buffer of two screenfuls. + */ +@property (nonatomic, assign) ASRangeTuningParameters tuningParameters; + +- (void)insertNodesAtIndexPaths:(NSArray *)indexPaths withSizes:(NSArray *)nodeSizes; + +- (void)deleteNodesAtIndexPaths:(NSArray *)indexPaths; + +- (void)insertSectionsAtIndexSet:(NSIndexSet *)indexSet; + +- (void)deleteSectionsAtIndexSet:(NSIndexSet *)indexSet; + +- (void)setVisibleNodeIndexPaths:(NSArray *)indexPaths; + +- (BOOL)shouldUpdateWorkingRangesForVisibleIndexPath:(NSArray *)indexPath + viewportSize:(CGSize)viewportSize; + +- (NSSet *)workingRangeIndexPathsForScrolling:(enum ASScrollDirection)scrollDirection + viewportSize:(CGSize)viewportSize; +@end diff --git a/AsyncDisplayKit/Details/ASMultidimensionalArrayUtils.h b/AsyncDisplayKit/Details/ASMultidimensionalArrayUtils.h new file mode 100644 index 0000000000..ff0cbaa5d6 --- /dev/null +++ b/AsyncDisplayKit/Details/ASMultidimensionalArrayUtils.h @@ -0,0 +1,46 @@ +// Copyright 2004-present Facebook. All Rights Reserved. + +#import + +#import "ASBaseDefines.h" + +/** + * Helper class for operation on multidimensional array, where the object of array may be an object or an array. + */ + +ASDISPLAYNODE_EXTERN_C_BEGIN + +/** + * Deep muutable copy of multidimensional array. + * It will recursively do the multiple copy for each subarray. + */ +extern NSObject *ASMultidimensionalArrayDeepMutableCopy(NSObject *obj); + +/** + * Insert the elements into the mutable multidimensional array at given index paths. + */ +extern void ASInsertElementsIntoMultidimensionalArrayAtIndexPaths(NSMutableArray *mutableArray, NSArray *indexPaths, NSArray *elements); + +/** + * Delete the elements of the mutable multidimensional array at given index paths + */ +extern void ASDeleteElementsInMultidimensionalArrayAtIndexPaths(NSMutableArray *mutableArray, NSArray *indexPaths); + +/** + * Find the elements of the mutable multidimensional array at given index paths. + */ +extern NSArray *ASFindElementsInMultidimensionalArrayAtIndexPaths(NSMutableArray *mutableArray, NSArray *indexPaths); + +/** + * Return all the index paths of mutable multidimensional array at given index set, in ascending order. + */ +extern NSArray *ASIndexPathsForMultidimensionalArrayAtIndexSet(NSArray *MultidimensionalArray, NSIndexSet *indexSet); + +/** + * Reteurn all the index paths of mutable multidimensional array, in ascending order. + */ +extern NSArray *ASIndexPathsForMultidimensionalArray(NSArray *MultidimensionalArray); + + +ASDISPLAYNODE_EXTERN_C_END + diff --git a/AsyncDisplayKit/Details/ASMultidimensionalArrayUtils.mm b/AsyncDisplayKit/Details/ASMultidimensionalArrayUtils.mm new file mode 100644 index 0000000000..aeca317dbe --- /dev/null +++ b/AsyncDisplayKit/Details/ASMultidimensionalArrayUtils.mm @@ -0,0 +1,119 @@ +// Copyright 2004-present Facebook. All Rights Reserved. + +#import "ASAssert.h" +#import "ASMultidimensionalArrayUtils.h" + +#pragma mark - Internal Methods + +static void ASRecursivelyUpdateMultidimensionalArrayAtIndexPaths(NSMutableArray *mutableArray, + const NSArray *indexPaths, + NSUInteger &curIdx, + NSIndexPath *curIndexPath, + const NSUInteger dimension, + void (^updateBlock)(NSMutableArray *arr, NSIndexSet *indexSet, NSUInteger idx)) { + if (curIdx == indexPaths.count) { + return; + } + + if (curIndexPath.length < dimension - 1) { + for (int i = 0; i < mutableArray.count; i++) { + ASRecursivelyUpdateMultidimensionalArrayAtIndexPaths(mutableArray[i], indexPaths, curIdx, [curIndexPath indexPathByAddingIndex:i], dimension, updateBlock); + } + } else { + NSMutableIndexSet *indexSet = [[NSMutableIndexSet alloc] init]; + + while (curIdx < indexPaths.count && + [curIndexPath isEqual:[indexPaths[curIdx] indexPathByRemovingLastIndex]]) { + [indexSet addIndex:[indexPaths[curIdx] indexAtPosition:curIndexPath.length]]; + curIdx++; + } + + updateBlock(mutableArray, indexSet, curIdx); + } +} + +static void ASRecursivelyFindIndexPathsForMultidimensionalArray(NSObject *obj, NSIndexPath *curIndexPath, NSMutableArray *res) { + if (![obj isKindOfClass:[NSArray class]]) { + [res addObject:curIndexPath]; + } else { + NSArray *arr = (NSArray *)obj; + [arr enumerateObjectsUsingBlock:^(NSObject *subObj, NSUInteger idx, BOOL *stop) { + ASRecursivelyFindIndexPathsForMultidimensionalArray(subObj, [curIndexPath indexPathByAddingIndex:idx], res); + }]; + } +} + +#pragma mark - Public Methods + +NSObject *ASMultidimensionalArrayDeepMutableCopy(NSObject *obj) { + if ([obj isKindOfClass:[NSArray class]]) { + NSArray *arr = (NSArray *)obj; + NSMutableArray * mutableArr = [NSMutableArray array]; + for (NSObject *elem in arr) { + [mutableArr addObject:ASMultidimensionalArrayDeepMutableCopy(elem)]; + } + return mutableArr; + } + + return obj; +} + +void ASInsertElementsIntoMultidimensionalArrayAtIndexPaths(NSMutableArray *mutableArray, NSArray *indexPaths, NSArray *elements) { + ASDisplayNodeCAssert(indexPaths.count == elements.count, @"Inconsistent indexPaths and elements"); + + if (!indexPaths.count) { + return; + } + + NSUInteger curIdx = 0; + NSIndexPath *indexPath = [[NSIndexPath alloc] init]; + ASRecursivelyUpdateMultidimensionalArrayAtIndexPaths(mutableArray, indexPaths, curIdx, indexPath, [indexPaths[0] length], ^(NSMutableArray *arr, NSIndexSet *indexSet, NSUInteger idx) { + [arr insertObjects:[elements subarrayWithRange:NSMakeRange(idx - indexSet.count, indexSet.count)] + atIndexes:indexSet]; + }); +} + +void ASDeleteElementsInMultidimensionalArrayAtIndexPaths(NSMutableArray *mutableArray, NSArray *indexPaths) { + if (!indexPaths.count) { + return; + } + + NSUInteger curIdx = 0; + NSIndexPath *indexPath = [[NSIndexPath alloc] init]; + + ASRecursivelyUpdateMultidimensionalArrayAtIndexPaths(mutableArray, indexPaths, curIdx, indexPath, [indexPaths[0] length], ^(NSMutableArray *arr, NSIndexSet *indexSet, NSUInteger idx) { + [arr removeObjectsAtIndexes:indexSet]; + }); +} + +NSArray *ASFindElementsInMultidimensionalArrayAtIndexPaths(NSMutableArray *mutableArray, NSArray *indexPaths) { + NSUInteger curIdx = 0; + NSIndexPath *indexPath = [[NSIndexPath alloc] init]; + NSMutableArray *deletedElements = [[NSMutableArray alloc] initWithCapacity:indexPaths.count]; + + if (!indexPaths.count) { + return deletedElements; + } + + ASRecursivelyUpdateMultidimensionalArrayAtIndexPaths(mutableArray, indexPaths, curIdx, indexPath, [indexPaths[0] length], ^(NSMutableArray *arr, NSIndexSet *indexSet, NSUInteger idx) { + [deletedElements addObjectsFromArray:[arr objectsAtIndexes:indexSet]]; + }); + + return deletedElements; +} + +NSArray *ASIndexPathsForMultidimensionalArrayAtIndexSet(NSArray *multidimensionalArray, NSIndexSet *indexSet) { + NSMutableArray *res = [[NSMutableArray alloc] initWithCapacity:multidimensionalArray.count]; + [indexSet enumerateIndexesUsingBlock:^(NSUInteger idx, BOOL *stop) { + ASRecursivelyFindIndexPathsForMultidimensionalArray(multidimensionalArray[idx], [NSIndexPath indexPathWithIndex:idx], res); + }]; + + return res; +} + +NSArray *ASIndexPathsForMultidimensionalArray(NSArray *multidimensionalArray) { + NSMutableArray *res = [NSMutableArray arrayWithCapacity:multidimensionalArray.count]; + ASRecursivelyFindIndexPathsForMultidimensionalArray(multidimensionalArray, [[NSIndexPath alloc] init], res); + return res; +} + diff --git a/AsyncDisplayKit/Details/ASRangeController.h b/AsyncDisplayKit/Details/ASRangeController.h index 201fdb3fc3..4befde4bb9 100644 --- a/AsyncDisplayKit/Details/ASRangeController.h +++ b/AsyncDisplayKit/Details/ASRangeController.h @@ -9,19 +9,9 @@ #import #import - -typedef struct { - // working range buffers, on either side of scroll - CGFloat trailingBufferScreenfuls; - CGFloat leadingBufferScreenfuls; -} ASRangeTuningParameters; - -typedef NS_ENUM(NSInteger, ASScrollDirection) { - ASScrollDirectionBackward, - ASScrollDirectionForward, -}; - -typedef NSRange (^asrangecontroller_working_range_calculation_block_t)(ASRangeTuningParameters params, ASScrollDirection scrollDirection, NSRange visibleRange, NSArray *nodeSizes, CGSize viewport); +#import +#import +#import @protocol ASRangeControllerDelegate; @@ -33,85 +23,30 @@ typedef NSRange (^asrangecontroller_working_range_calculation_block_t)(ASRangeTu * a working range, and is responsible for handling AsyncDisplayKit machinery (sizing cell nodes, enqueueing and * cancelling their asynchronous layout and display, and so on). */ -@interface ASRangeController : ASDealloc2MainObject - -/** - * Notify the receiver that its delegate's data source has been set or changed. This is like -[UITableView reloadData] - * but drastically more expensive, as it destroys the working range and all cached nodes. - */ -- (void)rebuildData; +@interface ASRangeController : ASDealloc2MainObject /** * Notify the receiver that the visible range has been updated. * * @see [ASRangeControllerDelegate rangeControllerVisibleNodeIndexPaths:] */ -- (void)visibleNodeIndexPathsDidChange; - -/** - * ASTableView is only aware of nodes that have already been sized. - * - * Custom ASCellNode implementations are encouraged to have "realistic placeholders", since they can only be onscreen if - * they have enough data for layout. E.g., try setting all subnodes' background colours to [UIColor lightGrayColor]. - */ -- (NSInteger)numberOfSizedSections; -- (NSInteger)numberOfSizedRowsInSection:(NSInteger)section; - -/** - * Configure the specified UITableViewCell's content view, and apply properties from ASCellNode. - * - * @param cell UITableViewCell to configure. - * - * @param indexPath Index path for the node of interest. - */ -- (void)configureTableViewCell:(UITableViewCell *)cell forIndexPath:(NSIndexPath *)indexPath; +- (void)visibleNodeIndexPathsDidChangeWithScrollDirection:(enum ASScrollDirection)scrollDirection; /** * Add the sized node for `indexPath` as a subview of `contentView`. * * @param contentView UIView to add a (sized) node's view to. * - * @param indexPath Index path for the node to be added. + * @param node The ASCellNode to be added. */ -- (void)configureContentView:(UIView *)contentView forIndexPath:(NSIndexPath *)indexPath; - -/** - * Query the sized node at `indexPath` for its calculatedSize. - * - * @param indexPath The index path for the node of interest. - * - * TODO: Currently we disallow direct access to ASCellNode outside ASRangeController since touching the node's view can - * break async display. We should expose the node anyway, possibly with an assertion guarding against external - * use of the view property, so ASCellNode can support configuration for UITableViewCell properties (selection - * style, separator style, etc.) and ASTableView can query that data. - */ -- (CGSize)calculatedSizeForNodeAtIndexPath:(NSIndexPath *)indexPath; - -/** - * Notify the receiver that its data source has been updated to append the specified nodes. - * - * @param indexPaths Array of NSIndexPaths for the newly-sized nodes. - */ -- (void)appendNodesWithIndexPaths:(NSArray *)indexPaths; +- (void)configureContentView:(UIView *)contentView forCellNode:(ASCellNode *)node; /** * Delegate and ultimate data source. Must not be nil. */ @property (nonatomic, weak) id delegate; -/** - * Tuning parameters for the working range. - * - * Defaults to a trailing buffer of one screenful and a leading buffer of two screenfuls. - */ -@property (nonatomic, assign) ASRangeTuningParameters tuningParameters; - -/** - * @abstract An optional block which can perform custom calculation for working range. - * - * @discussion Can be used to provide custom working range logic for custom layouts. - */ -@property (nonatomic, readwrite, copy) asrangecontroller_working_range_calculation_block_t workingRangeCalculationBlock; +@property (nonatomic, strong) id layoutController; @end @@ -136,52 +71,28 @@ typedef NSRange (^asrangecontroller_working_range_calculation_block_t)(ASRangeTu - (CGSize)rangeControllerViewportSize:(ASRangeController *)rangeController; /** - * @param rangeController Sender. - * - * @returns The number of total sections. - * - * @discussion forwards this method to its data source. + * Fetch nodes at specific index paths. */ -- (NSInteger)rangeControllerSections:(ASRangeController *)rangeController; +- (NSArray *)rangeController:(ASRangeController *)rangeController nodesAtIndexPaths:(NSArray *)indexPaths; /** - * @param rangeController Sender. - * - * @param section Section. - * - * @returns The number of rows in `section`. - * - * @discussion forwards this method to its data source. + * Called for nodes insertion. */ -- (NSInteger)rangeController:(ASRangeController *)rangeController rowsInSection:(NSInteger)section; +- (void)rangeController:(ASRangeController *)rangeController didInsertNodesAtIndexPaths:(NSArray *)indexPaths; /** - * @param rangeController Sender. - * - * @param indexPath Index path for the node of interest. - * - * @returns A new corresponding to `indexPath`. - * - * @discussion forwards this method to its data source. + * Called for nodes deletion. */ -- (ASCellNode *)rangeController:(ASRangeController *)rangeController nodeForIndexPath:(NSIndexPath *)indexPath; +- (void)rangeController:(ASRangeController *)rangeController didDeleteNodesAtIndexPaths:(NSArray *)indexPaths; /** - * @param rangeController Sender. - * - * @param indexPath Node to be sized. - * - * @returns Sizing constraints for the node at `indexPath`, to be used as an argument to <[ASDisplayNode measure:]>. + * Called for section insertion. */ -- (CGSize)rangeController:(ASRangeController *)rangeController constrainedSizeForNodeAtIndexPath:(NSIndexPath *)indexPath; +- (void)rangeController:(ASRangeController *)rangeController didInsertSectionsAtIndexSet:(NSIndexSet *)indexSet; /** - * Notifies the receiver that the specified nodes have been sized and are ready for display. - * - * @param rangeController Sender. - * - * @param indexPaths Array of NSIndexPaths for the newly-sized nodes. + * Called for section deletion. */ -- (void)rangeController:(ASRangeController *)rangeController didSizeNodesWithIndexPaths:(NSArray *)indexPaths; +- (void)rangeController:(ASRangeController *)rangeController didDeleteSectionsAtIndexSet:(NSIndexSet *)indexSet; @end diff --git a/AsyncDisplayKit/Details/ASRangeController.mm b/AsyncDisplayKit/Details/ASRangeController.mm index 67752261c8..946a795342 100644 --- a/AsyncDisplayKit/Details/ASRangeController.mm +++ b/AsyncDisplayKit/Details/ASRangeController.mm @@ -11,7 +11,9 @@ #import "ASAssert.h" #import "ASDisplayNodeExtras.h" #import "ASDisplayNodeInternal.h" -#import "ASRangeControllerInternal.h" +#import "ASLayoutController.h" + +#import "ASMultiDimensionalArrayUtils.h" @interface ASDisplayNode (ASRangeController) @@ -53,170 +55,35 @@ @end @interface ASRangeController () { - // index path -> node mapping - NSMutableDictionary *_nodes; - - // array of boxed CGSizes. _nodeSizes.count == the number of nodes that have been sized - // TODO optimise this, perhaps by making _nodes an array - NSMutableArray *_nodeSizes; - - // consumer data source information - NSArray *_sectionCounts; - NSInteger _totalNodeCount; - - // used for global <-> section.row mapping. _sectionOffsets[section] is the index at which the section starts - NSArray *_sectionOffsets; - - // sized data source information - NSInteger _sizedNodeCount; - - // ranges + NSSet *_workingRangeIndexPaths; + NSSet *_workingRangeNodes; + BOOL _queuedRangeUpdate; + ASScrollDirection _scrollDirection; - NSRange _visibleRange; - NSRange _workingRange; - NSMutableOrderedSet *_workingIndexPaths; } @end - @implementation ASRangeController -#pragma mark - -#pragma mark Lifecycle. +- (instancetype)init { + if (self = [super init]) { -- (instancetype)init -{ - if (!(self = [super init])) - return nil; - - _tuningParameters = { - .trailingBufferScreenfuls = 1.0f, - .leadingBufferScreenfuls = 2.0f, - }; + _workingRangeIndexPaths = [NSSet set]; + } return self; } -- (void)dealloc -{ - [self teardownAllNodes]; -} - -- (void)teardownAllNodes -{ - for (ASCellNode *node in _nodes.allValues) { - [node removeFromSupernode]; - - if (node.nodeLoaded) - [node.view removeFromSuperview]; - } - [_nodes removeAllObjects]; - _nodes = nil; - -} - -- (void)cancelSizeNextBlock{ - [NSObject cancelPreviousPerformRequestsWithTarget:self selector:@selector(sizeNextBlock) object:nil]; -} - -+ (dispatch_queue_t)sizingQueue -{ - static dispatch_queue_t sizingQueue = NULL; - static dispatch_once_t onceToken; - dispatch_once(&onceToken, ^{ - sizingQueue = dispatch_queue_create("com.facebook.AsyncDisplayKit.ASRangeController.sizingQueue", DISPATCH_QUEUE_CONCURRENT); - // we use the highpri queue to prioritize UI rendering over other async operations - dispatch_set_target_queue(sizingQueue, dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_HIGH, 0)); - }); - - return sizingQueue; -} - - -#pragma mark - -#pragma mark Helpers. - -static NSOrderedSet *ASCopySetMinusSet(NSOrderedSet *minuend, NSOrderedSet *subtrahend) -{ - NSMutableOrderedSet *difference = [minuend mutableCopy]; - [difference minusOrderedSet:subtrahend]; - return difference; -} - -// useful for debugging: working range, buffer sizes, and visible range -__attribute__((unused)) static NSString *ASWorkingRangeDebugDescription(NSRange workingRange, NSRange visibleRange) -{ - NSInteger visibleRangeLastElement = NSMaxRange(visibleRange) - 1; - NSInteger workingRangeLastElement = NSMaxRange(workingRange) - 1; - return [NSString stringWithFormat:@"[%zd(%zd) [%zd, %zd] (%zd)%zd]", - workingRange.location, - visibleRange.location - workingRange.location, - visibleRange.location, - visibleRangeLastElement, - workingRangeLastElement - visibleRangeLastElement, - workingRangeLastElement]; -} - -#pragma mark NSRange <-> NSIndexPath. - -static BOOL ASRangeIsValid(NSRange range) -{ - return range.location != NSNotFound && range.length > 0; -} - -- (NSIndexPath *)indexPathForIndex:(NSInteger)index -{ - ASDisplayNodeAssert(index < _totalNodeCount, @"invalid argument"); - - for (NSInteger section = _sectionCounts.count - 1; section >= 0; section--) { - NSInteger offset = [_sectionOffsets[section] integerValue]; - if (offset <= index) { - return [NSIndexPath indexPathForRow:index - offset inSection:section]; - } - } - - ASDisplayNodeAssert(NO, @"logic error"); - return nil; -} - -- (NSArray *)indexPathsForRange:(NSRange)range -{ - ASDisplayNodeAssert(ASRangeIsValid(range) && NSMaxRange(range) <= _totalNodeCount, @"invalid argument"); - - NSMutableArray *result = [NSMutableArray arrayWithCapacity:range.length]; - - NSIndexPath *indexPath = [self indexPathForIndex:range.location]; - for (NSInteger i = range.location; i < NSMaxRange(range); i++) { - [result addObject:indexPath]; - - if (indexPath.row + 1 >= [_sectionCounts[indexPath.section] integerValue]) { - indexPath = [NSIndexPath indexPathForRow:0 inSection:indexPath.section + 1]; - } else { - indexPath = [NSIndexPath indexPathForRow:indexPath.row + 1 inSection:indexPath.section]; - } - } - - return result; -} - -- (NSInteger)indexForIndexPath:(NSIndexPath *)indexPath -{ - NSInteger index = [_sectionOffsets[indexPath.section] integerValue] + indexPath.row; - ASDisplayNodeAssert(index < _totalNodeCount, @"invalid argument"); - return index; -} - -#pragma mark View manipulation. +#pragma mark - View manipulation. - (void)discardNode:(ASCellNode *)node { ASDisplayNodeAssertMainThread(); ASDisplayNodeAssert(node, @"invalid argument"); - NSInteger index = [self indexForIndexPath:node.asyncdisplaykit_indexPath]; - if (NSLocationInRange(index, _workingRange)) { + if ([_workingRangeNodes containsObject:node]) { // move the node's view to the working range area, so its rendering persists [self addNodeToWorkingRange:node]; } else { @@ -236,8 +103,6 @@ static BOOL ASRangeIsValid(NSRange range) // since this class usually manages large or infinite data sets, the working range // directly bounds memory usage by requiring redrawing any content that falls outside the range. [node recursivelyReclaimMemory]; - - [_workingIndexPaths removeObject:node.asyncdisplaykit_indexPath]; } - (void)addNodeToWorkingRange:(ASCellNode *)node @@ -249,8 +114,6 @@ static BOOL ASRangeIsValid(NSRange range) [node.view removeFromSuperview]; [node recursivelyDisplay]; - - [_workingIndexPaths addObject:node.asyncdisplaykit_indexPath]; } - (void)moveNode:(ASCellNode *)node toView:(UIView *)view @@ -266,63 +129,16 @@ static BOOL ASRangeIsValid(NSRange range) [CATransaction commit]; } - #pragma mark - #pragma mark API. -- (void)recalculateDataSourceCounts +- (void)visibleNodeIndexPathsDidChangeWithScrollDirection:(ASScrollDirection)scrollDirection { - // data source information (_sectionCounts, _sectionOffsets, _totalNodeCount) is not currently thread-safe - ASDisplayNodeAssertMainThread(); + _scrollDirection = scrollDirection; - NSInteger sections = [_delegate rangeControllerSections:self]; - - NSMutableArray *sectionCounts = [NSMutableArray arrayWithCapacity:sections]; - for (NSInteger section = 0; section < sections; section++) { - sectionCounts[section] = @([_delegate rangeController:self rowsInSection:section]); - } - - NSMutableArray *sectionOffsets = [NSMutableArray arrayWithCapacity:sections]; - NSInteger offset = 0; - for (NSInteger section = 0; section < sections; section++) { - sectionOffsets[section] = @(offset); - offset += [sectionCounts[section] integerValue]; - } - - _sectionCounts = sectionCounts; - _sectionOffsets = sectionOffsets; - _totalNodeCount = offset; -} - -- (void)rebuildData -{ - /* - * teardown - */ - [self teardownAllNodes]; - [self cancelSizeNextBlock]; - - /* - * setup - */ - [self recalculateDataSourceCounts]; - _nodes = [NSMutableDictionary dictionaryWithCapacity:_totalNodeCount]; - _visibleRange = _workingRange = NSMakeRange(NSNotFound, 0); - _sizedNodeCount = 0; - _nodeSizes = [NSMutableArray array]; - _scrollDirection = ASScrollDirectionForward; - _workingIndexPaths = [NSMutableOrderedSet orderedSet]; - - // don't bother sizing if the data source is empty - if (_totalNodeCount > 0) { - [self sizeNextBlock]; - } -} - -- (void)visibleNodeIndexPathsDidChange -{ - if (_queuedRangeUpdate) + if (_queuedRangeUpdate) { return; + } // coalesce these events -- handling them multiple times per runloop is noisy and expensive _queuedRangeUpdate = YES; @@ -334,67 +150,50 @@ static BOOL ASRangeIsValid(NSRange range) - (void)updateVisibleNodeIndexPaths { + if (!_queuedRangeUpdate) { + return; + } + NSArray *indexPaths = [_delegate rangeControllerVisibleNodeIndexPaths:self]; - if (indexPaths.count) { - [self setVisibleRange:NSMakeRange([self indexForIndexPath:[indexPaths firstObject]], - indexPaths.count)]; + CGSize viewportSize = [_delegate rangeControllerViewportSize:self]; + + if ([_layoutController shouldUpdateWorkingRangesForVisibleIndexPath:indexPaths viewportSize:viewportSize]) { + [_layoutController setVisibleNodeIndexPaths:indexPaths]; + NSSet *workingRangeIndexPaths = [_layoutController workingRangeIndexPathsForScrolling:_scrollDirection viewportSize:viewportSize]; + NSSet *visibleRangeIndexPaths = [NSSet setWithArray:indexPaths]; + + NSMutableSet *removedIndexPaths = [_workingRangeIndexPaths mutableCopy]; + [removedIndexPaths minusSet:workingRangeIndexPaths]; + [removedIndexPaths minusSet:visibleRangeIndexPaths]; + if (removedIndexPaths.count) { + NSArray *removedNodes = [_delegate rangeController:self nodesAtIndexPaths:[removedIndexPaths allObjects]]; + [removedNodes enumerateObjectsUsingBlock:^(ASCellNode *node, NSUInteger idx, BOOL *stop) { + [self removeNodeFromWorkingRange:node]; + }]; + } + + NSMutableSet *addedIndexPaths = [workingRangeIndexPaths mutableCopy]; + [addedIndexPaths minusSet:_workingRangeIndexPaths]; + [addedIndexPaths minusSet:visibleRangeIndexPaths]; + if (addedIndexPaths.count) { + NSArray *addedNodes = [_delegate rangeController:self nodesAtIndexPaths:[addedIndexPaths allObjects]]; + [addedNodes enumerateObjectsUsingBlock:^(ASCellNode *node, NSUInteger idx, BOOL *stop) { + [self addNodeToWorkingRange:node]; + }]; + } + + _workingRangeIndexPaths = workingRangeIndexPaths; + _workingRangeNodes = [NSSet setWithArray:[_delegate rangeController:self nodesAtIndexPaths:[workingRangeIndexPaths allObjects]]]; } _queuedRangeUpdate = NO; } -- (NSInteger)numberOfSizedSections +- (void)configureContentView:(UIView *)contentView forCellNode:(ASCellNode *)cellNode { - // short-circuit if we haven't started sizing - if (_sizedNodeCount == 0) - return 0; + [cellNode recursivelySetDisplaySuspended:NO]; - NSIndexPath *lastSizedIndex = [self indexPathForIndex:_sizedNodeCount - 1]; - NSInteger sizedSectionCount = lastSizedIndex.section + 1; - - ASDisplayNodeAssert(sizedSectionCount <= _sectionCounts.count, @"logic error"); - return sizedSectionCount; -} - -- (NSInteger)numberOfSizedRowsInSection:(NSInteger)section -{ - // short-circuit if we haven't started sizing - if (_sizedNodeCount == 0) - return 0; - - if (section > _sectionCounts.count) { - ASDisplayNodeAssert(NO, @"this isn't even a valid section"); - return 0; - } - - NSIndexPath *lastSizedIndex = [self indexPathForIndex:_sizedNodeCount - 1]; - if (section > lastSizedIndex.section) { - ASDisplayNodeAssert(NO, @"this section hasn't been sized yet"); - return 0; - } else if (section == lastSizedIndex.section) { - // we're still sizing this section, return the count we have - return lastSizedIndex.row + 1; - } else { - // we've already sized beyond this section, return the full count - return [_sectionCounts[section] integerValue]; - } -} - -- (void)configureTableViewCell:(UITableViewCell *)cell forIndexPath:(NSIndexPath *)indexPath -{ - [self configureContentView:cell.contentView forIndexPath:indexPath]; - - ASCellNode *node = [self sizedNodeForIndexPath:indexPath]; - cell.backgroundColor = node.backgroundColor; - cell.selectionStyle = node.selectionStyle; -} - -- (void)configureContentView:(UIView *)contentView forIndexPath:(NSIndexPath *)indexPath -{ - ASCellNode *newNode = [self sizedNodeForIndexPath:indexPath]; - ASDisplayNodeAssert(newNode, @"this node hasn't been sized yet!"); - - if (newNode.view.superview == contentView) { + if (cellNode.view.superview == contentView) { // this content view is already correctly configured return; } @@ -411,269 +210,58 @@ static BOOL ASRangeIsValid(NSRange range) } } - [self moveNode:newNode toView:contentView]; + [self moveNode:cellNode toView:contentView]; } -- (CGSize)calculatedSizeForNodeAtIndexPath:(NSIndexPath *)indexPath -{ - // TODO add an assertion (here or in ASTableView) that the calculated size isn't bogus (eg must be < tableview width) - ASCellNode *node = [self sizedNodeForIndexPath:indexPath]; - return node.calculatedSize; -} +#pragma mark - ASDataControllerDelegete - -#pragma mark - -#pragma mark Working range. - -- (void)setTuningParameters:(ASRangeTuningParameters)tuningParameters -{ - _tuningParameters = tuningParameters; - - if (ASRangeIsValid(_visibleRange)) { - [self recalculateWorkingRange]; - } -} - -static NSRange ASCalculateWorkingRange(ASRangeTuningParameters params, ASScrollDirection scrollDirection, - NSRange visibleRange, NSArray *nodeSizes, CGSize viewport) -{ - ASDisplayNodeCAssert(NSMaxRange(visibleRange) <= nodeSizes.count, @"nodes can't be visible until they're sized"); - - // extend the visible range by enough nodes to fill at least the requested number of screenfuls - // NB. this logic assumes there is no overlap between nodes. It also doesn't - // take spacing between nodes into account. - CGFloat viewportArea = viewport.width * viewport.height; - CGFloat minUpperBufferSize, minLowerBufferSize; - switch (scrollDirection) { - case ASScrollDirectionBackward: - minUpperBufferSize = viewportArea * params.leadingBufferScreenfuls; - minLowerBufferSize = viewportArea * params.trailingBufferScreenfuls; - break; - - case ASScrollDirectionForward: - minUpperBufferSize = viewportArea * params.trailingBufferScreenfuls; - minLowerBufferSize = viewportArea * params.leadingBufferScreenfuls; - break; - } - - // "top" buffer (above the screen, if we're scrolling vertically) - NSInteger upperBuffer = 0; - CGFloat upperBufferArea = 0.0f; - for (NSInteger idx = visibleRange.location - 1; idx >= 0 && upperBufferArea < minUpperBufferSize; idx--) { - upperBuffer++; - CGSize nodeSize = [nodeSizes[idx] CGSizeValue]; - upperBufferArea += nodeSize.width * nodeSize.height; - } - - // "bottom" buffer (below the screen, if we're scrolling vertically) - NSInteger lowerBuffer = 0; - CGFloat lowerBufferArea = 0.0f; - for (NSInteger idx = NSMaxRange(visibleRange); idx < nodeSizes.count && lowerBufferArea < minLowerBufferSize; idx++) { - lowerBuffer++; - CGSize nodeSize = [nodeSizes[idx] CGSizeValue]; - lowerBufferArea += nodeSize.width * nodeSize.height; - } - - return NSMakeRange(visibleRange.location - upperBuffer, - visibleRange.length + upperBuffer + lowerBuffer); -} - -- (void)setVisibleRange:(NSRange)visibleRange -{ - if (NSEqualRanges(_visibleRange, visibleRange)) - return; - - ASDisplayNodeAssert(ASRangeIsValid(visibleRange), @"invalid argument"); - NSRange previouslyVisible = ASRangeIsValid(_visibleRange) ? _visibleRange : visibleRange; - _visibleRange = visibleRange; - - // figure out where we're going, because that's where the bulk of the working range needs to be - NSInteger scrollDelta = _visibleRange.location - previouslyVisible.location; - if (scrollDelta < 0) - _scrollDirection = ASScrollDirectionBackward; - if (scrollDelta > 0) - _scrollDirection = ASScrollDirectionForward; - - [self recalculateWorkingRange]; -} - -- (void)recalculateWorkingRange -{ - NSRange workingRange; - if (self.workingRangeCalculationBlock != NULL) { - workingRange = self.workingRangeCalculationBlock(_tuningParameters, - _scrollDirection, - _visibleRange, - _nodeSizes, - [_delegate rangeControllerViewportSize:self]); +/** + * Dispatch to main thread for updating ranges. + * We are considering to move it to background queue if we could call recursive display in background thread. + */ +- (void)updateOnMainThreadWithBlock:(dispatch_block_t)block { + if ([NSThread isMainThread]) { + block(); } else { - workingRange = ASCalculateWorkingRange(_tuningParameters, - _scrollDirection, - _visibleRange, - _nodeSizes, - [_delegate rangeControllerViewportSize:self]); - } - - [self setWorkingRange:workingRange]; -} - -- (void)setWorkingRange:(NSRange)newWorkingRange -{ - if (NSEqualRanges(_workingRange, newWorkingRange)) - return; - - // the working range is a superset of the visible range, but we only care about offscreen nodes - ASDisplayNodeAssert(NSEqualRanges(_visibleRange, NSIntersectionRange(_visibleRange, newWorkingRange)), @"logic error"); - NSOrderedSet *visibleIndexPaths = [NSOrderedSet orderedSetWithArray:[self indexPathsForRange:_visibleRange]]; - NSOrderedSet *oldWorkingIndexPaths = ASCopySetMinusSet(_workingIndexPaths, visibleIndexPaths); - NSOrderedSet *newWorkingIndexPaths = ASCopySetMinusSet([NSOrderedSet orderedSetWithArray:[self indexPathsForRange:newWorkingRange]], visibleIndexPaths); - - // update bookkeeping for visible nodes; these will be removed from the working range later in -configureContentView:: - [_workingIndexPaths minusOrderedSet:visibleIndexPaths]; - - // evict nodes that have left the working range (i.e., those that are in the old working range but not the new one) - NSOrderedSet *removedIndexPaths = ASCopySetMinusSet(oldWorkingIndexPaths, newWorkingIndexPaths); - for (NSIndexPath *indexPath in removedIndexPaths) { - ASCellNode *node = [self sizedNodeForIndexPath:indexPath]; - ASDisplayNodeAssert(node, @"an unsized node should never have entered the working range"); - [self removeNodeFromWorkingRange:node]; - } - - // add nodes that have entered the working range (i.e., those that are in the new working range but not the old one) - NSOrderedSet *addedIndexPaths = ASCopySetMinusSet(newWorkingIndexPaths, oldWorkingIndexPaths); - for (NSIndexPath *indexPath in addedIndexPaths) { - // if a node in the working range is still sizing, the sizing logic will add it to the working range for us later - ASCellNode *node = [self sizedNodeForIndexPath:indexPath]; - if (node) { - [self addNodeToWorkingRange:node]; - } else { - ASDisplayNodeAssert(_sizedNodeCount != _totalNodeCount, @"logic error"); - } - } - - _workingRange = newWorkingRange; -} - - -#pragma mark - -#pragma mark Async sizing. - -- (ASCellNode *)sizedNodeForIndexPath:(NSIndexPath *)indexPath -{ - if ([self indexForIndexPath:indexPath] >= _sizedNodeCount) { - // this node hasn't been sized yet - return nil; - } - - // work around applebug: a UIMutableIndexPath with row r and section s is not considered equal to an NSIndexPath with - // row r and section s, so we cannot use the provided indexPath directly as a dictionary index. - ASCellNode *sizedNode = _nodes[[NSIndexPath indexPathForRow:indexPath.row inSection:indexPath.section]]; - ASDisplayNodeAssert(sizedNode, @"this node should be sized but doesn't even exist"); - ASDisplayNodeAssert([sizedNode.asyncdisplaykit_indexPath isEqual:indexPath], @"this node has the wrong index path"); - [sizedNode recursivelySetDisplaySuspended:NO]; - return sizedNode; -} - -- (void)sizeNextBlock -{ - // can't size anything if we don't have a delegate - if (!_delegate) - return; - - // concurrently size as many nodes as the CPU allows - static const NSInteger blockSize = [[NSProcessInfo processInfo] processorCount]; - NSRange sizingRange = NSMakeRange(_sizedNodeCount, MIN(blockSize, _totalNodeCount - _sizedNodeCount)); - - // manage sizing on a throwaway background queue; we'll be blocking it - dispatch_async(dispatch_queue_create(NULL, DISPATCH_QUEUE_CONCURRENT), ^{ - dispatch_group_t group = dispatch_group_create(); - - NSArray *indexPaths = [self indexPathsForRange:sizingRange]; - for (NSIndexPath *indexPath in indexPaths) { - ASCellNode *node = [_delegate rangeController:self nodeForIndexPath:indexPath]; - node.asyncdisplaykit_indexPath = indexPath; - _nodes[indexPath] = node; - - dispatch_group_async(group, [ASRangeController sizingQueue], ^{ - [node measure:[_delegate rangeController:self constrainedSizeForNodeAtIndexPath:indexPath]]; - node.frame = CGRectMake(0.0f, 0.0f, node.calculatedSize.width, node.calculatedSize.height); - }); - } - - // wait for all sizing to finish, then bounce back to main - // TODO consider using a semaphore here -- we currently don't size nodes while updating the working range - dispatch_group_wait(group, DISPATCH_TIME_FOREVER); dispatch_async(dispatch_get_main_queue(), ^{ - // update sized node information - _sizedNodeCount = NSMaxRange(sizingRange); - for (NSIndexPath *indexPath in indexPaths) { - ASCellNode *node = _nodes[indexPath]; - _nodeSizes[[self indexForIndexPath:indexPath]] = [NSValue valueWithCGSize:node.calculatedSize]; - } - ASDisplayNodeAssert(_nodeSizes.count == _sizedNodeCount, @"logic error"); - - // update the working range - if (ASRangeIsValid(_visibleRange)) { - [self recalculateWorkingRange]; - } - - // delegateify - [_delegate rangeController:self didSizeNodesWithIndexPaths:indexPaths]; - - // kick off the next block - if (_sizedNodeCount < _totalNodeCount) { - [self performSelector:@selector(sizeNextBlock) withObject:NULL afterDelay:0]; - } + block(); }); - }); -} - - -#pragma mark - -#pragma mark Editing. - -static BOOL ASIndexPathsAreSequential(NSIndexPath *first, NSIndexPath *second) -{ - BOOL row = (second.row == first.row + 1 && second.section == first.section); - BOOL section = (second.row == 0 && second.section == first.section + 1); - return row || section; -} - -- (void)appendNodesWithIndexPaths:(NSArray *)indexPaths -{ - // sanity-check input - // TODO this is proof-of-concept-quality, expand validation when fleshing out update / editing support - NSIndexPath *lastNode = (_totalNodeCount > 0) ? [self indexPathForIndex:_totalNodeCount - 1] : nil; - BOOL indexPathsAreValid = ((lastNode && ASIndexPathsAreSequential(lastNode, [indexPaths firstObject])) || - [[indexPaths firstObject] isEqual:[NSIndexPath indexPathForRow:0 inSection:0]]); - if (!indexPaths || !indexPaths.count || !indexPathsAreValid) { - ASDisplayNodeAssert(NO, @"invalid argument"); - return; - } - - // update all the things - void (^updateBlock)() = ^{ - BOOL isSizing = (_sizedNodeCount < _totalNodeCount); - NSInteger expectedTotalNodeCount = _totalNodeCount + indexPaths.count; - - [self recalculateDataSourceCounts]; - ASDisplayNodeAssert(_totalNodeCount == expectedTotalNodeCount, @"data source error"); - - if (!isSizing) { - // the last sizing pass completely finished, start a new one - [self sizeNextBlock]; - } - }; - - // trampoline to main if necessary, we don't have locks on _sectionCounts / _sectionOffsets / _totalNodeCount - if (![NSThread isMainThread]) { - dispatch_sync(dispatch_get_main_queue(), ^{ - updateBlock(); - }); - } else { - updateBlock(); } } +- (void)dataController:(ASDataController *)dataController didInsertNodes:(NSArray *)nodes atIndexPaths:(NSArray *)indexPaths { + ASDisplayNodeAssert(nodes.count == indexPaths.count, @"Invalid index path"); + + NSMutableArray *nodeSizes = [NSMutableArray arrayWithCapacity:nodes.count]; + [nodes enumerateObjectsUsingBlock:^(ASCellNode *node, NSUInteger idx, BOOL *stop) { + [nodeSizes addObject:[NSValue valueWithCGSize:node.calculatedSize]]; + }]; + + [self updateOnMainThreadWithBlock:^{ + [_layoutController insertNodesAtIndexPaths:indexPaths withSizes:nodeSizes]; + [_delegate rangeController:self didInsertNodesAtIndexPaths:indexPaths]; + }]; +} + +- (void)dataController:(ASDataController *)dataController didDeleteNodesAtIndexPaths:(NSArray *)indexPaths { + [self updateOnMainThreadWithBlock:^{ + [_layoutController deleteNodesAtIndexPaths:indexPaths]; + [_delegate rangeController:self didDeleteNodesAtIndexPaths:indexPaths]; + }]; +} + +- (void)dataController:(ASDataController *)dataController didInsertSectionsAtIndexSet:(NSIndexSet *)indexSet { + [self updateOnMainThreadWithBlock:^{ + [_layoutController insertSectionsAtIndexSet:indexSet]; + [_delegate rangeController:self didInsertSectionsAtIndexSet:indexSet]; + }]; +} + +- (void)dataController:(ASDataController *)dataController didDeleteSectionsAtIndexSet:(NSIndexSet *)indexSet { + [self updateOnMainThreadWithBlock:^{ + [_layoutController deleteSectionsAtIndexSet:indexSet]; + [_delegate rangeController:self didInsertSectionsAtIndexSet:indexSet]; + }]; +} @end diff --git a/AsyncDisplayKit/Private/ASDisplayNode+AsyncDisplay.mm b/AsyncDisplayKit/Private/ASDisplayNode+AsyncDisplay.mm index 9fb21dad84..3e906a8225 100644 --- a/AsyncDisplayKit/Private/ASDisplayNode+AsyncDisplay.mm +++ b/AsyncDisplayKit/Private/ASDisplayNode+AsyncDisplay.mm @@ -309,7 +309,7 @@ static void __ASDisplayLayerDecrementConcurrentDisplayCount(BOOL displayIsAsync, if (stretchable) { ASDisplayNodeSetupLayerContentsWithResizableImage(_layer, image); } else { - _layer.contentsScale = self.contentsScale; + _layer.contentsScale = image.scale; _layer.contents = (id)image.CGImage; } [self didDisplayAsyncLayer:self.asyncLayer]; diff --git a/AsyncDisplayKit/Private/ASDisplayNode+UIViewBridge.mm b/AsyncDisplayKit/Private/ASDisplayNode+UIViewBridge.mm index 51f865dffd..b74103e1d0 100644 --- a/AsyncDisplayKit/Private/ASDisplayNode+UIViewBridge.mm +++ b/AsyncDisplayKit/Private/ASDisplayNode+UIViewBridge.mm @@ -392,21 +392,21 @@ - (UIColor *)tintColor { - _bridge_prologue; - ASDisplayNodeAssert(!_flags.layerBacked, @"Danger: this property is undefined on layer-backed nodes."); - return _getFromViewOnly(tintColor); + _bridge_prologue; + ASDisplayNodeAssert(!_flags.layerBacked, @"Danger: this property is undefined on layer-backed nodes."); + return _getFromViewOnly(tintColor); } - (void)setTintColor:(UIColor *)color { - _bridge_prologue; - ASDisplayNodeAssert(!_flags.layerBacked, @"Danger: this property is undefined on layer-backed nodes."); - _setToViewOnly(tintColor, color); + _bridge_prologue; + ASDisplayNodeAssert(!_flags.layerBacked, @"Danger: this property is undefined on layer-backed nodes."); + _setToViewOnly(tintColor, color); } - (void)tintColorDidChange { - // ignore this, allow subclasses to be notified + // ignore this, allow subclasses to be notified } - (CGColorRef)shadowColor diff --git a/AsyncDisplayKit/Private/ASRangeControllerInternal.h b/AsyncDisplayKit/Private/ASRangeControllerInternal.h deleted file mode 100644 index c3b71b9451..0000000000 --- a/AsyncDisplayKit/Private/ASRangeControllerInternal.h +++ /dev/null @@ -1,15 +0,0 @@ -/* 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 "ASCellNode.h" - -@interface ASCellNode (ASRangeController) - -@property (nonatomic, copy) NSIndexPath *asyncdisplaykit_indexPath; - -@end diff --git a/Base/ASAssert.h b/Base/ASAssert.h index 33a198d569..84fed6977f 100644 --- a/Base/ASAssert.h +++ b/Base/ASAssert.h @@ -28,6 +28,7 @@ #define ASDisplayNodeAssertImplementedBySubclass() ASDisplayNodeAssertWithSignal(NO, nil, @"This method must be implemented by subclass %@", [self class]); #define ASDisplayNodeAssertNotInstantiable() ASDisplayNodeAssertWithSignal(NO, nil, @"This class is not instantiable."); +#define ASDisplayNodeAssertNotSupported() ASDisplayNodeAssertWithSignal(NO, nil, @"This method is not supported by class %@", [self class]); #define ASDisplayNodeAssertMainThread() ASDisplayNodeAssertWithSignal([NSThread isMainThread], nil, @"This method must be called on the main thread") #define ASDisplayNodeCAssertMainThread() ASDisplayNodeCAssertWithSignal([NSThread isMainThread], nil, @"This function must be called on the main thread") diff --git a/examples/Kittens/Sample/KittenNode.m b/examples/Kittens/Sample/KittenNode.m index 34d02c8bf4..6b9b6f085a 100644 --- a/examples/Kittens/Sample/KittenNode.m +++ b/examples/Kittens/Sample/KittenNode.m @@ -79,7 +79,6 @@ static const CGFloat kInnerPadding = 10.0f; _imageNode.URL = [NSURL URLWithString:[NSString stringWithFormat:@"http://placekitten.com/%zd/%zd", (NSInteger)roundl(_kittenSize.width), (NSInteger)roundl(_kittenSize.height)]]; -// _imageNode.contentMode = UIViewContentModeCenter; [self addSubnode:_imageNode]; // lorem ipsum text, plus some nice styling