diff --git a/.gitignore b/.gitignore index aee909766f..84de6767f4 100644 --- a/.gitignore +++ b/.gitignore @@ -26,3 +26,5 @@ docs/.sass-cache *.lock *.gcov +*.gcno +*.gcda diff --git a/AsyncDisplayKit-Prefix.gcda b/AsyncDisplayKit-Prefix.gcda new file mode 100644 index 0000000000..450c9162de Binary files /dev/null and b/AsyncDisplayKit-Prefix.gcda differ diff --git a/AsyncDisplayKit.xcodeproj/project.pbxproj b/AsyncDisplayKit.xcodeproj/project.pbxproj index 1a58fabfe9..f096375ce4 100644 --- a/AsyncDisplayKit.xcodeproj/project.pbxproj +++ b/AsyncDisplayKit.xcodeproj/project.pbxproj @@ -140,6 +140,12 @@ 205F0E211B376416007741D0 /* CGRect+ASConvenience.h in Headers */ = {isa = PBXBuildFile; fileRef = 205F0E1F1B376416007741D0 /* CGRect+ASConvenience.h */; settings = {ATTRIBUTES = (Public, ); }; }; 205F0E221B376416007741D0 /* CGRect+ASConvenience.m in Sources */ = {isa = PBXBuildFile; fileRef = 205F0E201B376416007741D0 /* CGRect+ASConvenience.m */; }; 242995D31B29743C00090100 /* ASBasicImageDownloaderTests.m in Sources */ = {isa = PBXBuildFile; fileRef = 242995D21B29743C00090100 /* ASBasicImageDownloaderTests.m */; }; + 251B8EF71BBB3D690087C538 /* ASCollectionDataController.h in Headers */ = {isa = PBXBuildFile; fileRef = 251B8EF21BBB3D690087C538 /* ASCollectionDataController.h */; settings = {ASSET_TAGS = (); }; }; + 251B8EF81BBB3D690087C538 /* ASCollectionDataController.mm in Sources */ = {isa = PBXBuildFile; fileRef = 251B8EF31BBB3D690087C538 /* ASCollectionDataController.mm */; settings = {ASSET_TAGS = (); }; }; + 251B8EF91BBB3D690087C538 /* ASCollectionViewFlowLayoutInspector.h in Headers */ = {isa = PBXBuildFile; fileRef = 251B8EF41BBB3D690087C538 /* ASCollectionViewFlowLayoutInspector.h */; settings = {ASSET_TAGS = (); }; }; + 251B8EFA1BBB3D690087C538 /* ASCollectionViewFlowLayoutInspector.m in Sources */ = {isa = PBXBuildFile; fileRef = 251B8EF51BBB3D690087C538 /* ASCollectionViewFlowLayoutInspector.m */; settings = {ASSET_TAGS = (); }; }; + 251B8EFB1BBB3D690087C538 /* ASDataController+Subclasses.h in Headers */ = {isa = PBXBuildFile; fileRef = 251B8EF61BBB3D690087C538 /* ASDataController+Subclasses.h */; settings = {ASSET_TAGS = (); }; }; + 2538B6F31BC5D2A2003CA0B4 /* ASCollectionViewFlowLayoutInspectorTests.m in Sources */ = {isa = PBXBuildFile; fileRef = 2538B6F21BC5D2A2003CA0B4 /* ASCollectionViewFlowLayoutInspectorTests.m */; settings = {ASSET_TAGS = (); }; }; 2767E9411BB19BD600EA9B77 /* ASViewController.h in Headers */ = {isa = PBXBuildFile; fileRef = ACC945A81BA9E7A0005E1FB8 /* ASViewController.h */; settings = {ATTRIBUTES = (Public, ); }; }; 2767E9421BB19BD600EA9B77 /* ASViewController.m in Sources */ = {isa = PBXBuildFile; fileRef = ACC945AA1BA9E7C1005E1FB8 /* ASViewController.m */; settings = {ASSET_TAGS = (); }; }; 2911485C1A77147A005D0878 /* ASControlNodeTests.m in Sources */ = {isa = PBXBuildFile; fileRef = 2911485B1A77147A005D0878 /* ASControlNodeTests.m */; }; @@ -206,6 +212,8 @@ 509E68651B3AEDC5009B9150 /* CGRect+ASConvenience.h in Headers */ = {isa = PBXBuildFile; fileRef = 205F0E1F1B376416007741D0 /* CGRect+ASConvenience.h */; settings = {ATTRIBUTES = (Public, ); }; }; 509E68661B3AEDD7009B9150 /* CGRect+ASConvenience.m in Sources */ = {isa = PBXBuildFile; fileRef = 205F0E201B376416007741D0 /* CGRect+ASConvenience.m */; }; 6BDC61F61979037800E50D21 /* AsyncDisplayKit.h in Headers */ = {isa = PBXBuildFile; fileRef = 6BDC61F51978FEA400E50D21 /* AsyncDisplayKit.h */; settings = {ATTRIBUTES = (Public, ); }; }; + 9B92C8851BC2EB6E00EE46B2 /* ASCollectionDataController.mm in Sources */ = {isa = PBXBuildFile; fileRef = 251B8EF31BBB3D690087C538 /* ASCollectionDataController.mm */; }; + 9B92C8861BC2EB7600EE46B2 /* ASCollectionViewFlowLayoutInspector.m in Sources */ = {isa = PBXBuildFile; fileRef = 251B8EF51BBB3D690087C538 /* ASCollectionViewFlowLayoutInspector.m */; }; 9C49C36F1B853957000B0DD5 /* ASStackLayoutable.h in Headers */ = {isa = PBXBuildFile; fileRef = 9C49C36E1B853957000B0DD5 /* ASStackLayoutable.h */; settings = {ATTRIBUTES = (Public, ); }; }; 9C49C3701B853961000B0DD5 /* ASStackLayoutable.h in Headers */ = {isa = PBXBuildFile; fileRef = 9C49C36E1B853957000B0DD5 /* ASStackLayoutable.h */; settings = {ATTRIBUTES = (Public, ); }; }; 9C5FA3511B8F6ADF00A62714 /* ASLayoutOptions.h in Headers */ = {isa = PBXBuildFile; fileRef = 9C5FA34F1B8F6ADF00A62714 /* ASLayoutOptions.h */; settings = {ATTRIBUTES = (Public, ); }; }; @@ -544,6 +552,12 @@ 205F0E1F1B376416007741D0 /* CGRect+ASConvenience.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = "CGRect+ASConvenience.h"; sourceTree = ""; }; 205F0E201B376416007741D0 /* CGRect+ASConvenience.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = "CGRect+ASConvenience.m"; sourceTree = ""; }; 242995D21B29743C00090100 /* ASBasicImageDownloaderTests.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = ASBasicImageDownloaderTests.m; sourceTree = ""; }; + 251B8EF21BBB3D690087C538 /* ASCollectionDataController.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = ASCollectionDataController.h; sourceTree = ""; }; + 251B8EF31BBB3D690087C538 /* ASCollectionDataController.mm */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.cpp.objcpp; path = ASCollectionDataController.mm; sourceTree = ""; }; + 251B8EF41BBB3D690087C538 /* ASCollectionViewFlowLayoutInspector.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = ASCollectionViewFlowLayoutInspector.h; sourceTree = ""; }; + 251B8EF51BBB3D690087C538 /* ASCollectionViewFlowLayoutInspector.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = ASCollectionViewFlowLayoutInspector.m; sourceTree = ""; }; + 251B8EF61BBB3D690087C538 /* ASDataController+Subclasses.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = "ASDataController+Subclasses.h"; sourceTree = ""; }; + 2538B6F21BC5D2A2003CA0B4 /* ASCollectionViewFlowLayoutInspectorTests.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = ASCollectionViewFlowLayoutInspectorTests.m; sourceTree = ""; }; 2911485B1A77147A005D0878 /* ASControlNodeTests.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = ASControlNodeTests.m; sourceTree = ""; }; 292C59991A956527007E5DD6 /* ASLayoutRangeType.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = ASLayoutRangeType.h; sourceTree = ""; }; 292C599A1A956527007E5DD6 /* ASRangeHandlerPreload.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = ASRangeHandlerPreload.h; sourceTree = ""; }; @@ -829,6 +843,7 @@ 058D0A37195D057000B7D73C /* ASTextNodeWordKernerTests.mm */, 058D09C6195D04C000B7D73C /* Supporting Files */, 052EE06A1A15A0D8002C6279 /* TestResources */, + 2538B6F21BC5D2A2003CA0B4 /* ASCollectionViewFlowLayoutInspectorTests.m */, ); path = AsyncDisplayKitTests; sourceTree = ""; @@ -847,6 +862,11 @@ children = ( CC7FD9DC1BB5E962005CCB2B /* ASPhotosFrameworkImageRequest.h */, CC7FD9DD1BB5E962005CCB2B /* ASPhotosFrameworkImageRequest.m */, + 251B8EF21BBB3D690087C538 /* ASCollectionDataController.h */, + 251B8EF31BBB3D690087C538 /* ASCollectionDataController.mm */, + 251B8EF41BBB3D690087C538 /* ASCollectionViewFlowLayoutInspector.h */, + 251B8EF51BBB3D690087C538 /* ASCollectionViewFlowLayoutInspector.m */, + 251B8EF61BBB3D690087C538 /* ASDataController+Subclasses.h */, 058D09E2195D050800B7D73C /* _ASDisplayLayer.h */, 058D09E3195D050800B7D73C /* _ASDisplayLayer.mm */, 058D09E4195D050800B7D73C /* _ASDisplayView.h */, @@ -1062,6 +1082,7 @@ 054963491A1EA066000F8E56 /* ASBasicImageDownloader.h in Headers */, 2967F9E21AB0A5190072E4AB /* ASBasicImageDownloaderInternal.h in Headers */, 299DA1A91A828D2900162D41 /* ASBatchContext.h in Headers */, + 251B8EF91BBB3D690087C538 /* ASCollectionViewFlowLayoutInspector.h in Headers */, 044285071BAA63FE00D16268 /* ASBatchFetching.h in Headers */, 055F1A3C19ABD43F004DAFF1 /* ASCellNode.h in Headers */, ACF6ED1C1B17843500DA7C62 /* ASCenterLayoutSpec.h in Headers */, @@ -1091,6 +1112,7 @@ ACF6ED221B17843500DA7C62 /* ASInsetLayoutSpec.h in Headers */, ACF6ED4B1B17847A00DA7C62 /* ASInternalHelpers.h in Headers */, ACF6ED241B17843500DA7C62 /* ASLayout.h in Headers */, + 251B8EFB1BBB3D690087C538 /* ASDataController+Subclasses.h in Headers */, ACF6ED2A1B17843500DA7C62 /* ASLayoutable.h in Headers */, 9CDC18CC1B910E12004965E2 /* ASLayoutablePrivate.h in Headers */, 464052241A3F83C40061C0BA /* ASLayoutController.h in Headers */, @@ -1125,6 +1147,7 @@ 9C6BB3B21B8CC9C200F13F52 /* ASStaticLayoutable.h in Headers */, ACF6ED311B17843500DA7C62 /* ASStaticLayoutSpec.h in Headers */, 055F1A3419ABD3E3004DAFF1 /* ASTableView.h in Headers */, + 251B8EF71BBB3D690087C538 /* ASCollectionDataController.h in Headers */, 0574D5E219C110940097DC25 /* ASTableViewProtocols.h in Headers */, 058D0A51195D05CB00B7D73C /* ASTextNode.h in Headers */, 058D0A5B195D05DC00B7D73C /* ASTextNodeCoreTextAdditions.h in Headers */, @@ -1474,6 +1497,7 @@ ACF6ED251B17843500DA7C62 /* ASLayout.mm in Sources */, 9C5FA3531B8F6ADF00A62714 /* ASLayoutOptions.mm in Sources */, 9C5FA35F1B90C9A500A62714 /* ASLayoutOptionsPrivate.mm in Sources */, + 251B8EFA1BBB3D690087C538 /* ASCollectionViewFlowLayoutInspector.m in Sources */, ACF6ED271B17843500DA7C62 /* ASLayoutSpec.mm in Sources */, 0516FA411A1563D200B4EBED /* ASMultiplexImageNode.mm in Sources */, 058D0A1B195D050800B7D73C /* ASMutableAttributedStringBuilder.m in Sources */, @@ -1490,6 +1514,7 @@ D785F6631A74327E00291744 /* ASScrollNode.m in Sources */, 058D0A2C195D050800B7D73C /* ASSentinel.m in Sources */, 9C8221971BA237B80037F19A /* ASStackBaselinePositionedLayout.mm in Sources */, + 251B8EF81BBB3D690087C538 /* ASCollectionDataController.mm in Sources */, ACF6ED301B17843500DA7C62 /* ASStackLayoutSpec.mm in Sources */, ACF6ED501B17847A00DA7C62 /* ASStackPositionedLayout.mm in Sources */, ACF6ED521B17847A00DA7C62 /* ASStackUnpositionedLayout.mm in Sources */, @@ -1522,6 +1547,7 @@ 2911485C1A77147A005D0878 /* ASControlNodeTests.m in Sources */, ACF6ED5D1B178DC700DA7C62 /* ASDimensionTests.mm in Sources */, 058D0A38195D057000B7D73C /* ASDisplayLayerTests.m in Sources */, + 2538B6F31BC5D2A2003CA0B4 /* ASCollectionViewFlowLayoutInspectorTests.m in Sources */, 058D0A39195D057000B7D73C /* ASDisplayNodeAppearanceTests.m in Sources */, 058D0A3A195D057000B7D73C /* ASDisplayNodeTests.m in Sources */, 058D0A3B195D057000B7D73C /* ASDisplayNodeTestsHelper.m in Sources */, @@ -1548,6 +1574,8 @@ isa = PBXSourcesBuildPhase; buildActionMask = 2147483647; files = ( + 9B92C8861BC2EB7600EE46B2 /* ASCollectionViewFlowLayoutInspector.m in Sources */, + 9B92C8851BC2EB6E00EE46B2 /* ASCollectionDataController.mm in Sources */, B350623D1B010EFD0018CF92 /* _ASAsyncTransaction.m in Sources */, B35062401B010EFD0018CF92 /* _ASAsyncTransactionContainer.m in Sources */, B35062421B010EFD0018CF92 /* _ASAsyncTransactionGroup.m in Sources */, diff --git a/AsyncDisplayKit/ASCollectionView.h b/AsyncDisplayKit/ASCollectionView.h index d8f78c6e13..14ec90f894 100644 --- a/AsyncDisplayKit/ASCollectionView.h +++ b/AsyncDisplayKit/ASCollectionView.h @@ -17,7 +17,7 @@ @class ASCellNode; @protocol ASCollectionViewDataSource; @protocol ASCollectionViewDelegate; - +@protocol ASCollectionViewLayoutInspecting; /** * Node-based collection view. @@ -63,7 +63,7 @@ * * @param asyncDataFetchingEnabled Enable the data fetching in async mode. * - * @discussion If asyncDataFetching is enabled, the `AScollectionView` will fetch data through `collectionView:numberOfRowsInSection:` and + * @discussion If asyncDataFetching is enabled, the `ASCollectionView` will fetch data through `collectionView:numberOfRowsInSection:` and * `collectionView:nodeForRowAtIndexPath:` in async mode from background thread. Otherwise, the methods will be invoked synchronically * from calling thread. * Enabling asyncDataFetching could avoid blocking main thread for `ASCellNode` allocation, which is frequently reported issue for @@ -80,6 +80,18 @@ */ @property (nonatomic, assign) CGFloat leadingScreensForBatching; +/** + * Optional introspection object for the collection view's layout. + * + * @discussion Since supplementary and decoration views are controlled by the collection view's layout, this object + * is used as a bridge to provide information to the internal data controller about the existence of these views and + * their associated index paths. For collection views using `UICollectionViewFlowLayout`, a default inspector + * implementation `ASCollectionViewFlowLayoutInspector` is created and set on this property by default. Custom + * collection view layout subclasses will need to provide their own implementation of an inspector object for their + * supplementary views to be compatible with `ASCollectionView`'s supplementary node support. + */ +@property (nonatomic, weak) id layoutDelegate; + /** * Perform a batch of updates asynchronously, optionally disabling all animations in the batch. This method must be called from the main thread. * The asyncDataSource must be updated to reflect the changes before the update block completes. @@ -119,6 +131,18 @@ */ - (void)reloadData; +/** + * Registers the given kind of supplementary node for use in creating node-backed supplementary views. + * + * @param kind The kind of supplementary node that will be requested through the data source. + * + * @discussion Use this method to register support for the use of supplementary nodes in place of the default + * `registerClass:forSupplementaryViewOfKind:withReuseIdentifier:` and `registerNib:forSupplementaryViewOfKind:withReuseIdentifier:` + * methods. This method will register an internal backing view that will host the contents of the supplementary nodes + * returned from the data source. + */ +- (void)registerSupplementaryNodeOfKind:(NSString *)elementKind; + /** * Inserts one or more sections. * @@ -272,6 +296,15 @@ @optional +/** + * Asks the collection view to provide a supplementary node to display in the collection view. + * + * @param collectionView An object representing the collection view requesting this information. + * @param kind The kind of supplementary node to provide. + * @param indexPath The index path that specifies the location of the new supplementary node. + */ +- (ASCellNode *)collectionView:(ASCollectionView *)collectionView nodeForSupplementaryElementOfKind:(NSString *)kind atIndexPath:(NSIndexPath *)indexPath; + /** * Provides the constrained size range for measuring the node at the index path. * @@ -342,18 +375,37 @@ */ - (BOOL)shouldBatchFetchForCollectionView:(ASCollectionView *)collectionView; +@end + +/** + * Defines methods that let you coordinate with a `UICollectionViewFlowLayout` in combination with an `ASCollectionView`. + */ +@protocol ASCollectionViewDelegateFlowLayout + +@optional + /** * Passthrough support to UICollectionViewDelegateFlowLayout sectionInset behavior. * * @param collectionView The sender. * @param collectionViewLayout The layout object requesting the information. - * #param section The index number of the section whose insets are needed. + * @param section The index number of the section whose insets are needed. * * @discussion The same rules apply as the UICollectionView implementation, but this can also be used without a UICollectionViewFlowLayout. * https://developer.apple.com/library/ios/documentation/UIKit/Reference/UICollectionViewDelegateFlowLayout_protocol/index.html#//apple_ref/occ/intfm/UICollectionViewDelegateFlowLayout/collectionView:layout:insetForSectionAtIndex: * */ -- (UIEdgeInsets)collectionView:(UICollectionView *)collectionView layout:(UICollectionViewLayout *)collectionViewLayout insetForSectionAtIndex:(NSInteger)section; +- (UIEdgeInsets)collectionView:(ASCollectionView *)collectionView layout:(UICollectionViewLayout *)collectionViewLayout insetForSectionAtIndex:(NSInteger)section; + +/** + * Asks the delegate for the size of the header in the specified section. + */ +- (CGSize)collectionView:(ASCollectionView *)collectionView layout:(UICollectionViewLayout *)collectionViewLayout referenceSizeForHeaderInSection:(NSInteger)section; + +/** + * Asks the delegate for the size of the footer in the specified section. + */ +- (CGSize)collectionView:(ASCollectionView *)collectionView layout:(UICollectionViewLayout *)collectionViewLayout referenceSizeForFooterInSection:(NSInteger)section; @end diff --git a/AsyncDisplayKit/ASCollectionView.mm b/AsyncDisplayKit/ASCollectionView.mm index 3c1cc4dc18..b312b86c10 100644 --- a/AsyncDisplayKit/ASCollectionView.mm +++ b/AsyncDisplayKit/ASCollectionView.mm @@ -11,11 +11,12 @@ #import "ASAssert.h" #import "ASCollectionViewLayoutController.h" #import "ASRangeController.h" -#import "ASDataController.h" +#import "ASCollectionDataController.h" #import "ASDisplayNodeInternal.h" #import "ASBatchFetching.h" #import "UICollectionViewLayout+ASConvenience.h" #import "ASInternalHelpers.h" +#import "ASCollectionViewFlowLayoutInspector.h" // FIXME: Temporary nonsense import until method names are finalized and exposed #import "ASDisplayNode+Subclasses.h" @@ -37,9 +38,7 @@ static BOOL _isInterceptedSelector(SEL sel) // handled by ASCollectionView node<->cell machinery sel == @selector(collectionView:cellForItemAtIndexPath:) || sel == @selector(collectionView:layout:sizeForItemAtIndexPath:) || - - // TODO: Supplementary views are currently not supported. An assertion is triggered if the _asyncDataSource implements this method. - // sel == @selector(collectionView:viewForSupplementaryElementOfKind:atIndexPath:) || + sel == @selector(collectionView:viewForSupplementaryElementOfKind:atIndexPath:) || // handled by ASRangeController sel == @selector(numberOfSectionsInCollectionView:) || @@ -136,9 +135,10 @@ static BOOL _isInterceptedSelector(SEL sel) _ASCollectionViewProxy *_proxyDataSource; _ASCollectionViewProxy *_proxyDelegate; - ASDataController *_dataController; + ASCollectionDataController *_dataController; ASRangeController *_rangeController; ASCollectionViewLayoutController *_layoutController; + ASCollectionViewFlowLayoutInspector *_flowLayoutInspector; BOOL _performingBatchUpdates; NSMutableArray *_batchUpdateBlocks; @@ -153,6 +153,8 @@ static BOOL _isInterceptedSelector(SEL sel) CGSize _maxSizeForNodesConstrainedSize; BOOL _ignoreMaxSizeChange; + NSMutableArray *_registeredSupplementaryKinds; + /** * If YES, the `UICollectionView` will reload its data on next layout pass so we should not forward any updates to it. @@ -201,10 +203,10 @@ static BOOL _isInterceptedSelector(SEL sel) _rangeController.delegate = self; _rangeController.layoutController = _layoutController; - _dataController = [[ASDataController alloc] initWithAsyncDataFetching:asyncDataFetchingEnabled]; + _dataController = [[ASCollectionDataController alloc] initWithAsyncDataFetching:asyncDataFetchingEnabled]; _dataController.delegate = _rangeController; _dataController.dataSource = self; - + _batchContext = [[ASBatchContext alloc] init]; _leadingScreensForBatching = 1.0; @@ -224,6 +226,14 @@ static BOOL _isInterceptedSelector(SEL sel) // and should not trigger a relayout. _ignoreMaxSizeChange = CGSizeEqualToSize(_maxSizeForNodesConstrainedSize, CGSizeZero); + // Register the default layout inspector delegate for flow layouts only, custom layouts + // will need to roll their own ASCollectionViewLayoutInspecting implementation and set a layout delegate + if ([layout asdk_isFlowLayout]) { + _layoutDelegate = [self flowLayoutInspector]; + } + + _registeredSupplementaryKinds = [NSMutableArray array]; + self.backgroundColor = [UIColor whiteColor]; [self registerClass:[_ASCollectionViewCell class] forCellWithReuseIdentifier:@"_ASCollectionViewCell"]; @@ -239,6 +249,19 @@ static BOOL _isInterceptedSelector(SEL sel) super.dataSource = nil; } +/** + * A layout inspector implementation specific for the sizing behavior of UICollectionViewFlowLayouts + */ +- (ASCollectionViewFlowLayoutInspector *)flowLayoutInspector +{ + if (_flowLayoutInspector == nil) { + UICollectionViewFlowLayout *layout = (UICollectionViewFlowLayout *)self.collectionViewLayout; + _flowLayoutInspector = [[ASCollectionViewFlowLayoutInspector alloc] initWithCollectionView:self + flowLayout:layout]; + } + return _flowLayoutInspector; +} + #pragma mark - #pragma mark Overrides. @@ -283,10 +306,6 @@ static BOOL _isInterceptedSelector(SEL sel) _asyncDataSourceImplementsConstrainedSizeForNode = NO; } else { _asyncDataSource = asyncDataSource; - // TODO: Support supplementary views with ASCollectionView. - if ([_asyncDataSource respondsToSelector:@selector(collectionView:viewForSupplementaryElementOfKind:atIndexPath:)]) { - ASDisplayNodeAssert(NO, @"ASCollectionView is planned to support supplementary views by September 2015. You can work around this issue by using standard items."); - } _proxyDataSource = [[_ASCollectionViewProxy alloc] initWithTarget:_asyncDataSource interceptor:self]; super.dataSource = (id)_proxyDataSource; _asyncDataSourceImplementsConstrainedSizeForNode = ([_asyncDataSource respondsToSelector:@selector(collectionView:constrainedSizeForNodeAtIndexPath:)] ? 1 : 0); @@ -313,6 +332,17 @@ static BOOL _isInterceptedSelector(SEL sel) super.delegate = (id)_proxyDelegate; _asyncDelegateImplementsInsetSection = ([_asyncDelegate respondsToSelector:@selector(collectionView:layout:insetForSectionAtIndex:)] ? 1 : 0); } + + [_flowLayoutInspector cacheSelectorsForCollectionView:self]; +} + +- (void)setCollectionViewLayout:(UICollectionViewLayout *)collectionViewLayout +{ + [super setCollectionViewLayout:collectionViewLayout]; + if ([collectionViewLayout asdk_isFlowLayout]) { + _flowLayoutInspector = nil; + _layoutDelegate = [self flowLayoutInspector]; + } } - (void)setTuningParameters:(ASRangeTuningParameters)tuningParameters forRangeType:(ASLayoutRangeType)rangeType @@ -379,6 +409,14 @@ static BOOL _isInterceptedSelector(SEL sel) [self performBatchAnimated:YES updates:updates completion:completion]; } +- (void)registerSupplementaryNodeOfKind:(NSString *)elementKind +{ + ASDisplayNodeAssert(elementKind != nil, @"A kind is needed for supplementary node registration"); + [_registeredSupplementaryKinds addObject:elementKind]; + [self registerClass:[UICollectionReusableView class] forSupplementaryViewOfKind:elementKind + withReuseIdentifier:[self __reuseIdentifierForKind:elementKind]]; +} + - (void)insertSections:(NSIndexSet *)sections { ASDisplayNodeAssertMainThread(); @@ -427,6 +465,11 @@ static BOOL _isInterceptedSelector(SEL sel) [_dataController moveRowAtIndexPath:indexPath toIndexPath:newIndexPath withAnimationOptions:kASCollectionViewAnimationNone]; } +- (NSString *)__reuseIdentifierForKind:(NSString *)kind +{ + return [@"_ASCollectionSupplementaryView_" stringByAppendingString:kind]; +} + #pragma mark - #pragma mark Intercepted selectors. @@ -437,6 +480,7 @@ static BOOL _isInterceptedSelector(SEL sel) _ASCollectionViewCell *cell = [self dequeueReusableCellWithReuseIdentifier:reuseIdentifier forIndexPath:indexPath]; ASCellNode *node = [_dataController nodeAtIndexPath:indexPath]; + [_rangeController configureContentView:cell.contentView forCellNode:node]; cell.node = node; @@ -449,6 +493,15 @@ static BOOL _isInterceptedSelector(SEL sel) return [[_dataController nodeAtIndexPath:indexPath] calculatedSize]; } +- (UICollectionReusableView *)collectionView:(UICollectionView *)collectionView viewForSupplementaryElementOfKind:(NSString *)kind atIndexPath:(NSIndexPath *)indexPath +{ + NSString *identifier = [self __reuseIdentifierForKind:kind]; + UICollectionReusableView *view = [self dequeueReusableSupplementaryViewOfKind:kind withReuseIdentifier:identifier forIndexPath:indexPath]; + ASCellNode *node = [_dataController supplementaryNodeOfKind:kind atIndexPath:indexPath]; + [_rangeController configureContentView:view forCellNode:node]; + return view; +} + - (NSInteger)numberOfSectionsInCollectionView:(UICollectionView *)collectionView { _superIsPendingDataLoad = NO; @@ -548,7 +601,7 @@ static BOOL _isInterceptedSelector(SEL sel) _ignoreMaxSizeChange = NO; } else { [self performBatchAnimated:NO updates:^{ - [_dataController relayoutAllRows]; + [_dataController relayoutAllNodes]; } completion:nil]; } } @@ -628,7 +681,7 @@ static BOOL _isInterceptedSelector(SEL sel) } if (_asyncDelegateImplementsInsetSection) { - sectionInset = [_asyncDelegate collectionView:self layout:self.collectionViewLayout insetForSectionAtIndex:indexPath.section]; + sectionInset = [(id)_asyncDelegate collectionView:self layout:self.collectionViewLayout insetForSectionAtIndex:indexPath.section]; } if (ASScrollDirectionContainsHorizontalDirection([self scrollableDirections])) { @@ -653,7 +706,7 @@ static BOOL _isInterceptedSelector(SEL sel) return [_asyncDataSource collectionView:self numberOfItemsInSection:section]; } -- (NSUInteger)dataControllerNumberOfSections:(ASDataController *)dataController { +- (NSUInteger)numberOfSectionsInDataController:(ASDataController *)dataController { if ([_asyncDataSource respondsToSelector:@selector(numberOfSectionsInCollectionView:)]) { return [_asyncDataSource numberOfSectionsInCollectionView:self]; } else { @@ -681,8 +734,39 @@ static BOOL _isInterceptedSelector(SEL sel) } } -#pragma mark - -#pragma mark ASRangeControllerDelegate. +#pragma mark - ASCollectionViewDataControllerSource Supplementary view support + +- (ASCellNode *)dataController:(ASCollectionDataController *)dataController supplementaryNodeOfKind:(NSString *)kind atIndexPath:(NSIndexPath *)indexPath +{ + ASCellNode *node = [_asyncDataSource collectionView:self nodeForSupplementaryElementOfKind:kind atIndexPath:indexPath]; + ASDisplayNodeAssert(node != nil, @"A node must be returned for a supplementary node"); + return node; +} + +- (NSArray *)supplementaryNodeKindsInDataController:(ASCollectionDataController *)dataController +{ + return _registeredSupplementaryKinds; +} + +- (ASSizeRange)dataController:(ASCollectionDataController *)dataController constrainedSizeForSupplementaryNodeOfKind:(NSString *)kind atIndexPath:(NSIndexPath *)indexPath +{ + ASDisplayNodeAssert(_layoutDelegate != nil, @"To support supplementary nodes in ASCollectionView, it must have a layoutDelegate for layout inspection. (See ASCollectionViewFlowLayoutInspector for an example.)"); + return [_layoutDelegate collectionView:self constrainedSizeForSupplementaryNodeOfKind:kind atIndexPath:indexPath]; +} + +- (NSUInteger)dataController:(ASCollectionDataController *)dataController supplementaryNodesOfKind:(NSString *)kind inSection:(NSUInteger)section +{ + ASDisplayNodeAssert(_layoutDelegate != nil, @"To support supplementary nodes in ASCollectionView, it must have a layoutDelegate for layout inspection. (See ASCollectionViewFlowLayoutInspector for an example.)"); + return [_layoutDelegate collectionView:self supplementaryNodesOfKind:kind inSection:section]; +} + +- (NSUInteger)dataController:(ASCollectionDataController *)dataController numberOfSectionsForSupplementaryNodeOfKind:(NSString *)kind; +{ + ASDisplayNodeAssert(_layoutDelegate != nil, @"To support supplementary nodes in ASCollectionView, it must have a layoutDelegate for layout inspection. (See ASCollectionViewFlowLayoutInspector for an example.)"); + return [_layoutDelegate collectionView:self numberOfSectionsForSupplementaryNodeOfKind:kind]; +} + +#pragma mark - ASRangeControllerDelegate. - (void)rangeControllerBeginUpdates:(ASRangeController *)rangeController { ASDisplayNodeAssertMainThread(); diff --git a/AsyncDisplayKit/ASTableView.mm b/AsyncDisplayKit/ASTableView.mm index 20c058d658..314d7664c6 100644 --- a/AsyncDisplayKit/ASTableView.mm +++ b/AsyncDisplayKit/ASTableView.mm @@ -405,7 +405,7 @@ void ASPerformBlockWithoutAnimation(BOOL withoutAnimation, void (^block)()) { _ignoreMaxWidthChange = NO; } else { [self beginUpdates]; - [_dataController relayoutAllRows]; + [_dataController relayoutAllNodes]; [self endUpdates]; } } @@ -859,7 +859,7 @@ void ASPerformBlockWithoutAnimation(BOOL withoutAnimation, void (^block)()) { return [_asyncDataSource tableView:self numberOfRowsInSection:section]; } -- (NSUInteger)dataControllerNumberOfSections:(ASDataController *)dataController +- (NSUInteger)numberOfSectionsInDataController:(ASDataController *)dataController { if ([_asyncDataSource respondsToSelector:@selector(numberOfSectionsInTableView:)]) { return [_asyncDataSource numberOfSectionsInTableView:self]; diff --git a/AsyncDisplayKit/Details/ASCollectionDataController.h b/AsyncDisplayKit/Details/ASCollectionDataController.h new file mode 100644 index 0000000000..54ddfbb810 --- /dev/null +++ b/AsyncDisplayKit/Details/ASCollectionDataController.h @@ -0,0 +1,39 @@ +/* Copyright (c) 2015-present, Facebook, Inc. + * All rights reserved. + * + * This source code is licensed under the BSD-style license found in the + * LICENSE file in the root directory of this source tree. An additional grant + * of patent rights can be found in the PATENTS file in the same directory. + */ + +#import + +#import +#import + +@class ASDisplayNode; +@class ASCollectionDataController; +@protocol ASDataControllerSource; + +@protocol ASCollectionDataControllerSource + +- (ASCellNode *)dataController:(ASCollectionDataController *)dataController supplementaryNodeOfKind:(NSString *)kind atIndexPath:(NSIndexPath *)indexPath; + +/** + The constrained size range for layout. + */ +- (ASSizeRange)dataController:(ASCollectionDataController *)dataController constrainedSizeForSupplementaryNodeOfKind:(NSString *)kind atIndexPath:(NSIndexPath *)indexPath; + +- (NSArray *)supplementaryNodeKindsInDataController:(ASCollectionDataController *)dataController; + +- (NSUInteger)dataController:(ASCollectionDataController *)dataController numberOfSectionsForSupplementaryNodeOfKind:(NSString *)kind; + +- (NSUInteger)dataController:(ASCollectionDataController *)dataController supplementaryNodesOfKind:(NSString *)kind inSection:(NSUInteger)section; + +@end + +@interface ASCollectionDataController : ASDataController + +- (ASCellNode *)supplementaryNodeOfKind:(NSString *)kind atIndexPath:(NSIndexPath *)indexPath; + +@end \ No newline at end of file diff --git a/AsyncDisplayKit/Details/ASCollectionDataController.mm b/AsyncDisplayKit/Details/ASCollectionDataController.mm new file mode 100644 index 0000000000..45f1d3af37 --- /dev/null +++ b/AsyncDisplayKit/Details/ASCollectionDataController.mm @@ -0,0 +1,217 @@ +/* Copyright (c) 2015-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 "ASCollectionDataController.h" + +#import "ASAssert.h" +#import "ASMultidimensionalArrayUtils.h" +#import "ASDisplayNode.h" +#import "ASDisplayNodeInternal.h" +#import "ASDataController+Subclasses.h" + +//#define LOG(...) NSLog(__VA_ARGS__) +#define LOG(...) + +@interface ASCollectionDataController () + +- (id)collectionDataSource; + +@end + +@implementation ASCollectionDataController { + NSMutableDictionary *_pendingNodes; + NSMutableDictionary *_pendingIndexPaths; +} + +- (void)prepareForReloadData +{ + _pendingNodes = [NSMutableDictionary dictionary]; + _pendingIndexPaths = [NSMutableDictionary dictionary]; + + [[self supplementaryKinds] enumerateObjectsUsingBlock:^(NSString *kind, NSUInteger idx, BOOL *stop) { + LOG(@"Populating elements of kind: %@", kind); + NSMutableArray *indexPaths = [NSMutableArray array]; + NSMutableArray *nodes = [NSMutableArray array]; + [self _populateSupplementaryNodesOfKind:kind withMutableNodes:nodes mutableIndexPaths:indexPaths]; + _pendingNodes[kind] = nodes; + _pendingIndexPaths[kind] = indexPaths; + + // Measure loaded nodes before leaving the main thread + [self layoutLoadedNodes:nodes ofKind:kind atIndexPaths:indexPaths]; + }]; +} + +- (void)willReloadData +{ + [_pendingNodes enumerateKeysAndObjectsUsingBlock:^(NSString *kind, NSMutableArray *nodes, BOOL *stop) { + // Remove everything that existed before the reload, now that we're ready to insert replacements + NSArray *indexPaths = [self indexPathsForEditingNodesOfKind:kind]; + [self deleteNodesOfKind:kind atIndexPaths:indexPaths completion:nil]; + + NSArray *editingNodes = [self editingNodesOfKind:kind]; + NSMutableIndexSet *indexSet = [[NSMutableIndexSet alloc] initWithIndexesInRange:NSMakeRange(0, editingNodes.count)]; + [self deleteSectionsOfKind:kind atIndexSet:indexSet completion:nil]; + + // Insert each section + NSUInteger sectionCount = [self.collectionDataSource dataController:self numberOfSectionsForSupplementaryNodeOfKind:kind]; + NSMutableArray *sections = [NSMutableArray arrayWithCapacity:sectionCount]; + for (int i = 0; i < sectionCount; i++) { + [sections addObject:[[NSMutableArray alloc] init]]; + } + [self insertSections:sections ofKind:kind atIndexSet:[NSIndexSet indexSetWithIndexesInRange:NSMakeRange(0, sectionCount)] completion:nil]; + + [self batchLayoutNodes:nodes ofKind:kind atIndexPaths:_pendingIndexPaths[kind] completion:^(NSArray *nodes, NSArray *indexPaths) { + [self insertNodes:nodes ofKind:kind atIndexPaths:indexPaths completion:nil]; + }]; + [_pendingNodes removeObjectForKey:kind]; + [_pendingIndexPaths removeObjectForKey:kind]; + }]; +} + +- (void)prepareForInsertSections:(NSIndexSet *)sections +{ + [[self supplementaryKinds] enumerateObjectsUsingBlock:^(NSString *kind, NSUInteger idx, BOOL *stop) { + LOG(@"Populating elements of kind: %@, for sections: %@", kind, sections); + NSMutableArray *nodes = [NSMutableArray array]; + NSMutableArray *indexPaths = [NSMutableArray array]; + [self _populateSupplementaryNodesOfKind:kind withSections:sections mutableNodes:nodes mutableIndexPaths:indexPaths]; + _pendingNodes[kind] = nodes; + _pendingIndexPaths[kind] = indexPaths; + + // Measure loaded nodes before leaving the main thread + [self layoutLoadedNodes:nodes ofKind:kind atIndexPaths:indexPaths]; + }]; +} + +- (void)willInsertSections:(NSIndexSet *)sections +{ + [_pendingNodes enumerateKeysAndObjectsUsingBlock:^(NSString *kind, NSMutableArray *nodes, BOOL *stop) { + NSMutableArray *sectionArray = [NSMutableArray arrayWithCapacity:sections.count]; + for (NSUInteger i = 0; i < sections.count; i++) { + [sectionArray addObject:[NSMutableArray array]]; + } + + [self insertSections:sectionArray ofKind:kind atIndexSet:sections completion:nil]; + [self batchLayoutNodes:nodes ofKind:kind atIndexPaths:_pendingIndexPaths[kind] completion:nil]; + _pendingNodes[kind] = nil; + _pendingIndexPaths[kind] = nil; + }]; +} + +- (void)willDeleteSections:(NSIndexSet *)sections +{ + [[self supplementaryKinds] enumerateObjectsUsingBlock:^(NSString *kind, NSUInteger idx, BOOL *stop) { + NSArray *indexPaths = ASIndexPathsForMultidimensionalArrayAtIndexSet([self editingNodesOfKind:kind], sections); + + [self deleteNodesOfKind:kind atIndexPaths:indexPaths completion:nil]; + [self deleteSectionsOfKind:kind atIndexSet:sections completion:nil]; + }]; +} + +- (void)prepareForReloadSections:(NSIndexSet *)sections +{ + [[self supplementaryKinds] enumerateObjectsUsingBlock:^(NSString *kind, NSUInteger idx, BOOL *stop) { + NSMutableArray *nodes = [NSMutableArray array]; + NSMutableArray *indexPaths = [NSMutableArray array]; + [self _populateSupplementaryNodesOfKind:kind withSections:sections mutableNodes:nodes mutableIndexPaths:indexPaths]; + _pendingNodes[kind] = nodes; + _pendingIndexPaths[kind] = indexPaths; + + // Measure loaded nodes before leaving the main thread + [self layoutLoadedNodes:nodes ofKind:kind atIndexPaths:indexPaths]; + }]; +} + +- (void)willReloadSections:(NSIndexSet *)sections +{ + [_pendingNodes enumerateKeysAndObjectsUsingBlock:^(NSString *kind, NSMutableArray *nodes, BOOL *stop) { + NSArray *indexPaths = ASIndexPathsForMultidimensionalArrayAtIndexSet([self editingNodesOfKind:kind], sections); + [self deleteNodesOfKind:kind atIndexPaths:indexPaths completion:nil]; + // reinsert the elements + [self batchLayoutNodes:nodes ofKind:kind atIndexPaths:_pendingIndexPaths[kind] completion:nil]; + _pendingNodes[kind] = nil; + _pendingIndexPaths[kind] = nil; + }]; +} + +- (void)willMoveSection:(NSInteger)section toSection:(NSInteger)newSection +{ + [[self supplementaryKinds] enumerateObjectsUsingBlock:^(NSString *kind, NSUInteger idx, BOOL *stop) { + NSArray *indexPaths = ASIndexPathsForMultidimensionalArrayAtIndexSet([self editingNodesOfKind:kind], [NSIndexSet indexSetWithIndex:section]); + NSArray *nodes = ASFindElementsInMultidimensionalArrayAtIndexPaths([self editingNodesOfKind:kind], indexPaths); + [self deleteNodesOfKind:kind atIndexPaths:indexPaths completion:nil]; + + // 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]]]; + }]; + [self insertNodes:nodes ofKind:kind atIndexPaths:indexPaths completion:nil]; + }]; +} + +- (void)_populateSupplementaryNodesOfKind:(NSString *)kind withMutableNodes:(NSMutableArray *)nodes mutableIndexPaths:(NSMutableArray *)indexPaths +{ + NSUInteger sectionCount = [self.collectionDataSource dataController:self numberOfSectionsForSupplementaryNodeOfKind:kind]; + for (NSUInteger i = 0; i < sectionCount; i++) { + NSIndexPath *sectionIndexPath = [[NSIndexPath alloc] initWithIndex:i]; + NSUInteger rowCount = [self.collectionDataSource dataController:self supplementaryNodesOfKind:kind inSection:i]; + for (NSUInteger j = 0; j < rowCount; j++) { + NSIndexPath *indexPath = [sectionIndexPath indexPathByAddingIndex:j]; + [indexPaths addObject:indexPath]; + [nodes addObject:[self.collectionDataSource dataController:self supplementaryNodeOfKind:kind atIndexPath:indexPath]]; + } + } +} + +- (void)_populateSupplementaryNodesOfKind:(NSString *)kind withSections:(NSIndexSet *)sections mutableNodes:(NSMutableArray *)nodes mutableIndexPaths:(NSMutableArray *)indexPaths +{ + [sections enumerateIndexesUsingBlock:^(NSUInteger idx, BOOL *stop) { + NSUInteger rowNum = [self.collectionDataSource dataController:self supplementaryNodesOfKind:kind inSection:idx]; + NSIndexPath *sectionIndex = [[NSIndexPath alloc] initWithIndex:idx]; + for (NSUInteger i = 0; i < rowNum; i++) { + NSIndexPath *indexPath = [sectionIndex indexPathByAddingIndex:i]; + [indexPaths addObject:indexPath]; + [nodes addObject:[self.collectionDataSource dataController:self supplementaryNodeOfKind:kind atIndexPath:indexPath]]; + } + }]; +} + +#pragma mark - Sizing query + +- (ASSizeRange)constrainedSizeForNodeOfKind:(NSString *)kind atIndexPath:(NSIndexPath *)indexPath +{ + if ([kind isEqualToString:ASDataControllerRowNodeKind]) { + return [super constrainedSizeForNodeOfKind:kind atIndexPath:indexPath]; + } else { + return [self.collectionDataSource dataController:self constrainedSizeForSupplementaryNodeOfKind:kind atIndexPath:indexPath]; + } +} + +#pragma mark - External supplementary store querying + +- (ASCellNode *)supplementaryNodeOfKind:(NSString *)kind atIndexPath:(NSIndexPath *)indexPath +{ + ASDisplayNodeAssertMainThread(); + return [self completedNodesOfKind:kind][indexPath.section][indexPath.item]; +} + +#pragma mark - Private Helpers + +- (NSArray *)supplementaryKinds +{ + return [self.collectionDataSource supplementaryNodeKindsInDataController:self]; +} + +- (id)collectionDataSource +{ + return (id)self.dataSource; +} + +@end diff --git a/AsyncDisplayKit/Details/ASCollectionViewFlowLayoutInspector.h b/AsyncDisplayKit/Details/ASCollectionViewFlowLayoutInspector.h new file mode 100644 index 0000000000..5aab03e012 --- /dev/null +++ b/AsyncDisplayKit/Details/ASCollectionViewFlowLayoutInspector.h @@ -0,0 +1,42 @@ +/* Copyright (c) 2015-present, Facebook, Inc. + * All rights reserved. + * + * This source code is licensed under the BSD-style license found in the + * LICENSE file in the root directory of this source tree. An additional grant + * of patent rights can be found in the PATENTS file in the same directory. + */ + +#import + +#import + +@class ASCollectionView; + +@protocol ASCollectionViewLayoutInspecting + +/** + * Asks the inspector to provide a constrained size range for the given supplementary node. + */ +- (ASSizeRange)collectionView:(ASCollectionView *)collectionView constrainedSizeForSupplementaryNodeOfKind:(NSString *)kind atIndexPath:(NSIndexPath *)indexPath; + +/** + * Asks the inspector for the number of supplementary sections in the collection view for the given kind. + */ +- (NSUInteger)collectionView:(ASCollectionView *)collectionView numberOfSectionsForSupplementaryNodeOfKind:(NSString *)kind; + +/** + * Asks the inspector for the number of supplementary views for the given kind in the specified section. + */ +- (NSUInteger)collectionView:(ASCollectionView *)collectionView supplementaryNodesOfKind:(NSString *)kind inSection:(NSUInteger)section; + +@end + +@interface ASCollectionViewFlowLayoutInspector : NSObject + +@property (nonatomic, weak) UICollectionViewFlowLayout *layout; + +- (instancetype)initWithCollectionView:(ASCollectionView *)collectionView flowLayout:(UICollectionViewFlowLayout *)flowLayout; + +- (void)cacheSelectorsForCollectionView:(ASCollectionView *)collectionView; + +@end diff --git a/AsyncDisplayKit/Details/ASCollectionViewFlowLayoutInspector.m b/AsyncDisplayKit/Details/ASCollectionViewFlowLayoutInspector.m new file mode 100644 index 0000000000..e68020490d --- /dev/null +++ b/AsyncDisplayKit/Details/ASCollectionViewFlowLayoutInspector.m @@ -0,0 +1,121 @@ +/* Copyright (c) 2015-present, Facebook, Inc. + * All rights reserved. + * + * This source code is licensed under the BSD-style license found in the + * LICENSE file in the root directory of this source tree. An additional grant + * of patent rights can be found in the PATENTS file in the same directory. + */ + +#import + +#import "ASCollectionViewFlowLayoutInspector.h" + +#import "ASCollectionView.h" + +@implementation ASCollectionViewFlowLayoutInspector { + BOOL _delegateImplementsReferenceSizeForHeader; + BOOL _delegateImplementsReferenceSizeForFooter; +} + +#pragma mark - Accessors + +- (instancetype)initWithCollectionView:(ASCollectionView *)collectionView flowLayout:(UICollectionViewFlowLayout *)flowLayout; +{ + self = [super init]; + + if (flowLayout == nil) { + return nil; + } + + if (self != nil) { + [self cacheSelectorsForCollectionView:collectionView]; + _layout = flowLayout; + } + return self; +} + +- (void)cacheSelectorsForCollectionView:(ASCollectionView *)collectionView +{ + if (collectionView == nil) { + _delegateImplementsReferenceSizeForHeader = nil; + _delegateImplementsReferenceSizeForFooter = nil; + } else { + _delegateImplementsReferenceSizeForHeader = [[self delegateForCollectionView:collectionView] respondsToSelector:@selector(collectionView:layout:referenceSizeForHeaderInSection:)]; + _delegateImplementsReferenceSizeForFooter = [[self delegateForCollectionView:collectionView] respondsToSelector:@selector(collectionView:layout:referenceSizeForFooterInSection:)]; + } +} + +#pragma mark - ASCollectionViewLayoutInspecting + +- (ASSizeRange)collectionView:(ASCollectionView *)collectionView constrainedSizeForSupplementaryNodeOfKind:(NSString *)kind atIndexPath:(NSIndexPath *)indexPath +{ + CGSize constrainedSize; + CGSize supplementarySize = [self sizeForSupplementaryViewOfKind:kind inSection:indexPath.section collectionView:collectionView]; + if (_layout.scrollDirection == UICollectionViewScrollDirectionVertical) { + constrainedSize = CGSizeMake(collectionView.bounds.size.width, supplementarySize.height); + } else { + constrainedSize = CGSizeMake(supplementarySize.height, collectionView.bounds.size.height); + } + return ASSizeRangeMake(CGSizeZero, constrainedSize); +} + +- (NSUInteger)collectionView:(ASCollectionView *)collectionView numberOfSectionsForSupplementaryNodeOfKind:(NSString *)kind +{ + if ([collectionView.asyncDataSource respondsToSelector:@selector(numberOfSectionsInCollectionView:)]) { + return [collectionView.asyncDataSource numberOfSectionsInCollectionView:collectionView]; + } else { + return 1; + } +} + +- (NSUInteger)collectionView:(ASCollectionView *)collectionView supplementaryNodesOfKind:(NSString *)kind inSection:(NSUInteger)section +{ + return [self layoutHasSupplementaryViewOfKind:kind inSection:section collectionView:collectionView] ? 1 : 0; +} + +#pragma mark - Private helpers + +- (CGSize)sizeForSupplementaryViewOfKind:(NSString *)kind inSection:(NSUInteger)section collectionView:(ASCollectionView *)collectionView +{ + if ([kind isEqualToString:UICollectionElementKindSectionHeader]) { + if (_delegateImplementsReferenceSizeForHeader) { + return [[self delegateForCollectionView:collectionView] collectionView:collectionView layout:_layout referenceSizeForHeaderInSection:section]; + } else { + return [self.layout headerReferenceSize]; + } + } else if ([kind isEqualToString:UICollectionElementKindSectionFooter]) { + if (_delegateImplementsReferenceSizeForFooter) { + return [[self delegateForCollectionView:collectionView] collectionView:collectionView layout:_layout referenceSizeForFooterInSection:section]; + } else { + return [self.layout footerReferenceSize]; + } + } else { + return CGSizeZero; + } +} + +- (BOOL)layoutHasSupplementaryViewOfKind:(NSString *)kind inSection:(NSUInteger)section collectionView:(ASCollectionView *)collectionView +{ + CGSize size = [self sizeForSupplementaryViewOfKind:kind inSection:section collectionView:collectionView]; + if ([self usedLayoutValueForSize:size] > 0) { + return YES; + } else { + return NO; + } +} + +- (CGFloat)usedLayoutValueForSize:(CGSize)size +{ + if (_layout.scrollDirection == UICollectionViewScrollDirectionVertical) { + return size.height; + } else { + return size.width; + } +} + +- (id)delegateForCollectionView:(ASCollectionView *)collectionView +{ + return (id)collectionView.asyncDelegate; +} + +@end diff --git a/AsyncDisplayKit/Details/ASCollectionViewLayoutController.mm b/AsyncDisplayKit/Details/ASCollectionViewLayoutController.mm index cbed7f2bb7..9619cba111 100644 --- a/AsyncDisplayKit/Details/ASCollectionViewLayoutController.mm +++ b/AsyncDisplayKit/Details/ASCollectionViewLayoutController.mm @@ -137,7 +137,9 @@ typedef struct ASRangeGeometry ASRangeGeometry; NSMutableSet *indexPathSet = [[NSMutableSet alloc] init]; NSArray *layoutAttributes = [_collectionViewLayout layoutAttributesForElementsInRect:rangeBounds]; for (UICollectionViewLayoutAttributes *la in layoutAttributes) { - [indexPathSet addObject:la.indexPath]; + if (la.representedElementCategory == UICollectionElementCategoryCell) { + [indexPathSet addObject:la.indexPath]; + } } return indexPathSet; } diff --git a/AsyncDisplayKit/Details/ASDataController+Subclasses.h b/AsyncDisplayKit/Details/ASDataController+Subclasses.h new file mode 100644 index 0000000000..3e33449389 --- /dev/null +++ b/AsyncDisplayKit/Details/ASDataController+Subclasses.h @@ -0,0 +1,159 @@ +/* Copyright (c) 2015-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 "ASDataController.h" + +@interface ASDataController (Subclasses) + +#pragma mark - Internal editing & completed store querying + +/** + * Provides a collection of index paths for nodes of the given kind that are currently in the editing store + */ +- (NSArray *)indexPathsForEditingNodesOfKind:(NSString *)kind; + +/** + * Read-only access to the underlying editing nodes of the given kind + */ +- (NSMutableArray *)editingNodesOfKind:(NSString *)kind; + +/** + * Read only access to the underlying completed nodes of the given kind + */ +- (NSMutableArray *)completedNodesOfKind:(NSString *)kind; + +#pragma mark - Node sizing + +/** + * Measure and layout the given nodes in optimized batches, constraining each to a given size in `constrainedSizeForNodeOfKind:atIndexPath:`. + */ +- (void)batchLayoutNodes:(NSArray *)nodes ofKind:(NSString *)kind atIndexPaths:(NSArray *)indexPaths completion:(void (^)(NSArray *nodes, NSArray *indexPaths))completionBlock; + +/* + * Perform measurement and layout of loaded nodes on the main thread, skipping unloaded nodes. + * + * @discussion Once nodes have loaded their views, we can't layout in the background so this is a chance + * to do so immediately on the main thread. + */ +- (void)layoutLoadedNodes:(NSArray *)nodes ofKind:(NSString *)kind atIndexPaths:(NSArray *)indexPaths; + +/** + * Provides the size range for a specific node during the layout process. + */ +- (ASSizeRange)constrainedSizeForNodeOfKind:(NSString *)kind atIndexPath:(NSIndexPath *)indexPath; + +#pragma mark - Node & Section Insertion/Deletion API + +/** + * Inserts the given nodes of the specified kind into the backing store, calling completion on the main thread when the write finishes. + */ +- (void)insertNodes:(NSArray *)nodes ofKind:(NSString *)kind atIndexPaths:(NSArray *)indexPaths completion:(void (^)(NSArray *nodes, NSArray *indexPaths))completionBlock; + +/** + * Deletes the given nodes of the specified kind in the backing store, calling completion on the main thread when the deletion finishes. + */ +- (void)deleteNodesOfKind:(NSString *)kind atIndexPaths:(NSArray *)indexPaths completion:(void (^)(NSArray *nodes, NSArray *indexPaths))completionBlock; + +/** + * Inserts the given sections of the specified kind in the backing store, calling completion on the main thread when finished. + */ +- (void)insertSections:(NSMutableArray *)sections ofKind:(NSString *)kind atIndexSet:(NSIndexSet *)indexSet completion:(void (^)(NSArray *sections, NSIndexSet *indexSet))completionBlock; + +/** + * Deletes the given sections of the specified kind in the backing store, calling completion on the main thread when finished. + */ +- (void)deleteSectionsOfKind:(NSString *)kind atIndexSet:(NSIndexSet *)indexSet completion:(void (^)(NSIndexSet *indexSet))completionBlock; + +#pragma mark - Data Manipulation Hooks + +/** + * Notifies the subclass to perform any work needed before the data controller is reloaded entirely + * + * @discussion This method will be performed before the data controller enters its editing queue, usually on the main + * thread. The data source is locked at this point and accessing it is safe. Use this method to set up any nodes or + * data stores before entering into editing the backing store on a background thread. + */ + - (void)prepareForReloadData; + +/** + * Notifies the subclass that the data controller is about to reload its data entirely + * + * @discussion This method will be performed on the data controller's editing background queue before the parent's + * concrete implementation. This is a great place to perform new node creation like supplementary views + * or header/footer nodes. + */ +- (void)willReloadData; + +/** + * Notifies the subclass to perform setup before sections are inserted in the data controller + * + * @discussion This method will be performed before the data controller enters its editing queue, usually on the main + * thread. The data source is locked at this point and accessing it is safe. Use this method to set up any nodes or + * data stores before entering into editing the backing store on a background thread. + * + * @param sections Indices of sections to be inserted + */ +- (void)prepareForInsertSections:(NSIndexSet *)sections; + +/** + * Notifies the subclass that the data controller will insert new sections at the given position + * + * @discussion This method will be performed on the data controller's editing background queue before the parent's + * concrete implementation. This is a great place to perform any additional transformations like supplementary views + * or header/footer nodes. + * + * @param sections Indices of sections to be inserted + */ +- (void)willInsertSections:(NSIndexSet *)sections; + +/** + * Notifies the subclass that the data controller will delete sections at the given positions + * + * @discussion This method will be performed on the data controller's editing background queue before the parent's + * concrete implementation. This is a great place to perform any additional transformations like supplementary views + * or header/footer nodes. + * + * @param sections Indices of sections to be deleted + */ +- (void)willDeleteSections:(NSIndexSet *)sections; + +/** + * Notifies the subclass to perform any work needed before the given sections will be reloaded. + * + * @discussion This method will be performed before the data controller enters its editing queue, usually on the main + * thread. The data source is locked at this point and accessing it is safe. Use this method to set up any nodes or + * data stores before entering into editing the backing store on a background thread. + * + * @param sections Indices of sections to be reloaded + */ +- (void)prepareForReloadSections:(NSIndexSet *)sections; + +/** + * Notifies the subclass that the data controller will reload the sections in the given index set + * + * @discussion This method will be performed on the data controller's editing background queue before the parent's + * concrete implementation. This is a great place to perform any additional transformations like supplementary views + * or header/footer nodes. + * + * @param sections Indices of sections to be reloaded + */ +- (void)willReloadSections:(NSIndexSet *)sections; + +/** + * Notifies the subclass that the data controller will move a section to a new position + * + * @discussion This method will be performed on the data controller's editing background queue before the parent's + * concrete implementation. This is a great place to perform any additional transformations like supplementary views + * or header/footer nodes. + * + * @param section Index of current section position + * @param newSection Index of new section position + */ +- (void)willMoveSection:(NSInteger)section toSection:(NSInteger)newSection; + +@end diff --git a/AsyncDisplayKit/Details/ASDataController.h b/AsyncDisplayKit/Details/ASDataController.h index 589d210f9d..e605aa86d6 100644 --- a/AsyncDisplayKit/Details/ASDataController.h +++ b/AsyncDisplayKit/Details/ASDataController.h @@ -14,6 +14,8 @@ @class ASCellNode; @class ASDataController; +FOUNDATION_EXPORT NSString * const ASDataControllerRowNodeKind; + typedef NSUInteger ASDataControllerAnimationOptions; /** @@ -40,7 +42,7 @@ typedef NSUInteger ASDataControllerAnimationOptions; /** Fetch the number of sections. */ -- (NSUInteger)dataControllerNumberOfSections:(ASDataController *)dataController; +- (NSUInteger)numberOfSectionsInDataController:(ASDataController *)dataController; /** Lock the data source for data fetching. @@ -90,7 +92,6 @@ typedef NSUInteger ASDataControllerAnimationOptions; @end - /** * Controller to layout data in background, and managed data updating. * @@ -156,10 +157,12 @@ typedef NSUInteger ASDataControllerAnimationOptions; - (void)reloadRowsAtIndexPaths:(NSArray *)indexPaths withAnimationOptions:(ASDataControllerAnimationOptions)animationOptions; /** - * Re-measures all loaded nodes. Used to respond to a change in size of the containing view + * Re-measures all loaded nodes in the backing store. + * + * @discussion Used to respond to a change in size of the containing view * (e.g. ASTableView or ASCollectionView after an orientation change). */ -- (void)relayoutAllRows; +- (void)relayoutAllNodes; - (void)moveRowAtIndexPath:(NSIndexPath *)indexPath toIndexPath:(NSIndexPath *)newIndexPath withAnimationOptions:(ASDataControllerAnimationOptions)animationOptions; @@ -177,6 +180,9 @@ typedef NSUInteger ASDataControllerAnimationOptions; - (NSArray *)nodesAtIndexPaths:(NSArray *)indexPaths; -- (NSArray *)completedNodes; // This provides efficient access to the entire _completedNodes multidimensional array. +/** + * Direct access to the nodes that have completed calculation and layout + */ +- (NSArray *)completedNodes; @end diff --git a/AsyncDisplayKit/Details/ASDataController.mm b/AsyncDisplayKit/Details/ASDataController.mm index 6a5b361ff0..400a1161fd 100644 --- a/AsyncDisplayKit/Details/ASDataController.mm +++ b/AsyncDisplayKit/Details/ASDataController.mm @@ -21,12 +21,16 @@ const static NSUInteger kASDataControllerSizingCountPerProcessor = 5; +NSString * const ASDataControllerRowNodeKind = @"_ASDataControllerRowNodeKind"; + static void *kASSizingQueueContext = &kASSizingQueueContext; @interface ASDataController () { NSMutableArray *_externalCompletedNodes; // Main thread only. External data access can immediately query this if available. - NSMutableArray *_completedNodes; // Main thread only. External data access can immediately query this if _externalCompletedNodes is unavailable. - NSMutableArray *_editingNodes; // Modified on _editingTransactionQueue only. Updates propogated to _completedNodes. + NSMutableDictionary *_completedNodes; // Main thread only. External data access can immediately query this if _externalCompletedNodes is unavailable. + NSMutableDictionary *_editingNodes; // Modified on _editingTransactionQueue only. Updates propogated to _completedNodes. + + NSMutableArray *_pendingEditCommandBlocks; // To be run on the main thread. Handles begin/endUpdates tracking. NSOperationQueue *_editingTransactionQueue; // Serial background queue. Dispatches concurrent layout and manages _editingNodes. @@ -52,9 +56,12 @@ static void *kASSizingQueueContext = &kASSizingQueueContext; return nil; } - _completedNodes = [NSMutableArray array]; - _editingNodes = [NSMutableArray array]; + _completedNodes = [NSMutableDictionary dictionary]; + _editingNodes = [NSMutableDictionary dictionary]; + _completedNodes[ASDataControllerRowNodeKind] = [NSMutableArray array]; + _editingNodes[ASDataControllerRowNodeKind] = [NSMutableArray array]; + _pendingEditCommandBlocks = [NSMutableArray array]; _editingTransactionQueue = [[NSOperationQueue alloc] init]; @@ -96,26 +103,53 @@ static void *kASSizingQueueContext = &kASSizingQueueContext; #pragma mark - Cell Layout -/* - * FIXME: Shouldn't this method, as well as `_layoutNodes:atIndexPaths:withAnimationOptions:` use the word "measure" instead? - * - * Once nodes have loaded their views, we can't layout in the background so this is a chance - * to do so immediately on the main thread. - */ -- (void)_layoutNodesWithMainThreadAffinity:(NSArray *)nodes atIndexPaths:(NSArray *)indexPaths { +- (void)batchLayoutNodes:(NSArray *)nodes ofKind:(NSString *)kind atIndexPaths:(NSArray *)indexPaths completion:(void (^)(NSArray *nodes, NSArray *indexPaths))completionBlock +{ + NSUInteger blockSize = [[ASDataController class] parallelProcessorCount] * kASDataControllerSizingCountPerProcessor; + + // Processing in batches + for (NSUInteger i = 0; i < indexPaths.count; i += blockSize) { + NSRange batchedRange = NSMakeRange(i, MIN(indexPaths.count - i, blockSize)); + NSArray *batchedIndexPaths = [indexPaths subarrayWithRange:batchedRange]; + NSArray *batchedNodes = [nodes subarrayWithRange:batchedRange]; + + [self _layoutNodes:batchedNodes ofKind:kind atIndexPaths:batchedIndexPaths completion:completionBlock]; + } +} + +- (void)layoutLoadedNodes:(NSArray *)nodes ofKind:(NSString *)kind atIndexPaths:(NSArray *)indexPaths { NSAssert(NSThread.isMainThread, @"Main thread layout must be on the main thread."); [indexPaths enumerateObjectsUsingBlock:^(NSIndexPath *indexPath, NSUInteger idx, __unused BOOL * stop) { ASCellNode *node = nodes[idx]; if (node.isNodeLoaded) { - ASSizeRange constrainedSize = [_dataSource dataController:self constrainedSizeForNodeAtIndexPath:indexPath]; - [node measureWithSizeRange:constrainedSize]; - node.frame = CGRectMake(0.0f, 0.0f, node.calculatedSize.width, node.calculatedSize.height); + ASSizeRange constrainedSize = [self constrainedSizeForNodeOfKind:kind atIndexPath:indexPath]; + [self _layoutNode:node withConstrainedSize:constrainedSize]; } }]; } -- (void)_layoutNodes:(NSArray *)nodes atIndexPaths:(NSArray *)indexPaths withAnimationOptions:(ASDataControllerAnimationOptions)animationOptions +/** + * Measure and layout the given node with the constrained size range. + */ +- (void)_layoutNode:(ASCellNode *)node withConstrainedSize:(ASSizeRange)constrainedSize +{ + [node measureWithSizeRange:constrainedSize]; + node.frame = CGRectMake(0.0f, 0.0f, node.calculatedSize.width, node.calculatedSize.height); +} + +/** + * Measures and defines the layout for each node in optimized batches on an editing queue, inserting the results into the backing store. + */ +- (void)_batchLayoutNodes:(NSArray *)nodes atIndexPaths:(NSArray *)indexPaths withAnimationOptions:(ASDataControllerAnimationOptions)animationOptions +{ + [self batchLayoutNodes:nodes ofKind:ASDataControllerRowNodeKind atIndexPaths:indexPaths completion:^(NSArray *nodes, NSArray *indexPaths) { + // Insert finished nodes into data storage + [self _insertNodes:nodes atIndexPaths:indexPaths withAnimationOptions:animationOptions]; + }]; +} + +- (void)_layoutNodes:(NSArray *)nodes ofKind:(NSString *)kind atIndexPaths:(NSArray *)indexPaths completion:(void (^)(NSArray *nodes, NSArray *indexPaths))completionBlock { ASDisplayNodeAssert([NSOperationQueue currentQueue] == _editingTransactionQueue, @"Cell node layout must be initiated from edit transaction queue"); @@ -131,7 +165,7 @@ static void *kASSizingQueueContext = &kASSizingQueueContext; for (NSUInteger k = j; k < j + batchCount; k++) { ASCellNode *node = nodes[k]; if (!node.isNodeLoaded) { - nodeBoundSizes[k] = [_dataSource dataController:self constrainedSizeForNodeAtIndexPath:indexPaths[k]]; + nodeBoundSizes[k] = [self constrainedSizeForNodeOfKind:kind atIndexPath:indexPaths[k]]; } } @@ -139,11 +173,9 @@ static void *kASSizingQueueContext = &kASSizingQueueContext; for (NSUInteger k = j; k < j + batchCount; k++) { ASCellNode *node = nodes[k]; // Only measure nodes whose views aren't loaded, since we're in the background. - // We should already have measured loaded nodes before we left the main thread, using _layoutNodesWithMainThreadAffinity: + // We should already have measured loaded nodes before we left the main thread, using layoutLoadedNodes:ofKind:atIndexPaths: if (!node.isNodeLoaded) { - ASSizeRange constrainedSize = nodeBoundSizes[k]; - [node measureWithSizeRange:constrainedSize]; - node.frame = CGRectMake(0.0f, 0.0f, node.calculatedSize.width, node.calculatedSize.height); + [self _layoutNode:node withConstrainedSize:nodeBoundSizes[k]]; } } }); @@ -152,83 +184,147 @@ static void *kASSizingQueueContext = &kASSizingQueueContext; // Block the _editingTransactionQueue from executing a new edit transaction until layout is done & _editingNodes array is updated. dispatch_group_wait(layoutGroup, DISPATCH_TIME_FOREVER); free(nodeBoundSizes); - // Insert finished nodes into data storage - [self _insertNodes:nodes atIndexPaths:indexPaths withAnimationOptions:animationOptions]; + + if (completionBlock) + completionBlock(nodes, indexPaths); } -- (void)_batchLayoutNodes:(NSArray *)nodes atIndexPaths:(NSArray *)indexPaths withAnimationOptions:(ASDataControllerAnimationOptions)animationOptions +- (ASSizeRange)constrainedSizeForNodeOfKind:(NSString *)kind atIndexPath:(NSIndexPath *)indexPath { - NSUInteger blockSize = [[ASDataController class] parallelProcessorCount] * kASDataControllerSizingCountPerProcessor; - - // Processing in batches - for (NSUInteger i = 0; i < indexPaths.count; i += blockSize) { - NSRange batchedRange = NSMakeRange(i, MIN(indexPaths.count - i, blockSize)); - NSArray *batchedIndexPaths = [indexPaths subarrayWithRange:batchedRange]; - NSArray *batchedNodes = [nodes subarrayWithRange:batchedRange]; - - [self _layoutNodes:batchedNodes atIndexPaths:batchedIndexPaths withAnimationOptions:animationOptions]; - } + return [_dataSource dataController:self constrainedSizeForNodeAtIndexPath:indexPath]; } -#pragma mark - Internal Data Querying + Editing +#pragma mark - External Data Querying + Editing -- (void)_insertNodes:(NSArray *)nodes atIndexPaths:(NSArray *)indexPaths withAnimationOptions:(ASDataControllerAnimationOptions)animationOptions +- (void)insertNodes:(NSArray *)nodes ofKind:(NSString *)kind atIndexPaths:(NSArray *)indexPaths completion:(void (^)(NSArray *nodes, NSArray *indexPaths))completionBlock { if (indexPaths.count == 0) return; - ASInsertElementsIntoMultidimensionalArrayAtIndexPaths(_editingNodes, indexPaths, nodes); + + NSMutableArray *editingNodes = _editingNodes[kind]; + ASInsertElementsIntoMultidimensionalArrayAtIndexPaths(editingNodes, indexPaths, nodes); + _editingNodes[kind] = editingNodes; // Deep copy is critical here, or future edits to the sub-arrays will pollute state between _editing and _complete on different threads. - NSMutableArray *completedNodes = (NSMutableArray *)ASMultidimensionalArrayDeepMutableCopy(_editingNodes); + NSMutableArray *completedNodes = (NSMutableArray *)ASMultidimensionalArrayDeepMutableCopy(editingNodes); ASDisplayNodePerformBlockOnMainThread(^{ - _completedNodes = completedNodes; - if (_delegateDidInsertNodes) - [_delegate dataController:self didInsertNodes:nodes atIndexPaths:indexPaths withAnimationOptions:animationOptions]; + _completedNodes[kind] = completedNodes; + if (completionBlock) { + completionBlock(nodes, indexPaths); + } }); } -- (void)_deleteNodesAtIndexPaths:(NSArray *)indexPaths withAnimationOptions:(ASDataControllerAnimationOptions)animationOptions +- (void)deleteNodesOfKind:(NSString *)kind atIndexPaths:(NSArray *)indexPaths completion:(void (^)(NSArray *nodes, NSArray *indexPaths))completionBlock { if (indexPaths.count == 0) return; - LOG(@"_deleteNodesAtIndexPaths:%@, full index paths in _editingNodes = %@", indexPaths, ASIndexPathsForMultidimensionalArray(_editingNodes)); - ASDeleteElementsInMultidimensionalArrayAtIndexPaths(_editingNodes, indexPaths); + LOG(@"_deleteNodesAtIndexPaths:%@ ofKind:%@, full index paths in _editingNodes = %@", indexPaths, kind, ASIndexPathsForMultidimensionalArray(_editingNodes[kind])); + NSMutableArray *editingNodes = _editingNodes[kind]; + ASDeleteElementsInMultidimensionalArrayAtIndexPaths(editingNodes, indexPaths); + _editingNodes[kind] = editingNodes; ASDisplayNodePerformBlockOnMainThread(^{ - NSArray *nodes = ASFindElementsInMultidimensionalArrayAtIndexPaths(_completedNodes, indexPaths); - ASDeleteElementsInMultidimensionalArrayAtIndexPaths(_completedNodes, indexPaths); - if (_delegateDidDeleteNodes) - [_delegate dataController:self didDeleteNodes:nodes atIndexPaths:indexPaths withAnimationOptions:animationOptions]; + NSArray *nodes = ASFindElementsInMultidimensionalArrayAtIndexPaths(_completedNodes[kind], indexPaths); + ASDeleteElementsInMultidimensionalArrayAtIndexPaths(_completedNodes[kind], indexPaths); + if (completionBlock) { + completionBlock(nodes, indexPaths); + } }); } -- (void)_insertSections:(NSMutableArray *)sections atIndexSet:(NSIndexSet *)indexSet withAnimationOptions:(ASDataControllerAnimationOptions)animationOptions +- (void)insertSections:(NSMutableArray *)sections ofKind:(NSString *)kind atIndexSet:(NSIndexSet *)indexSet completion:(void (^)(NSArray *sections, NSIndexSet *indexSet))completionBlock { if (indexSet.count == 0) return; - [_editingNodes insertObjects:sections atIndexes:indexSet]; + + if (_editingNodes[kind] == nil) { + _editingNodes[kind] = [NSMutableArray array]; + } + + [_editingNodes[kind] insertObjects:sections atIndexes:indexSet]; // Deep copy is critical here, or future edits to the sub-arrays will pollute state between _editing and _complete on different threads. NSArray *sectionsForCompleted = (NSMutableArray *)ASMultidimensionalArrayDeepMutableCopy(sections); ASDisplayNodePerformBlockOnMainThread(^{ - [_completedNodes insertObjects:sectionsForCompleted atIndexes:indexSet]; - if (_delegateDidInsertSections) - [_delegate dataController:self didInsertSections:sections atIndexSet:indexSet withAnimationOptions:animationOptions]; + [_completedNodes[kind] insertObjects:sectionsForCompleted atIndexes:indexSet]; + if (completionBlock) { + completionBlock(sections, indexSet); + } }); } -- (void)_deleteSectionsAtIndexSet:(NSIndexSet *)indexSet withAnimationOptions:(ASDataControllerAnimationOptions)animationOptions +- (void)deleteSectionsOfKind:(NSString *)kind atIndexSet:(NSIndexSet *)indexSet completion:(void (^)(NSIndexSet *indexSet))completionBlock { if (indexSet.count == 0) return; - [_editingNodes removeObjectsAtIndexes:indexSet]; + [_editingNodes[kind] removeObjectsAtIndexes:indexSet]; ASDisplayNodePerformBlockOnMainThread(^{ - [_completedNodes removeObjectsAtIndexes:indexSet]; + [_completedNodes[kind] removeObjectsAtIndexes:indexSet]; + if (completionBlock) { + completionBlock(indexSet); + } + }); +} + +#pragma mark - Internal Data Querying + Editing + +/** + * Inserts the specified nodes into the given index paths and notifies the delegate of newly inserted nodes. + * + * @discussion Nodes are first inserted into the editing store, then the completed store is replaced by a deep copy + * of the editing nodes. The delegate is invoked on the main thread. + */ +- (void)_insertNodes:(NSArray *)nodes atIndexPaths:(NSArray *)indexPaths withAnimationOptions:(ASDataControllerAnimationOptions)animationOptions +{ + [self insertNodes:nodes ofKind:ASDataControllerRowNodeKind atIndexPaths:indexPaths completion:^(NSArray *nodes, NSArray *indexPaths) { + if (_delegateDidInsertNodes) + [_delegate dataController:self didInsertNodes:nodes atIndexPaths:indexPaths withAnimationOptions:animationOptions]; + }]; +} + +/** + * Removes the specified nodes at the given index paths and notifies the delegate of the nodes removed. + * + * @discussion Nodes are first removed from the editing store then removed from the completed store on the main thread. + * Once the backing stores are consistent, the delegate is invoked on the main thread. + */ +- (void)_deleteNodesAtIndexPaths:(NSArray *)indexPaths withAnimationOptions:(ASDataControllerAnimationOptions)animationOptions +{ + [self deleteNodesOfKind:ASDataControllerRowNodeKind atIndexPaths:indexPaths completion:^(NSArray *nodes, NSArray *indexPaths) { + if (_delegateDidDeleteNodes) + [_delegate dataController:self didDeleteNodes:nodes atIndexPaths:indexPaths withAnimationOptions:animationOptions]; + }]; +} + +/** + * Inserts sections, represented as arrays, into the backing store at the given indicies and notifies the delegate. + * + * @discussion The section arrays are inserted into the editing store, then a deep copy of the sections are inserted + * in the completed store on the main thread. The delegate is invoked on the main thread. + */ +- (void)_insertSections:(NSMutableArray *)sections atIndexSet:(NSIndexSet *)indexSet withAnimationOptions:(ASDataControllerAnimationOptions)animationOptions +{ + [self insertSections:sections ofKind:ASDataControllerRowNodeKind atIndexSet:indexSet completion:^(NSArray *sections, NSIndexSet *indexSet) { + if (_delegateDidInsertSections) + [_delegate dataController:self didInsertSections:sections atIndexSet:indexSet withAnimationOptions:animationOptions]; + }]; +} + +/** + * Removes sections at the given indicies from the backing store and notifies the delegate. + * + * @discussion Section array are first removed from the editing store, then the associated section in the completed + * store is removed on the main thread. The delegate is invoked on the main thread. + */ +- (void)_deleteSectionsAtIndexSet:(NSIndexSet *)indexSet withAnimationOptions:(ASDataControllerAnimationOptions)animationOptions +{ + [self deleteSectionsOfKind:ASDataControllerRowNodeKind atIndexSet:indexSet completion:^(NSIndexSet *indexSet) { if (_delegateDidDeleteSections) [_delegate dataController:self didDeleteSectionsAtIndexSet:indexSet withAnimationOptions:animationOptions]; - }); + }]; } #pragma mark - Initial Load & Full Reload (External API) @@ -239,7 +335,7 @@ static void *kASSizingQueueContext = &kASSizingQueueContext; ASDisplayNodeAssertMainThread(); [self accessDataSourceWithBlock:^{ NSMutableArray *indexPaths = [NSMutableArray array]; - NSUInteger sectionNum = [_dataSource dataControllerNumberOfSections:self]; + NSUInteger sectionNum = [_dataSource numberOfSectionsInDataController:self]; // insert sections [self insertSections:[NSIndexSet indexSetWithIndexesInRange:NSMakeRange(0, sectionNum)] withAnimationOptions:0]; @@ -266,24 +362,30 @@ static void *kASSizingQueueContext = &kASSizingQueueContext; [_editingTransactionQueue waitUntilAllOperationsAreFinished]; [self accessDataSourceWithBlock:^{ - NSUInteger sectionCount = [_dataSource dataControllerNumberOfSections:self]; + NSUInteger sectionCount = [_dataSource numberOfSectionsInDataController:self]; NSMutableArray *updatedNodes = [NSMutableArray array]; NSMutableArray *updatedIndexPaths = [NSMutableArray array]; [self _populateFromEntireDataSourceWithMutableNodes:updatedNodes mutableIndexPaths:updatedIndexPaths]; // Measure nodes whose views are loaded before we leave the main thread - [self _layoutNodesWithMainThreadAffinity:updatedNodes atIndexPaths:updatedIndexPaths]; + [self layoutLoadedNodes:updatedNodes ofKind:ASDataControllerRowNodeKind atIndexPaths:updatedIndexPaths]; + + // Allow subclasses to perform setup before going into the edit transaction + [self prepareForReloadData]; [_editingTransactionQueue addOperationWithBlock:^{ LOG(@"Edit Transaction - reloadData"); // Remove everything that existed before the reload, now that we're ready to insert replacements - NSArray *indexPaths = ASIndexPathsForMultidimensionalArray(_editingNodes); + NSArray *indexPaths = ASIndexPathsForMultidimensionalArray(_editingNodes[ASDataControllerRowNodeKind]); [self _deleteNodesAtIndexPaths:indexPaths withAnimationOptions:animationOptions]; - NSMutableIndexSet *indexSet = [[NSMutableIndexSet alloc] initWithIndexesInRange:NSMakeRange(0, _editingNodes.count)]; + NSMutableArray *editingNodes = _editingNodes[ASDataControllerRowNodeKind]; + NSMutableIndexSet *indexSet = [[NSMutableIndexSet alloc] initWithIndexesInRange:NSMakeRange(0, editingNodes.count)]; [self _deleteSectionsAtIndexSet:indexSet withAnimationOptions:animationOptions]; + [self willReloadData]; + // Insert each section NSMutableArray *sections = [NSMutableArray arrayWithCapacity:sectionCount]; for (int i = 0; i < sectionCount; i++) { @@ -304,6 +406,11 @@ static void *kASSizingQueueContext = &kASSizingQueueContext; #pragma mark - Data Source Access (Calling _dataSource) +/** + * Safely locks access to the data source and executes the given block, unlocking once complete. + * + * @discussion When `asyncDataFetching` is enabled, the block is executed on a background thread. + */ - (void)accessDataSourceWithBlock:(dispatch_block_t)block { if (_asyncDataFetchingEnabled) { @@ -319,6 +426,11 @@ static void *kASSizingQueueContext = &kASSizingQueueContext; } } +/** + * Fetches row nodes and their specified index paths for the provided sections from the data source. + * + * @discussion Results are stored in the passed mutable arrays. + */ - (void)_populateFromDataSourceWithSectionIndexSet:(NSIndexSet *)indexSet mutableNodes:(NSMutableArray *)nodes mutableIndexPaths:(NSMutableArray *)indexPaths { [indexSet enumerateIndexesUsingBlock:^(NSUInteger idx, BOOL *stop) { @@ -333,9 +445,14 @@ static void *kASSizingQueueContext = &kASSizingQueueContext; }]; } +/** + * Fetches row nodes and their specified index paths for all sections from the data source. + * + * @discussion Results are stored in the passed mutable arrays. + */ - (void)_populateFromEntireDataSourceWithMutableNodes:(NSMutableArray *)nodes mutableIndexPaths:(NSMutableArray *)indexPaths { - NSUInteger sectionNum = [_dataSource dataControllerNumberOfSections:self]; + NSUInteger sectionNum = [_dataSource numberOfSectionsInDataController:self]; for (NSUInteger i = 0; i < sectionNum; i++) { NSIndexPath *sectionIndexPath = [[NSIndexPath alloc] initWithIndex:i]; @@ -375,7 +492,7 @@ static void *kASSizingQueueContext = &kASSizingQueueContext; ASDisplayNodePerformBlockOnMainThread(^{ // Deep copy _completedNodes to _externalCompletedNodes. // Any external queries from now on will be done on _externalCompletedNodes, to guarantee data consistency with the delegate. - _externalCompletedNodes = (NSMutableArray *)ASMultidimensionalArrayDeepMutableCopy(_completedNodes); + _externalCompletedNodes = (NSMutableArray *)ASMultidimensionalArrayDeepMutableCopy(_completedNodes[ASDataControllerRowNodeKind]); LOG(@"endUpdatesWithCompletion - begin updates call to delegate"); [_delegate dataControllerBeginUpdates:self]; @@ -403,6 +520,12 @@ static void *kASSizingQueueContext = &kASSizingQueueContext; } } +/** + * Queues the given operation until an `endUpdates` synchronize update is completed. + * + * If this method is called outside of a begin/endUpdates batch update, the block is + * executed immediately. + */ - (void)performEditCommandWithBlock:(void (^)(void))block { // This method needs to block the thread and synchronously perform the operation if we are not @@ -416,49 +539,55 @@ static void *kASSizingQueueContext = &kASSizingQueueContext; #pragma mark - Section Editing (External API) -- (void)insertSections:(NSIndexSet *)indexSet withAnimationOptions:(ASDataControllerAnimationOptions)animationOptions +- (void)insertSections:(NSIndexSet *)sections withAnimationOptions:(ASDataControllerAnimationOptions)animationOptions { [self performEditCommandWithBlock:^{ ASDisplayNodeAssertMainThread(); - LOG(@"Edit Command - insertSections: %@", indexSet); + LOG(@"Edit Command - insertSections: %@", sections); [_editingTransactionQueue waitUntilAllOperationsAreFinished]; [self accessDataSourceWithBlock:^{ NSMutableArray *updatedNodes = [NSMutableArray array]; NSMutableArray *updatedIndexPaths = [NSMutableArray array]; - [self _populateFromDataSourceWithSectionIndexSet:indexSet mutableNodes:updatedNodes mutableIndexPaths:updatedIndexPaths]; + [self _populateFromDataSourceWithSectionIndexSet:sections mutableNodes:updatedNodes mutableIndexPaths:updatedIndexPaths]; // Measure nodes whose views are loaded before we leave the main thread - [self _layoutNodesWithMainThreadAffinity:updatedNodes atIndexPaths:updatedIndexPaths]; + [self layoutLoadedNodes:updatedNodes ofKind:ASDataControllerRowNodeKind atIndexPaths:updatedIndexPaths]; + + [self prepareForInsertSections:sections]; [_editingTransactionQueue addOperationWithBlock:^{ - LOG(@"Edit Transaction - insertSections: %@", indexSet); - NSMutableArray *sectionArray = [NSMutableArray arrayWithCapacity:indexSet.count]; - for (NSUInteger i = 0; i < indexSet.count; i++) { + [self willInsertSections:sections]; + + LOG(@"Edit Transaction - insertSections: %@", sections); + NSMutableArray *sectionArray = [NSMutableArray arrayWithCapacity:sections.count]; + for (NSUInteger i = 0; i < sections.count; i++) { [sectionArray addObject:[NSMutableArray array]]; } - [self _insertSections:sectionArray atIndexSet:indexSet withAnimationOptions:animationOptions]; + [self _insertSections:sectionArray atIndexSet:sections withAnimationOptions:animationOptions]; [self _batchLayoutNodes:updatedNodes atIndexPaths:updatedIndexPaths withAnimationOptions:animationOptions]; }]; }]; }]; } -- (void)deleteSections:(NSIndexSet *)indexSet withAnimationOptions:(ASDataControllerAnimationOptions)animationOptions +- (void)deleteSections:(NSIndexSet *)sections withAnimationOptions:(ASDataControllerAnimationOptions)animationOptions { [self performEditCommandWithBlock:^{ ASDisplayNodeAssertMainThread(); - LOG(@"Edit Command - deleteSections: %@", indexSet); + LOG(@"Edit Command - deleteSections: %@", sections); [_editingTransactionQueue waitUntilAllOperationsAreFinished]; [_editingTransactionQueue addOperationWithBlock:^{ + [self willDeleteSections:sections]; + // remove elements - LOG(@"Edit Transaction - deleteSections: %@", indexSet); - NSArray *indexPaths = ASIndexPathsForMultidimensionalArrayAtIndexSet(_editingNodes, indexSet); + LOG(@"Edit Transaction - deleteSections: %@", sections); + NSArray *indexPaths = ASIndexPathsForMultidimensionalArrayAtIndexSet(_editingNodes[ASDataControllerRowNodeKind], sections); [self _deleteNodesAtIndexPaths:indexPaths withAnimationOptions:animationOptions]; - [self _deleteSectionsAtIndexSet:indexSet withAnimationOptions:animationOptions]; + [self _deleteSectionsAtIndexSet:sections withAnimationOptions:animationOptions]; }]; }]; } @@ -481,12 +610,16 @@ static void *kASSizingQueueContext = &kASSizingQueueContext; // at this time. Thus _editingNodes could be empty and crash in ASIndexPathsForMultidimensional[...] // Measure nodes whose views are loaded before we leave the main thread - [self _layoutNodesWithMainThreadAffinity:updatedNodes atIndexPaths:updatedIndexPaths]; + [self layoutLoadedNodes:updatedNodes ofKind:ASDataControllerRowNodeKind atIndexPaths:updatedIndexPaths]; + + [self prepareForReloadSections:sections]; [_editingTransactionQueue addOperationWithBlock:^{ - NSArray *indexPaths = ASIndexPathsForMultidimensionalArrayAtIndexSet(_editingNodes, sections); + [self willReloadSections:sections]; + + NSArray *indexPaths = ASIndexPathsForMultidimensionalArrayAtIndexSet(_editingNodes[ASDataControllerRowNodeKind], sections); - LOG(@"Edit Transaction - reloadSections: updatedIndexPaths: %@, indexPaths: %@, _editingNodes: %@", updatedIndexPaths, indexPaths, ASIndexPathsForMultidimensionalArray(_editingNodes)); + LOG(@"Edit Transaction - reloadSections: updatedIndexPaths: %@, indexPaths: %@, _editingNodes: %@", updatedIndexPaths, indexPaths, ASIndexPathsForMultidimensionalArray(_editingNodes[ASDataControllerRowNodeKind])); [self _deleteNodesAtIndexPaths:indexPaths withAnimationOptions:animationOptions]; @@ -506,12 +639,14 @@ static void *kASSizingQueueContext = &kASSizingQueueContext; [_editingTransactionQueue waitUntilAllOperationsAreFinished]; [_editingTransactionQueue addOperationWithBlock:^{ + [self willMoveSection:section toSection:newSection]; + // remove elements LOG(@"Edit Transaction - moveSection"); - NSArray *indexPaths = ASIndexPathsForMultidimensionalArrayAtIndexSet(_editingNodes, [NSIndexSet indexSetWithIndex:section]); - NSArray *nodes = ASFindElementsInMultidimensionalArrayAtIndexPaths(_editingNodes, indexPaths); + NSArray *indexPaths = ASIndexPathsForMultidimensionalArrayAtIndexSet(_editingNodes[ASDataControllerRowNodeKind], [NSIndexSet indexSetWithIndex:section]); + NSArray *nodes = ASFindElementsInMultidimensionalArrayAtIndexPaths(_editingNodes[ASDataControllerRowNodeKind], indexPaths); [self _deleteNodesAtIndexPaths:indexPaths withAnimationOptions:animationOptions]; // update the section of indexpaths @@ -527,6 +662,49 @@ static void *kASSizingQueueContext = &kASSizingQueueContext; }]; } + +#pragma mark - Backing store manipulation optional hooks (Subclass API) + +- (void)prepareForReloadData +{ + // Optional template hook for subclasses (See ASDataController+Subclasses.h) +} + +- (void)willReloadData +{ + // Optional template hook for subclasses (See ASDataController+Subclasses.h) +} + +- (void)prepareForInsertSections:(NSIndexSet *)sections +{ + // Optional template hook for subclasses (See ASDataController+Subclasses.h) +} + +- (void)willInsertSections:(NSIndexSet *)sections +{ + // Optional template hook for subclasses (See ASDataController+Subclasses.h) +} + +- (void)willDeleteSections:(NSIndexSet *)sections +{ + // Optional template hook for subclasses (See ASDataController+Subclasses.h) +} + +- (void)prepareForReloadSections:(NSIndexSet *)sections +{ + // Optional template hook for subclasses (See ASDataController+Subclasses.h) +} + +- (void)willReloadSections:(NSIndexSet *)sections +{ + // Optional template hook for subclasses (See ASDataController+Subclasses.h) +} + +- (void)willMoveSection:(NSInteger)section toSection:(NSInteger)newSection +{ + // Optional template hook for subclasses (See ASDataController+Subclasses.h) +} + #pragma mark - Row Editing (External API) - (void)insertRowsAtIndexPaths:(NSArray *)indexPaths withAnimationOptions:(ASDataControllerAnimationOptions)animationOptions @@ -546,7 +724,7 @@ static void *kASSizingQueueContext = &kASSizingQueueContext; } // Measure nodes whose views are loaded before we leave the main thread - [self _layoutNodesWithMainThreadAffinity:nodes atIndexPaths:indexPaths]; + [self layoutLoadedNodes:nodes ofKind:ASDataControllerRowNodeKind atIndexPaths:indexPaths]; [_editingTransactionQueue addOperationWithBlock:^{ LOG(@"Edit Transaction - insertRows: %@", indexPaths); @@ -596,7 +774,7 @@ static void *kASSizingQueueContext = &kASSizingQueueContext; } // Measure nodes whose views are loaded before we leave the main thread - [self _layoutNodesWithMainThreadAffinity:nodes atIndexPaths:indexPaths]; + [self layoutLoadedNodes:nodes ofKind:ASDataControllerRowNodeKind atIndexPaths:indexPaths]; [_editingTransactionQueue addOperationWithBlock:^{ LOG(@"Edit Transaction - reloadRows: %@", indexPaths); @@ -607,41 +785,46 @@ static void *kASSizingQueueContext = &kASSizingQueueContext; }]; } -- (void)relayoutAllRows +- (void)relayoutAllNodes { [self performEditCommandWithBlock:^{ ASDisplayNodeAssertMainThread(); LOG(@"Edit Command - relayoutRows"); [_editingTransactionQueue waitUntilAllOperationsAreFinished]; - - void (^relayoutNodesBlock)(NSMutableArray *) = ^void(NSMutableArray *nodes) { - if (!nodes.count) { - return; - } - - [self accessDataSourceWithBlock:^{ - [nodes enumerateObjectsUsingBlock:^(NSMutableArray *section, NSUInteger sectionIndex, BOOL *stop) { - [section enumerateObjectsUsingBlock:^(ASCellNode *node, NSUInteger rowIndex, BOOL *stop) { - NSIndexPath *indexPath = [NSIndexPath indexPathForRow:rowIndex inSection:sectionIndex]; - ASSizeRange constrainedSize = [_dataSource dataController:self constrainedSizeForNodeAtIndexPath:indexPath]; - [node measureWithSizeRange:constrainedSize]; - node.frame = CGRectMake(0.0f, 0.0f, node.calculatedSize.width, node.calculatedSize.height); - }]; - }]; - }]; - }; // Can't relayout right away because _completedNodes may not be up-to-date, // i.e there might be some nodes that were measured using the old constrained size but haven't been added to _completedNodes // (see _layoutNodes:atIndexPaths:withAnimationOptions:). [_editingTransactionQueue addOperationWithBlock:^{ ASDisplayNodePerformBlockOnMainThread(^{ - relayoutNodesBlock(_completedNodes); + for (NSString *kind in [_completedNodes keyEnumerator]) { + [self _relayoutNodesOfKind:kind]; + } }); }]; }]; } +- (void)_relayoutNodesOfKind:(NSString *)kind +{ + ASDisplayNodeAssertMainThread(); + NSArray *nodes = [self completedNodesOfKind:kind]; + if (!nodes.count) { + return; + } + + [self accessDataSourceWithBlock:^{ + [nodes enumerateObjectsUsingBlock:^(NSMutableArray *section, NSUInteger sectionIndex, BOOL *stop) { + [section enumerateObjectsUsingBlock:^(ASCellNode *node, NSUInteger rowIndex, BOOL *stop) { + NSIndexPath *indexPath = [NSIndexPath indexPathForRow:rowIndex inSection:sectionIndex]; + ASSizeRange constrainedSize = [self constrainedSizeForNodeOfKind:kind atIndexPath:indexPath]; + [node measureWithSizeRange:constrainedSize]; + node.frame = CGRectMake(0.0f, 0.0f, node.calculatedSize.width, node.calculatedSize.height); + }]; + }]; + }]; +} + - (void)moveRowAtIndexPath:(NSIndexPath *)indexPath toIndexPath:(NSIndexPath *)newIndexPath withAnimationOptions:(ASDataControllerAnimationOptions)animationOptions { [self performEditCommandWithBlock:^{ @@ -651,7 +834,7 @@ static void *kASSizingQueueContext = &kASSizingQueueContext; [_editingTransactionQueue addOperationWithBlock:^{ LOG(@"Edit Transaction - moveRow: %@ > %@", indexPath, newIndexPath); - NSArray *nodes = ASFindElementsInMultidimensionalArrayAtIndexPaths(_editingNodes, [NSArray arrayWithObject:indexPath]); + NSArray *nodes = ASFindElementsInMultidimensionalArrayAtIndexPaths(_editingNodes[ASDataControllerRowNodeKind], [NSArray arrayWithObject:indexPath]); NSArray *indexPaths = [NSArray arrayWithObject:indexPath]; [self _deleteNodesAtIndexPaths:indexPaths withAnimationOptions:animationOptions]; @@ -662,6 +845,23 @@ static void *kASSizingQueueContext = &kASSizingQueueContext; }]; } +#pragma mark - Data Querying (Subclass API) + +- (NSArray *)indexPathsForEditingNodesOfKind:(NSString *)kind +{ + return _editingNodes[kind] != nil ? ASIndexPathsForMultidimensionalArray(_editingNodes[kind]) : [NSArray array]; +} + +- (NSMutableArray *)editingNodesOfKind:(NSString *)kind +{ + return _editingNodes[kind] != nil ? _editingNodes[kind] : [NSMutableArray array]; +} + +- (NSMutableArray *)completedNodesOfKind:(NSString *)kind +{ + return _completedNodes[kind]; +} + #pragma mark - Data Querying (External API) - (NSUInteger)numberOfSections @@ -711,7 +911,7 @@ static void *kASSizingQueueContext = &kASSizingQueueContext; - (NSArray *)completedNodes { ASDisplayNodeAssertMainThread(); - return _externalCompletedNodes != nil ? _externalCompletedNodes : _completedNodes; + return _externalCompletedNodes != nil ? _externalCompletedNodes : _completedNodes[ASDataControllerRowNodeKind]; } #pragma mark - Dealloc @@ -719,15 +919,17 @@ static void *kASSizingQueueContext = &kASSizingQueueContext; - (void)dealloc { ASDisplayNodeAssertMainThread(); - [_completedNodes enumerateObjectsUsingBlock:^(NSMutableArray *section, NSUInteger sectionIndex, BOOL *stop) { - [section enumerateObjectsUsingBlock:^(ASCellNode *node, NSUInteger rowIndex, BOOL *stop) { - if (node.isNodeLoaded) { - if (node.layerBacked) { - [node.layer removeFromSuperlayer]; - } else { - [node.view removeFromSuperview]; + [_completedNodes enumerateKeysAndObjectsUsingBlock:^(NSString *kind, NSMutableArray *nodes, BOOL *stop) { + [nodes enumerateObjectsUsingBlock:^(NSMutableArray *section, NSUInteger sectionIndex, BOOL *stop) { + [section enumerateObjectsUsingBlock:^(ASCellNode *node, NSUInteger rowIndex, BOOL *stop) { + if (node.isNodeLoaded) { + if (node.layerBacked) { + [node.layer removeFromSuperlayer]; + } else { + [node.view removeFromSuperview]; + } } - } + }]; }]; }]; } diff --git a/AsyncDisplayKit/Details/ASRangeController.h b/AsyncDisplayKit/Details/ASRangeController.h index 9d5bbe57f0..cf4cc37354 100644 --- a/AsyncDisplayKit/Details/ASRangeController.h +++ b/AsyncDisplayKit/Details/ASRangeController.h @@ -41,7 +41,7 @@ * * @param contentView UIView to add a (sized) node's view to. * - * @param node The ASCellNode to be added. + * @param cellNode The cell node to be added. */ - (void)configureContentView:(UIView *)contentView forCellNode:(ASCellNode *)node; diff --git a/AsyncDisplayKit/Details/ASRangeController.mm b/AsyncDisplayKit/Details/ASRangeController.mm index b22db853bf..146b85897f 100644 --- a/AsyncDisplayKit/Details/ASRangeController.mm +++ b/AsyncDisplayKit/Details/ASRangeController.mm @@ -48,7 +48,7 @@ #pragma mark - View manipulation -- (void)moveNode:(ASCellNode *)node toView:(UIView *)view +- (void)moveCellNode:(ASCellNode *)node toView:(UIView *)view { ASDisplayNodeAssertMainThread(); ASDisplayNodeAssert(node, @"Cannot move a nil node to a view"); @@ -75,7 +75,7 @@ // coalesce these events -- handling them multiple times per runloop is noisy and expensive _queuedRangeUpdate = YES; - + [self performSelector:@selector(updateVisibleNodeIndexPaths) withObject:nil afterDelay:0 @@ -158,9 +158,9 @@ return rangeType == ASLayoutRangeTypeRender; } -- (void)configureContentView:(UIView *)contentView forCellNode:(ASCellNode *)cellNode +- (void)configureContentView:(UIView *)contentView forCellNode:(ASCellNode *)node { - if (cellNode.view.superview == contentView) { + if (node.view.superview == contentView) { // this content view is already correctly configured return; } @@ -170,7 +170,7 @@ [view removeFromSuperview]; } - [self moveNode:cellNode toView:contentView]; + [self moveCellNode:node toView:contentView]; } diff --git a/AsyncDisplayKitTests/ASCollectionViewFlowLayoutInspectorTests.m b/AsyncDisplayKitTests/ASCollectionViewFlowLayoutInspectorTests.m new file mode 100644 index 0000000000..1230807f44 --- /dev/null +++ b/AsyncDisplayKitTests/ASCollectionViewFlowLayoutInspectorTests.m @@ -0,0 +1,368 @@ +/* Copyright (c) 2015-present, Facebook, Inc. + * All rights reserved. + * + * This source code is licensed under the BSD-style license found in the + * LICENSE file in the root directory of this source tree. An additional grant + * of patent rights can be found in the PATENTS file in the same directory. + */ + +#import +#import + +#import "ASCollectionView.h" +#import "ASCollectionViewFlowLayoutInspector.h" + +/** + * Test Data Source + */ +@interface InspectorTestDataSource : NSObject +@end + +@implementation InspectorTestDataSource + +- (ASCellNode *)collectionView:(ASCollectionView *)collectionView nodeForItemAtIndexPath:(NSIndexPath *)indexPath +{ + return [[ASCellNode alloc] init]; +} + +- (NSInteger)collectionView:(UICollectionView *)collectionView numberOfItemsInSection:(NSInteger)section +{ + return 0; +} + +- (NSInteger)numberOfSectionsInCollectionView:(UICollectionView *)collectionView +{ + return 2; +} + +@end + +@interface ASCollectionViewFlowLayoutInspectorTests : XCTestCase + +@end + +/** + * Test Delegate for Header Reference Size Implementation + */ +@interface HeaderReferenceSizeTestDelegate : NSObject + +@end + +@implementation HeaderReferenceSizeTestDelegate + +- (CGSize)collectionView:(ASCollectionView *)collectionView layout:(UICollectionViewLayout *)collectionViewLayout referenceSizeForHeaderInSection:(NSInteger)section +{ + return CGSizeMake(125.0, 125.0); +} + +@end + +/** + * Test Delegate for Footer Reference Size Implementation + */ +@interface FooterReferenceSizeTestDelegate : NSObject + +@end + +@implementation FooterReferenceSizeTestDelegate + +- (CGSize)collectionView:(ASCollectionView *)collectionView layout:(UICollectionViewLayout *)collectionViewLayout referenceSizeForFooterInSection:(NSInteger)section +{ + return CGSizeMake(125.0, 125.0); +} + +@end + +@implementation ASCollectionViewFlowLayoutInspectorTests + +- (void)setUp { + [super setUp]; + // Put setup code here. This method is called before the invocation of each test method in the class. +} + +- (void)tearDown { + // Put teardown code here. This method is called after the invocation of each test method in the class. + [super tearDown]; +} + +#pragma mark - #collectionView:constrainedSizeForSupplementaryNodeOfKind:atIndexPath: + +// Vertical + +// Delegate implementation + +- (void)testThatItReturnsAVerticalConstrainedSizeFromTheHeaderDelegateImplementation +{ + InspectorTestDataSource *dataSource = [[InspectorTestDataSource alloc] init]; + HeaderReferenceSizeTestDelegate *delegate = [[HeaderReferenceSizeTestDelegate alloc] init]; + + UICollectionViewFlowLayout *layout = [[UICollectionViewFlowLayout alloc] init]; + layout.scrollDirection = UICollectionViewScrollDirectionVertical; + + CGRect rect = CGRectMake(0, 0, 100.0, 100.0); + ASCollectionView *collectionView = [[ASCollectionView alloc] initWithFrame:rect collectionViewLayout:layout asyncDataFetching:NO]; + collectionView.asyncDataSource = dataSource; + collectionView.asyncDelegate = delegate; + + ASCollectionViewFlowLayoutInspector *inspector = [[ASCollectionViewFlowLayoutInspector alloc] initWithCollectionView:collectionView flowLayout:layout]; + ASSizeRange size = [inspector collectionView:collectionView constrainedSizeForSupplementaryNodeOfKind:UICollectionElementKindSectionHeader atIndexPath:[NSIndexPath indexPathForItem:0 inSection:0]]; + ASSizeRange sizeCompare = ASSizeRangeMake(CGSizeZero, CGSizeMake(collectionView.bounds.size.width, 125.0)); + XCTAssert(CGSizeEqualToSize(size.min, sizeCompare.min) && CGSizeEqualToSize(size.max, sizeCompare.max), @"should have a size constrained by the values returned in the delegate implementation"); + + collectionView.asyncDataSource = nil; + collectionView.asyncDelegate = nil; +} + +- (void)testThatItReturnsAVerticalConstrainedSizeFromTheFooterDelegateImplementation +{ + InspectorTestDataSource *dataSource = [[InspectorTestDataSource alloc] init]; + FooterReferenceSizeTestDelegate *delegate = [[FooterReferenceSizeTestDelegate alloc] init]; + + UICollectionViewFlowLayout *layout = [[UICollectionViewFlowLayout alloc] init]; + layout.scrollDirection = UICollectionViewScrollDirectionVertical; + + CGRect rect = CGRectMake(0, 0, 100.0, 100.0); + ASCollectionView *collectionView = [[ASCollectionView alloc] initWithFrame:rect collectionViewLayout:layout asyncDataFetching:NO]; + collectionView.asyncDataSource = dataSource; + collectionView.asyncDelegate = delegate; + + ASCollectionViewFlowLayoutInspector *inspector = [[ASCollectionViewFlowLayoutInspector alloc] initWithCollectionView:collectionView flowLayout:layout]; + ASSizeRange size = [inspector collectionView:collectionView constrainedSizeForSupplementaryNodeOfKind:UICollectionElementKindSectionFooter atIndexPath:[NSIndexPath indexPathForItem:0 inSection:0]]; + ASSizeRange sizeCompare = ASSizeRangeMake(CGSizeZero, CGSizeMake(collectionView.bounds.size.width, 125.0)); + XCTAssert(CGSizeEqualToSize(size.min, sizeCompare.min) && CGSizeEqualToSize(size.max, sizeCompare.max), @"should have a size constrained by the values returned in the delegate implementation"); + + collectionView.asyncDataSource = nil; + collectionView.asyncDelegate = nil; +} + +// Size implementation + +- (void)testThatItReturnsAVerticalConstrainedSizeFromTheHeaderProperty +{ + InspectorTestDataSource *dataSource = [[InspectorTestDataSource alloc] init]; + + UICollectionViewFlowLayout *layout = [[UICollectionViewFlowLayout alloc] init]; + layout.scrollDirection = UICollectionViewScrollDirectionVertical; + layout.headerReferenceSize = CGSizeMake(125.0, 125.0); + + CGRect rect = CGRectMake(0, 0, 100.0, 100.0); + ASCollectionView *collectionView = [[ASCollectionView alloc] initWithFrame:rect collectionViewLayout:layout asyncDataFetching:NO]; + collectionView.asyncDataSource = dataSource; + + ASCollectionViewFlowLayoutInspector *inspector = [[ASCollectionViewFlowLayoutInspector alloc] initWithCollectionView:collectionView flowLayout:layout]; + ASSizeRange size = [inspector collectionView:collectionView constrainedSizeForSupplementaryNodeOfKind:UICollectionElementKindSectionHeader atIndexPath:[NSIndexPath indexPathForItem:0 inSection:0]]; + ASSizeRange sizeCompare = ASSizeRangeMake(CGSizeZero, CGSizeMake(collectionView.bounds.size.width, 125.0)); + XCTAssert(CGSizeEqualToSize(size.min, sizeCompare.min) && CGSizeEqualToSize(size.max, sizeCompare.max), @"should have a size constrained by the size set on the layout"); + + collectionView.asyncDataSource = nil; + collectionView.asyncDelegate = nil; +} + +- (void)testThatItReturnsAVerticalConstrainedSizeFromTheFooterProperty +{ + InspectorTestDataSource *dataSource = [[InspectorTestDataSource alloc] init]; + + UICollectionViewFlowLayout *layout = [[UICollectionViewFlowLayout alloc] init]; + layout.scrollDirection = UICollectionViewScrollDirectionVertical; + layout.footerReferenceSize = CGSizeMake(125.0, 125.0); + + CGRect rect = CGRectMake(0, 0, 100.0, 100.0); + ASCollectionView *collectionView = [[ASCollectionView alloc] initWithFrame:rect collectionViewLayout:layout asyncDataFetching:NO]; + collectionView.asyncDataSource = dataSource; + + ASCollectionViewFlowLayoutInspector *inspector = [[ASCollectionViewFlowLayoutInspector alloc] initWithCollectionView:collectionView flowLayout:layout]; + ASSizeRange size = [inspector collectionView:collectionView constrainedSizeForSupplementaryNodeOfKind:UICollectionElementKindSectionFooter atIndexPath:[NSIndexPath indexPathForItem:0 inSection:0]]; + ASSizeRange sizeCompare = ASSizeRangeMake(CGSizeZero, CGSizeMake(collectionView.bounds.size.width, 125.0)); + XCTAssert(CGSizeEqualToSize(size.min, sizeCompare.min) && CGSizeEqualToSize(size.max, sizeCompare.max), @"should have a size constrained by the size set on the layout"); + + collectionView.asyncDataSource = nil; + collectionView.asyncDelegate = nil; +} + +// Horizontal + +- (void)testThatItReturnsAHorizontalConstrainedSizeFromTheHeaderDelegateImplementation +{ + InspectorTestDataSource *dataSource = [[InspectorTestDataSource alloc] init]; + HeaderReferenceSizeTestDelegate *delegate = [[HeaderReferenceSizeTestDelegate alloc] init]; + + UICollectionViewFlowLayout *layout = [[UICollectionViewFlowLayout alloc] init]; + layout.scrollDirection = UICollectionViewScrollDirectionHorizontal; + + CGRect rect = CGRectMake(0, 0, 100.0, 100.0); + ASCollectionView *collectionView = [[ASCollectionView alloc] initWithFrame:rect collectionViewLayout:layout asyncDataFetching:NO]; + collectionView.asyncDataSource = dataSource; + collectionView.asyncDelegate = delegate; + + ASCollectionViewFlowLayoutInspector *inspector = [[ASCollectionViewFlowLayoutInspector alloc] initWithCollectionView:collectionView flowLayout:layout]; + ASSizeRange size = [inspector collectionView:collectionView constrainedSizeForSupplementaryNodeOfKind:UICollectionElementKindSectionHeader atIndexPath:[NSIndexPath indexPathForItem:0 inSection:0]]; + ASSizeRange sizeCompare = ASSizeRangeMake(CGSizeZero, CGSizeMake(125.0, collectionView.bounds.size.height)); + XCTAssert(CGSizeEqualToSize(size.min, sizeCompare.min) && CGSizeEqualToSize(size.max, sizeCompare.max), @"should have a size constrained by the values returned in the delegate implementation"); + + collectionView.asyncDataSource = nil; + collectionView.asyncDelegate = nil; +} + +- (void)testThatItReturnsAHorizontalConstrainedSizeFromTheFooterDelegateImplementation +{ + InspectorTestDataSource *dataSource = [[InspectorTestDataSource alloc] init]; + FooterReferenceSizeTestDelegate *delegate = [[FooterReferenceSizeTestDelegate alloc] init]; + + UICollectionViewFlowLayout *layout = [[UICollectionViewFlowLayout alloc] init]; + layout.scrollDirection = UICollectionViewScrollDirectionHorizontal; + + CGRect rect = CGRectMake(0, 0, 100.0, 100.0); + ASCollectionView *collectionView = [[ASCollectionView alloc] initWithFrame:rect collectionViewLayout:layout asyncDataFetching:NO]; + collectionView.asyncDataSource = dataSource; + collectionView.asyncDelegate = delegate; + + ASCollectionViewFlowLayoutInspector *inspector = [[ASCollectionViewFlowLayoutInspector alloc] initWithCollectionView:collectionView flowLayout:layout]; + ASSizeRange size = [inspector collectionView:collectionView constrainedSizeForSupplementaryNodeOfKind:UICollectionElementKindSectionFooter atIndexPath:[NSIndexPath indexPathForItem:0 inSection:0]]; + ASSizeRange sizeCompare = ASSizeRangeMake(CGSizeZero, CGSizeMake(125.0, collectionView.bounds.size.height)); + XCTAssert(CGSizeEqualToSize(size.min, sizeCompare.min) && CGSizeEqualToSize(size.max, sizeCompare.max), @"should have a size constrained by the values returned in the delegate implementation"); + + collectionView.asyncDataSource = nil; + collectionView.asyncDelegate = nil; +} + +// Size implementation + +- (void)testThatItReturnsAHorizontalConstrainedSizeFromTheHeaderProperty +{ + InspectorTestDataSource *dataSource = [[InspectorTestDataSource alloc] init]; + + UICollectionViewFlowLayout *layout = [[UICollectionViewFlowLayout alloc] init]; + layout.scrollDirection = UICollectionViewScrollDirectionHorizontal; + layout.headerReferenceSize = CGSizeMake(125.0, 125.0); + + CGRect rect = CGRectMake(0, 0, 100.0, 100.0); + ASCollectionView *collectionView = [[ASCollectionView alloc] initWithFrame:rect collectionViewLayout:layout asyncDataFetching:NO]; + collectionView.asyncDataSource = dataSource; + + ASCollectionViewFlowLayoutInspector *inspector = [[ASCollectionViewFlowLayoutInspector alloc] initWithCollectionView:collectionView flowLayout:layout]; + ASSizeRange size = [inspector collectionView:collectionView constrainedSizeForSupplementaryNodeOfKind:UICollectionElementKindSectionHeader atIndexPath:[NSIndexPath indexPathForItem:0 inSection:0]]; + ASSizeRange sizeCompare = ASSizeRangeMake(CGSizeZero, CGSizeMake(125.0, collectionView.bounds.size.width)); + XCTAssert(CGSizeEqualToSize(size.min, sizeCompare.min) && CGSizeEqualToSize(size.max, sizeCompare.max), @"should have a size constrained by the size set on the layout"); + + collectionView.asyncDataSource = nil; + collectionView.asyncDelegate = nil; +} + +- (void)testThatItReturnsAHorizontalConstrainedSizeFromTheFooterProperty +{ + InspectorTestDataSource *dataSource = [[InspectorTestDataSource alloc] init]; + + UICollectionViewFlowLayout *layout = [[UICollectionViewFlowLayout alloc] init]; + layout.scrollDirection = UICollectionViewScrollDirectionHorizontal; + layout.footerReferenceSize = CGSizeMake(125.0, 125.0); + + CGRect rect = CGRectMake(0, 0, 100.0, 100.0); + ASCollectionView *collectionView = [[ASCollectionView alloc] initWithFrame:rect collectionViewLayout:layout asyncDataFetching:NO]; + collectionView.asyncDataSource = dataSource; + + ASCollectionViewFlowLayoutInspector *inspector = [[ASCollectionViewFlowLayoutInspector alloc] initWithCollectionView:collectionView flowLayout:layout]; + ASSizeRange size = [inspector collectionView:collectionView constrainedSizeForSupplementaryNodeOfKind:UICollectionElementKindSectionFooter atIndexPath:[NSIndexPath indexPathForItem:0 inSection:0]]; + ASSizeRange sizeCompare = ASSizeRangeMake(CGSizeZero, CGSizeMake(125.0, collectionView.bounds.size.height)); + XCTAssert(CGSizeEqualToSize(size.min, sizeCompare.min) && CGSizeEqualToSize(size.max, sizeCompare.max), @"should have a size constrained by the size set on the layout"); + + collectionView.asyncDataSource = nil; + collectionView.asyncDelegate = nil; +} + +- (void)testThatItReturnsZeroSizeWhenNoReferenceSizeIsImplemented +{ + InspectorTestDataSource *dataSource = [[InspectorTestDataSource alloc] init]; + HeaderReferenceSizeTestDelegate *delegate = [[HeaderReferenceSizeTestDelegate alloc] init]; + UICollectionViewFlowLayout *layout = [[UICollectionViewFlowLayout alloc] init]; + ASCollectionView *collectionView = [[ASCollectionView alloc] initWithFrame:CGRectZero collectionViewLayout:layout asyncDataFetching:NO]; + collectionView.asyncDataSource = dataSource; + collectionView.asyncDelegate = delegate; + ASCollectionViewFlowLayoutInspector *inspector = [[ASCollectionViewFlowLayoutInspector alloc] initWithCollectionView:collectionView flowLayout:layout]; + ASSizeRange size = [inspector collectionView:collectionView constrainedSizeForSupplementaryNodeOfKind:UICollectionElementKindSectionFooter atIndexPath:[NSIndexPath indexPathForItem:0 inSection:0]]; + ASSizeRange sizeCompare = ASSizeRangeMake(CGSizeZero, CGSizeZero); + XCTAssert(CGSizeEqualToSize(size.min, sizeCompare.min) && CGSizeEqualToSize(size.max, sizeCompare.max), @"should have a zero size"); + + collectionView.asyncDataSource = nil; + collectionView.asyncDelegate = nil; +} + +#pragma mark - #collectionView:numberOfSectionsForSupplementaryNodeOfKind: + +- (void)testThatItRespondsWithTheDefaultNumberOfSections +{ + UICollectionViewFlowLayout *layout = [[UICollectionViewFlowLayout alloc] init]; + ASCollectionView *collectionView = [[ASCollectionView alloc] initWithFrame:CGRectZero collectionViewLayout:layout asyncDataFetching:NO]; + ASCollectionViewFlowLayoutInspector *inspector = [[ASCollectionViewFlowLayoutInspector alloc] initWithCollectionView:collectionView flowLayout:layout]; + NSUInteger sections = [inspector collectionView:collectionView numberOfSectionsForSupplementaryNodeOfKind:UICollectionElementKindSectionHeader]; + XCTAssert(sections == 1, @"should return 1 by default"); + + collectionView.asyncDataSource = nil; + collectionView.asyncDelegate = nil; +} + +- (void)testThatItProvidesTheNumberOfSectionsInTheDataSource +{ + InspectorTestDataSource *dataSource = [[InspectorTestDataSource alloc] init]; + UICollectionViewFlowLayout *layout = [[UICollectionViewFlowLayout alloc] init]; + ASCollectionView *collectionView = [[ASCollectionView alloc] initWithFrame:CGRectZero collectionViewLayout:layout asyncDataFetching:NO]; + collectionView.asyncDataSource = dataSource; + ASCollectionViewFlowLayoutInspector *inspector = [[ASCollectionViewFlowLayoutInspector alloc] initWithCollectionView:collectionView flowLayout:layout]; + NSUInteger sections = [inspector collectionView:collectionView numberOfSectionsForSupplementaryNodeOfKind:UICollectionElementKindSectionHeader]; + XCTAssert(sections == 2, @"should return 2"); + + collectionView.asyncDataSource = nil; + collectionView.asyncDelegate = nil; +} + +#pragma mark - #collectionView:supplementaryNodesOfKind:inSection: + +- (void)testThatItReturnsOneWhenAValidSizeIsImplementedOnTheDelegate +{ + InspectorTestDataSource *dataSource = [[InspectorTestDataSource alloc] init]; + HeaderReferenceSizeTestDelegate *delegate = [[HeaderReferenceSizeTestDelegate alloc] init]; + UICollectionViewFlowLayout *layout = [[UICollectionViewFlowLayout alloc] init]; + ASCollectionView *collectionView = [[ASCollectionView alloc] initWithFrame:CGRectZero collectionViewLayout:layout asyncDataFetching:NO]; + collectionView.asyncDataSource = dataSource; + collectionView.asyncDelegate = delegate; + ASCollectionViewFlowLayoutInspector *inspector = [[ASCollectionViewFlowLayoutInspector alloc] initWithCollectionView:collectionView flowLayout:layout]; + NSUInteger count = [inspector collectionView:collectionView supplementaryNodesOfKind:UICollectionElementKindSectionHeader inSection:0]; + XCTAssert(count == 1, @"should have a header supplementary view"); + + collectionView.asyncDataSource = nil; + collectionView.asyncDelegate = nil; +} + +- (void)testThatItReturnsOneWhenAValidSizeIsImplementedOnTheLayout +{ + InspectorTestDataSource *dataSource = [[InspectorTestDataSource alloc] init]; + HeaderReferenceSizeTestDelegate *delegate = [[HeaderReferenceSizeTestDelegate alloc] init]; + UICollectionViewFlowLayout *layout = [[UICollectionViewFlowLayout alloc] init]; + layout.footerReferenceSize = CGSizeMake(125.0, 125.0); + ASCollectionView *collectionView = [[ASCollectionView alloc] initWithFrame:CGRectZero collectionViewLayout:layout asyncDataFetching:NO]; + collectionView.asyncDataSource = dataSource; + collectionView.asyncDelegate = delegate; + ASCollectionViewFlowLayoutInspector *inspector = [[ASCollectionViewFlowLayoutInspector alloc] initWithCollectionView:collectionView flowLayout:layout]; + NSUInteger count = [inspector collectionView:collectionView supplementaryNodesOfKind:UICollectionElementKindSectionFooter inSection:0]; + XCTAssert(count == 1, @"should have a footer supplementary view"); + + collectionView.asyncDataSource = nil; + collectionView.asyncDelegate = nil; +} + +- (void)testThatItReturnsNoneWhenNoReferenceSizeIsImplemented +{ + InspectorTestDataSource *dataSource = [[InspectorTestDataSource alloc] init]; + HeaderReferenceSizeTestDelegate *delegate = [[HeaderReferenceSizeTestDelegate alloc] init]; + UICollectionViewFlowLayout *layout = [[UICollectionViewFlowLayout alloc] init]; + ASCollectionView *collectionView = [[ASCollectionView alloc] initWithFrame:CGRectZero collectionViewLayout:layout asyncDataFetching:NO]; + collectionView.asyncDataSource = dataSource; + collectionView.asyncDelegate = delegate; + ASCollectionViewFlowLayoutInspector *inspector = [[ASCollectionViewFlowLayoutInspector alloc] initWithCollectionView:collectionView flowLayout:layout]; + NSUInteger count = [inspector collectionView:collectionView supplementaryNodesOfKind:UICollectionElementKindSectionFooter inSection:0]; + XCTAssert(count == 0, @"should not have a footer supplementary view"); + + collectionView.asyncDataSource = nil; + collectionView.asyncDelegate = nil; +} + +@end diff --git a/AsyncDisplayKitTests/ASCollectionViewTests.m b/AsyncDisplayKitTests/ASCollectionViewTests.m index 3b4d661514..db993713b5 100644 --- a/AsyncDisplayKitTests/ASCollectionViewTests.m +++ b/AsyncDisplayKitTests/ASCollectionViewTests.m @@ -6,7 +6,9 @@ // #import -#import +#import "ASCollectionView.h" +#import "ASCollectionDataController.h" +#import "ASCollectionViewFlowLayoutInspector.h" @interface ASCollectionViewTestDelegate : NSObject @@ -73,13 +75,43 @@ @end +@interface ASCollectionView (InternalTesting) + +- (NSArray *)supplementaryNodeKindsInDataController:(ASCollectionDataController *)dataController; + +@end + @interface ASCollectionViewTests : XCTestCase @end @implementation ASCollectionViewTests -- (void)DISABLED_testCollectionViewController { +- (void)testThatItSetsALayoutInspectorForFlowLayouts +{ + UICollectionViewFlowLayout *layout = [[UICollectionViewFlowLayout alloc] init]; + ASCollectionView *collectionView = [[ASCollectionView alloc] initWithFrame:CGRectZero collectionViewLayout:layout]; + XCTAssert(collectionView.layoutDelegate != nil, @"should automatically set a layout delegate for flow layouts"); + XCTAssert([collectionView.layoutDelegate isKindOfClass:[ASCollectionViewFlowLayoutInspector class]], @"should have a flow layout inspector by default"); +} + +- (void)testThatItDoesNotSetALayoutInspectorForCustomLayouts +{ + UICollectionViewLayout *layout = [[UICollectionViewLayout alloc] init]; + ASCollectionView *collectionView = [[ASCollectionView alloc] initWithFrame:CGRectZero collectionViewLayout:layout]; + XCTAssert(collectionView.layoutDelegate == nil, @"should not set a layout delegate for custom layouts"); +} + +- (void)testThatRegisteringASupplementaryNodeStoresItForIntrospection +{ + UICollectionViewFlowLayout *layout = [[UICollectionViewFlowLayout alloc] init]; + ASCollectionView *collectionView = [[ASCollectionView alloc] initWithFrame:CGRectZero collectionViewLayout:layout]; + [collectionView registerSupplementaryNodeOfKind:UICollectionElementKindSectionHeader]; + XCTAssertEqualObjects([collectionView supplementaryNodeKindsInDataController:nil], @[UICollectionElementKindSectionHeader]); +} + +- (void)DISABLED_testCollectionViewController +{ ASCollectionViewTestController *testController = [[ASCollectionViewTestController alloc] initWithNibName:nil bundle:nil]; UIView *containerView = [[UIView alloc] initWithFrame:CGRectMake(0, 0, 100, 100)]; diff --git a/ObjectiveC.gcno b/ObjectiveC.gcno deleted file mode 100644 index 05bc771aa7..0000000000 Binary files a/ObjectiveC.gcno and /dev/null differ diff --git a/QuartzCore.gcno b/QuartzCore.gcno deleted file mode 100644 index 05bc771aa7..0000000000 Binary files a/QuartzCore.gcno and /dev/null differ diff --git a/examples/ASCollectionView/Sample.xcodeproj/project.pbxproj b/examples/ASCollectionView/Sample.xcodeproj/project.pbxproj index 7f64fc5596..959990a70e 100644 --- a/examples/ASCollectionView/Sample.xcodeproj/project.pbxproj +++ b/examples/ASCollectionView/Sample.xcodeproj/project.pbxproj @@ -7,6 +7,8 @@ objects = { /* Begin PBXBuildFile section */ + 9B92C8811BC17D3000EE46B2 /* SupplementaryNode.m in Sources */ = {isa = PBXBuildFile; fileRef = 9B92C8801BC17D3000EE46B2 /* SupplementaryNode.m */; settings = {ASSET_TAGS = (); }; }; + 9BA2CEA11BB2579C00D18414 /* Launchboard.storyboard in Resources */ = {isa = PBXBuildFile; fileRef = 9BA2CEA01BB2579C00D18414 /* Launchboard.storyboard */; settings = {ASSET_TAGS = (); }; }; AC3C4A641A11F47200143C57 /* main.m in Sources */ = {isa = PBXBuildFile; fileRef = AC3C4A631A11F47200143C57 /* main.m */; }; AC3C4A671A11F47200143C57 /* AppDelegate.m in Sources */ = {isa = PBXBuildFile; fileRef = AC3C4A661A11F47200143C57 /* AppDelegate.m */; }; AC3C4A6A1A11F47200143C57 /* ViewController.m in Sources */ = {isa = PBXBuildFile; fileRef = AC3C4A691A11F47200143C57 /* ViewController.m */; }; @@ -16,6 +18,9 @@ /* Begin PBXFileReference section */ 2DBAEE96397BB913350C4530 /* Pods.debug.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = Pods.debug.xcconfig; path = "Pods/Target Support Files/Pods/Pods.debug.xcconfig"; sourceTree = ""; }; + 9B92C87F1BC17D3000EE46B2 /* SupplementaryNode.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = SupplementaryNode.h; sourceTree = ""; }; + 9B92C8801BC17D3000EE46B2 /* SupplementaryNode.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = SupplementaryNode.m; sourceTree = ""; }; + 9BA2CEA01BB2579C00D18414 /* Launchboard.storyboard */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = file.storyboard; path = Launchboard.storyboard; sourceTree = ""; }; AC3C4A5E1A11F47200143C57 /* Sample.app */ = {isa = PBXFileReference; explicitFileType = wrapper.application; includeInIndex = 0; path = Sample.app; sourceTree = BUILT_PRODUCTS_DIR; }; AC3C4A621A11F47200143C57 /* Info.plist */ = {isa = PBXFileReference; lastKnownFileType = text.plist.xml; path = Info.plist; sourceTree = ""; }; AC3C4A631A11F47200143C57 /* main.m */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.objc; path = main.m; sourceTree = ""; }; @@ -76,6 +81,8 @@ AC3C4A691A11F47200143C57 /* ViewController.m */, AC3C4A8D1A11F80C00143C57 /* Images.xcassets */, AC3C4A611A11F47200143C57 /* Supporting Files */, + 9B92C87F1BC17D3000EE46B2 /* SupplementaryNode.h */, + 9B92C8801BC17D3000EE46B2 /* SupplementaryNode.m */, ); indentWidth = 2; path = Sample; @@ -88,6 +95,7 @@ children = ( AC3C4A621A11F47200143C57 /* Info.plist */, AC3C4A631A11F47200143C57 /* main.m */, + 9BA2CEA01BB2579C00D18414 /* Launchboard.storyboard */, ); name = "Supporting Files"; sourceTree = ""; @@ -159,6 +167,7 @@ isa = PBXResourcesBuildPhase; buildActionMask = 2147483647; files = ( + 9BA2CEA11BB2579C00D18414 /* Launchboard.storyboard in Resources */, AC3C4A8E1A11F80C00143C57 /* Images.xcassets in Resources */, ); runOnlyForDeploymentPostprocessing = 0; @@ -204,6 +213,7 @@ buildActionMask = 2147483647; files = ( AC3C4A6A1A11F47200143C57 /* ViewController.m in Sources */, + 9B92C8811BC17D3000EE46B2 /* SupplementaryNode.m in Sources */, AC3C4A671A11F47200143C57 /* AppDelegate.m in Sources */, AC3C4A641A11F47200143C57 /* main.m in Sources */, ); diff --git a/examples/ASCollectionView/Sample/Info.plist b/examples/ASCollectionView/Sample/Info.plist index a3664b0b15..eeb71a8d35 100644 --- a/examples/ASCollectionView/Sample/Info.plist +++ b/examples/ASCollectionView/Sample/Info.plist @@ -26,6 +26,8 @@ 1 LSRequiresIPhoneOS + UILaunchStoryboardName + Launchboard UIRequiredDeviceCapabilities armv7 diff --git a/examples/ASCollectionView/Sample/Launchboard.storyboard b/examples/ASCollectionView/Sample/Launchboard.storyboard new file mode 100644 index 0000000000..673e0f7e68 --- /dev/null +++ b/examples/ASCollectionView/Sample/Launchboard.storyboard @@ -0,0 +1,7 @@ + + + + + + + diff --git a/examples/ASCollectionView/Sample/SupplementaryNode.h b/examples/ASCollectionView/Sample/SupplementaryNode.h new file mode 100644 index 0000000000..f75c929684 --- /dev/null +++ b/examples/ASCollectionView/Sample/SupplementaryNode.h @@ -0,0 +1,18 @@ +/* This file provided by Facebook is for non-commercial testing and evaluation + * purposes only. Facebook reserves all rights not expressly granted. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL + * FACEBOOK BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN + * ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN + * CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. + */ + +#import + +@interface SupplementaryNode : ASCellNode + +- (instancetype)initWithText:(NSString *)text; + +@end diff --git a/examples/ASCollectionView/Sample/SupplementaryNode.m b/examples/ASCollectionView/Sample/SupplementaryNode.m new file mode 100644 index 0000000000..ca5579e9a6 --- /dev/null +++ b/examples/ASCollectionView/Sample/SupplementaryNode.m @@ -0,0 +1,55 @@ +/* This file provided by Facebook is for non-commercial testing and evaluation + * purposes only. Facebook reserves all rights not expressly granted. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL + * FACEBOOK BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN + * ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN + * CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. + */ + +#import "SupplementaryNode.h" + +#import +#import +#import + +static CGFloat kInsets = 15.0; + +@implementation SupplementaryNode { + ASTextNode *_textNode; +} + +- (instancetype)initWithText:(NSString *)text +{ + self = [super init]; + if (self != nil) { + _textNode = [[ASTextNode alloc] init]; + _textNode.attributedString = [[NSAttributedString alloc] initWithString:text + attributes:[self textAttributes]]; + [self addSubnode:_textNode]; + } + return self; +} + +- (ASLayoutSpec *)layoutSpecThatFits:(ASSizeRange)constrainedSize +{ + ASCenterLayoutSpec *center = [[ASCenterLayoutSpec alloc] init]; + center.centeringOptions = ASCenterLayoutSpecCenteringXY; + center.child = _textNode; + UIEdgeInsets insets = UIEdgeInsetsMake(kInsets, kInsets, kInsets, kInsets); + return [ASInsetLayoutSpec insetLayoutSpecWithInsets:insets child:center]; +} + +#pragma mark - Text Formatting + +- (NSDictionary *)textAttributes +{ + return @{ + NSFontAttributeName: [UIFont systemFontOfSize:18.0], + NSForegroundColorAttributeName: [UIColor whiteColor], + }; +} + +@end diff --git a/examples/ASCollectionView/Sample/ViewController.m b/examples/ASCollectionView/Sample/ViewController.m index f87e26f9d7..01350da1a3 100644 --- a/examples/ASCollectionView/Sample/ViewController.m +++ b/examples/ASCollectionView/Sample/ViewController.m @@ -12,8 +12,9 @@ #import "ViewController.h" #import +#import "SupplementaryNode.h" -@interface ViewController () +@interface ViewController () { ASCollectionView *_collectionView; } @@ -32,13 +33,17 @@ return nil; UICollectionViewFlowLayout *layout = [[UICollectionViewFlowLayout alloc] init]; - layout.scrollDirection = UICollectionViewScrollDirectionHorizontal; + layout.headerReferenceSize = CGSizeMake(50.0, 50.0); + layout.footerReferenceSize = CGSizeMake(50.0, 50.0); _collectionView = [[ASCollectionView alloc] initWithFrame:CGRectZero collectionViewLayout:layout asyncDataFetching:YES]; _collectionView.asyncDataSource = self; _collectionView.asyncDelegate = self; _collectionView.backgroundColor = [UIColor whiteColor]; + [_collectionView registerSupplementaryNodeOfKind:UICollectionElementKindSectionHeader]; + [_collectionView registerSupplementaryNodeOfKind:UICollectionElementKindSectionFooter]; + return self; } @@ -73,9 +78,26 @@ return node; } +- (ASCellNode *)collectionView:(ASCollectionView *)collectionView nodeForSupplementaryElementOfKind:(NSString *)kind atIndexPath:(NSIndexPath *)indexPath +{ + NSString *text = [kind isEqualToString:UICollectionElementKindSectionHeader] ? @"Header" : @"Footer"; + SupplementaryNode *node = [[SupplementaryNode alloc] initWithText:text]; + if ([kind isEqualToString:UICollectionElementKindSectionHeader]) { + node.backgroundColor = [UIColor blueColor]; + } else { + node.backgroundColor = [UIColor redColor]; + } + return node; +} + - (NSInteger)collectionView:(UICollectionView *)collectionView numberOfItemsInSection:(NSInteger)section { - return 300; + return 10; +} + +- (NSInteger)numberOfSectionsInCollectionView:(UICollectionView *)collectionView +{ + return 100; } - (void)collectionViewLockDataSource:(ASCollectionView *)collectionView @@ -95,7 +117,7 @@ [context completeBatchFetching:YES]; } -- (UIEdgeInsets)collectionView:(UICollectionView *)collectionView layout:(UICollectionViewLayout *)collectionViewLayout insetForSectionAtIndex:(NSInteger)section { +- (UIEdgeInsets)collectionView:(ASCollectionView *)collectionView layout:(UICollectionViewLayout *)collectionViewLayout insetForSectionAtIndex:(NSInteger)section { return UIEdgeInsetsMake(20.0, 20.0, 20.0, 20.0); }