mirror of
https://github.com/Swiftgram/Telegram-iOS.git
synced 2025-12-24 07:05:35 +00:00
Support editing in table view and collection view
This commit is contained in:
@@ -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 = "<group>"; };
|
||||
052EE06A1A15A0D8002C6279 /* TestResources */ = {isa = PBXFileReference; lastKnownFileType = folder; path = TestResources; sourceTree = "<group>"; };
|
||||
053011A719B9882B00A9F2D0 /* ASRangeControllerInternal.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = ASRangeControllerInternal.h; sourceTree = "<group>"; };
|
||||
0540322A1A37B69C00001D02 /* ASEqualityHelpers.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = ASEqualityHelpers.h; sourceTree = "<group>"; };
|
||||
054963471A1EA066000F8E56 /* ASBasicImageDownloader.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = ASBasicImageDownloader.h; sourceTree = "<group>"; };
|
||||
054963481A1EA066000F8E56 /* ASBasicImageDownloader.mm */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.cpp.objcpp; path = ASBasicImageDownloader.mm; sourceTree = "<group>"; };
|
||||
055B9FA61A1C154B00035D6D /* ASNetworkImageNode.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = ASNetworkImageNode.h; sourceTree = "<group>"; };
|
||||
@@ -269,8 +277,14 @@
|
||||
05A6D05819D0EB64002DD95E /* ASDealloc2MainObject.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; name = ASDealloc2MainObject.h; path = ../Details/ASDealloc2MainObject.h; sourceTree = "<group>"; };
|
||||
05A6D05919D0EB64002DD95E /* ASDealloc2MainObject.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; name = ASDealloc2MainObject.m; path = ../Details/ASDealloc2MainObject.m; sourceTree = "<group>"; };
|
||||
05F20AA31A15733C00DCA68A /* ASImageProtocols.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = ASImageProtocols.h; sourceTree = "<group>"; };
|
||||
1950C4481A3BB5C1005C8279 /* ASEqualityHelpers.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = ASEqualityHelpers.h; sourceTree = "<group>"; };
|
||||
3C9C128419E616EF00E942A0 /* ASTableViewTests.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = ASTableViewTests.m; sourceTree = "<group>"; };
|
||||
464052191A3F83C40061C0BA /* ASDataController.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = ASDataController.h; sourceTree = "<group>"; };
|
||||
4640521A1A3F83C40061C0BA /* ASDataController.mm */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.cpp.objcpp; path = ASDataController.mm; sourceTree = "<group>"; };
|
||||
4640521B1A3F83C40061C0BA /* ASFlowLayoutController.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = ASFlowLayoutController.h; sourceTree = "<group>"; };
|
||||
4640521C1A3F83C40061C0BA /* ASFlowLayoutController.mm */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.cpp.objcpp; path = ASFlowLayoutController.mm; sourceTree = "<group>"; };
|
||||
4640521D1A3F83C40061C0BA /* ASLayoutController.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = ASLayoutController.h; sourceTree = "<group>"; };
|
||||
4640521E1A3F83C40061C0BA /* ASMultidimensionalArrayUtils.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = ASMultidimensionalArrayUtils.h; sourceTree = "<group>"; };
|
||||
4640521F1A3F83C40061C0BA /* ASMultidimensionalArrayUtils.mm */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.cpp.objcpp; path = ASMultidimensionalArrayUtils.mm; sourceTree = "<group>"; };
|
||||
6BDC61F51978FEA400E50D21 /* AsyncDisplayKit.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = AsyncDisplayKit.h; sourceTree = "<group>"; };
|
||||
AC3C4A4F1A1139C100143C57 /* ASCollectionView.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = ASCollectionView.h; sourceTree = "<group>"; };
|
||||
AC3C4A501A1139C100143C57 /* ASCollectionView.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = ASCollectionView.m; sourceTree = "<group>"; };
|
||||
@@ -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 */,
|
||||
|
||||
@@ -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
|
||||
|
||||
|
||||
|
||||
@@ -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.
|
||||
|
||||
@@ -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 () <ASRangeControllerDelegate> {
|
||||
@interface ASCollectionView () <ASRangeControllerDelegate, ASDataControllerSource> {
|
||||
_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
|
||||
|
||||
@@ -13,8 +13,8 @@
|
||||
#import <objc/runtime.h>
|
||||
|
||||
#import "_ASAsyncTransaction.h"
|
||||
#import "_ASPendingState.h"
|
||||
#import "_ASDisplayView.h"
|
||||
#import "_ASPendingState.h"
|
||||
#import "_ASScopeTimer.h"
|
||||
#import "ASDisplayNodeExtras.h"
|
||||
|
||||
|
||||
@@ -181,8 +181,7 @@
|
||||
|| alphaInfo == kCGImageAlphaPremultipliedLast;
|
||||
|
||||
BOOL contentModeSupported = contentMode == UIViewContentModeScaleAspectFill
|
||||
|| contentMode == UIViewContentModeScaleAspectFit
|
||||
|| contentMode == UIViewContentModeCenter;
|
||||
|| contentMode == UIViewContentModeScaleAspectFit;
|
||||
|
||||
CGSize backingSize;
|
||||
CGRect imageDrawRect;
|
||||
|
||||
@@ -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
|
||||
|
||||
|
||||
@@ -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 () <ASRangeControllerDelegate> {
|
||||
@interface ASTableView () <ASRangeControllerDelegate, ASDataControllerSource> {
|
||||
_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<UITableViewDataSource>)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
|
||||
|
||||
@@ -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]];
|
||||
|
||||
125
AsyncDisplayKit/Details/ASDataController.h
Normal file
125
AsyncDisplayKit/Details/ASDataController.h
Normal file
@@ -0,0 +1,125 @@
|
||||
// Copyright 2004-present Facebook. All Rights Reserved.
|
||||
|
||||
#import <UIKit/UIKit.h>
|
||||
#import <AsyncDisplayKit/ASDealloc2MainObject.h>
|
||||
|
||||
@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 <NSObject>
|
||||
|
||||
/**
|
||||
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 <NSObject>
|
||||
|
||||
@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<ASDataControllerSource> dataSource;
|
||||
|
||||
/**
|
||||
Delegate to notify when data is updated.
|
||||
*/
|
||||
@property (nonatomic, weak) id<ASDataControllerDelegate> 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
|
||||
474
AsyncDisplayKit/Details/ASDataController.mm
Normal file
474
AsyncDisplayKit/Details/ASDataController.mm
Normal file
@@ -0,0 +1,474 @@
|
||||
// Copyright 2004-present Facebook. All Rights Reserved.
|
||||
|
||||
#import "ASDataController.h"
|
||||
|
||||
#import <Foundation/NSProcessInfo.h>
|
||||
|
||||
#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
|
||||
21
AsyncDisplayKit/Details/ASFlowLayoutController.h
Normal file
21
AsyncDisplayKit/Details/ASFlowLayoutController.h
Normal file
@@ -0,0 +1,21 @@
|
||||
// Copyright 2004-present Facebook. All Rights Reserved.
|
||||
|
||||
#import <AsyncDisplayKit/ASLayoutController.h>
|
||||
|
||||
typedef NS_ENUM(NSUInteger, ASFlowLayoutDirection) {
|
||||
ASFlowLayoutDirectionVertical,
|
||||
ASFlowLayoutDirectionHorizontal,
|
||||
};
|
||||
|
||||
/**
|
||||
* The controller for flow layout.
|
||||
*/
|
||||
@interface ASFlowLayoutController : NSObject <ASLayoutController>
|
||||
|
||||
@property (nonatomic, assign) ASRangeTuningParameters tuningParameters;
|
||||
|
||||
@property (nonatomic, readonly, assign) ASFlowLayoutDirection layoutDirection;
|
||||
|
||||
- (instancetype)initWithScrollOption:(ASFlowLayoutDirection)layoutDirection;
|
||||
|
||||
@end
|
||||
200
AsyncDisplayKit/Details/ASFlowLayoutController.mm
Normal file
200
AsyncDisplayKit/Details/ASFlowLayoutController.mm
Normal file
@@ -0,0 +1,200 @@
|
||||
// Copyright 2004-present Facebook. All Rights Reserved.
|
||||
|
||||
#import "ASFlowLayoutController.h"
|
||||
|
||||
#include <map>
|
||||
#include <vector>
|
||||
#include <cassert>
|
||||
|
||||
#import "ASAssert.h"
|
||||
|
||||
static const CGFloat kASFlowLayoutControllerRefreshingThreshold = 0.3;
|
||||
|
||||
@interface ASFlowLayoutController() {
|
||||
std::vector<std::vector<CGSize> > _nodeSizes;
|
||||
|
||||
std::pair<int, int> _visibleRangeStartPos;
|
||||
std::pair<int, int> _visibleRangeEndPos;
|
||||
|
||||
std::pair<int, int> _workingRangeStartPos;
|
||||
std::pair<int, int> _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<CGSize> &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<CGSize> &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<CGSize>());
|
||||
}];
|
||||
}
|
||||
|
||||
- (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<int, int> 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<int, int> 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<int, int> &startPos, std::pair<int, int> &endPos) {
|
||||
NSIndexPath *initialIndexPath = [indexPaths firstObject];
|
||||
startPos = endPos = {initialIndexPath.section, initialIndexPath.row};
|
||||
[indexPaths enumerateObjectsUsingBlock:^(NSIndexPath *indexPath, NSUInteger idx, BOOL *stop) {
|
||||
std::pair<int, int> p(indexPath.section, indexPath.row);
|
||||
startPos = MIN(startPos, p);
|
||||
endPos = MAX(endPos, p);
|
||||
}];
|
||||
}
|
||||
|
||||
static const std::pair<int, int> ASFindIndexForRange(const std::vector<std::vector<CGSize>> &nodes,
|
||||
const std::pair<int, int> &pos,
|
||||
CGFloat range,
|
||||
ASFlowLayoutDirection layoutDirection) {
|
||||
std::pair<int, int> 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<int, int> &start, const std::pair<int, int> &end, const std::vector<std::vector<CGSize>> &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
|
||||
42
AsyncDisplayKit/Details/ASLayoutController.h
Normal file
42
AsyncDisplayKit/Details/ASLayoutController.h
Normal file
@@ -0,0 +1,42 @@
|
||||
// Copyright 2004-present Facebook. All Rights Reserved.
|
||||
|
||||
#import <UIKit/UIKit.h>
|
||||
|
||||
typedef struct {
|
||||
CGFloat leadingBufferScreenfuls;
|
||||
CGFloat trailingBufferScreenfuls;
|
||||
} ASRangeTuningParameters;
|
||||
|
||||
typedef NS_ENUM(NSInteger, ASScrollDirection) {
|
||||
ASScrollDirectionNone,
|
||||
ASScrollDirectionRight,
|
||||
ASScrollDirectionLeft,
|
||||
ASScrollDirectionUp,
|
||||
ASScrollDirectionDown,
|
||||
};
|
||||
|
||||
@protocol ASLayoutController <NSObject>
|
||||
|
||||
/**
|
||||
* 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
|
||||
46
AsyncDisplayKit/Details/ASMultidimensionalArrayUtils.h
Normal file
46
AsyncDisplayKit/Details/ASMultidimensionalArrayUtils.h
Normal file
@@ -0,0 +1,46 @@
|
||||
// Copyright 2004-present Facebook. All Rights Reserved.
|
||||
|
||||
#import <Foundation/Foundation.h>
|
||||
|
||||
#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<NSCopying> *ASMultidimensionalArrayDeepMutableCopy(NSObject<NSCopying> *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
|
||||
|
||||
119
AsyncDisplayKit/Details/ASMultidimensionalArrayUtils.mm
Normal file
119
AsyncDisplayKit/Details/ASMultidimensionalArrayUtils.mm
Normal file
@@ -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<NSCopying> *ASMultidimensionalArrayDeepMutableCopy(NSObject<NSCopying> *obj) {
|
||||
if ([obj isKindOfClass:[NSArray class]]) {
|
||||
NSArray *arr = (NSArray *)obj;
|
||||
NSMutableArray * mutableArr = [NSMutableArray array];
|
||||
for (NSObject<NSCopying> *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;
|
||||
}
|
||||
|
||||
@@ -9,19 +9,9 @@
|
||||
#import <Foundation/Foundation.h>
|
||||
|
||||
#import <AsyncDisplayKit/ASCellNode.h>
|
||||
|
||||
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 <AsyncDisplayKit/ASDataController.h>
|
||||
#import <AsyncDisplayKit/ASFlowLayoutController.h>
|
||||
#import <AsyncDisplayKit/ASLayoutController.h>
|
||||
|
||||
@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 <ASDataControllerDelegate>
|
||||
|
||||
/**
|
||||
* 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<ASRangeControllerDelegate> 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<ASLayoutController> 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 <ASTableView> 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 <ASTableView> 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 <ASCellNode> corresponding to `indexPath`.
|
||||
*
|
||||
* @discussion <ASTableView> 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
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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];
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
@@ -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")
|
||||
|
||||
@@ -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
|
||||
|
||||
Reference in New Issue
Block a user