// // ASIGListAdapterBasedDataSource.mm // Texture // // Copyright (c) Facebook, Inc. and its affiliates. All rights reserved. // Changes after 4/13/2017 are: Copyright (c) Pinterest, Inc. All rights reserved. // Licensed under Apache 2.0: http://www.apache.org/licenses/LICENSE-2.0 // #import #if AS_IG_LIST_KIT #import "ASIGListAdapterBasedDataSource.h" #import #import typedef IGListSectionController ASIGSectionController; /// The optional methods that a class implements from ASSectionController. /// Note: Bitfields are not supported by NSValue so we can't use them. typedef struct { BOOL sizeRangeForItem; BOOL shouldBatchFetch; BOOL beginBatchFetchWithContext; } ASSectionControllerOverrides; /// The optional methods that a class implements from ASSupplementaryNodeSource. /// Note: Bitfields are not supported by NSValue so we can't use them. typedef struct { BOOL sizeRangeForSupplementary; } ASSupplementarySourceOverrides; @protocol ASIGSupplementaryNodeSource @end @interface ASIGListAdapterBasedDataSource () @property (nonatomic, weak, readonly) IGListAdapter *listAdapter; @property (nonatomic, readonly) id delegate; @property (nonatomic, readonly) id dataSource; @property (nonatomic, weak, readonly) id collectionDelegate; /** * The section controller that we will forward beginBatchFetchWithContext: to. * Since shouldBatchFetch: is called on main, we capture the last section controller in there, * and then we use it and clear it in beginBatchFetchWithContext: (on default queue). * * It is safe to use it without a lock in this limited way, since those two methods will * never execute in parallel. */ @property (nonatomic, weak) ASIGSectionController *sectionControllerForBatchFetching; @end @implementation ASIGListAdapterBasedDataSource - (instancetype)initWithListAdapter:(IGListAdapter *)listAdapter collectionDelegate:(nullable id)collectionDelegate { if (self = [super init]) { #if IG_LIST_COLLECTION_VIEW [ASIGListAdapterBasedDataSource setASCollectionViewSuperclass]; #endif [ASIGListAdapterBasedDataSource configureUpdater:listAdapter.updater]; ASDisplayNodeAssert([listAdapter conformsToProtocol:@protocol(UICollectionViewDataSource)], @"Expected IGListAdapter to conform to UICollectionViewDataSource."); ASDisplayNodeAssert([listAdapter conformsToProtocol:@protocol(UICollectionViewDelegateFlowLayout)], @"Expected IGListAdapter to conform to UICollectionViewDelegateFlowLayout."); _listAdapter = listAdapter; _collectionDelegate = collectionDelegate; } return self; } - (id)dataSource { return (id)_listAdapter; } - (id)delegate { return (id)_listAdapter; } #pragma mark - ASCollectionDelegate - (void)collectionNode:(ASCollectionNode *)collectionNode didSelectItemAtIndexPath:(NSIndexPath *)indexPath { [self.delegate collectionView:collectionNode.view didSelectItemAtIndexPath:indexPath]; } - (void)scrollViewDidScroll:(UIScrollView *)scrollView { [self.delegate scrollViewDidScroll:scrollView]; } - (void)scrollViewWillBeginDragging:(UIScrollView *)scrollView { [self.delegate scrollViewWillBeginDragging:scrollView]; } - (void)scrollViewWillEndDragging:(UIScrollView *)scrollView withVelocity:(CGPoint)velocity targetContentOffset:(inout CGPoint *)targetContentOffset { // IGListAdapter doesn't implement scrollViewWillEndDragging yet (pending pull request), so we need this check for now. Doesn't hurt to have it anyways :) if ([self.delegate respondsToSelector:@selector(scrollViewWillEndDragging:withVelocity:targetContentOffset:)]) { [self.delegate scrollViewWillEndDragging:scrollView withVelocity:velocity targetContentOffset:targetContentOffset]; } } - (void)scrollViewDidEndDragging:(UIScrollView *)scrollView willDecelerate:(BOOL)decelerate { [self.delegate scrollViewDidEndDragging:scrollView willDecelerate:decelerate]; } - (void)scrollViewDidEndDecelerating:(UIScrollView *)scrollView { [self.delegate scrollViewDidEndDecelerating:scrollView]; } - (BOOL)shouldBatchFetchForCollectionNode:(ASCollectionNode *)collectionNode { if ([_collectionDelegate respondsToSelector:@selector(shouldBatchFetchForCollectionNode:)]) { return [_collectionDelegate shouldBatchFetchForCollectionNode:collectionNode]; } NSInteger sectionCount = [self numberOfSectionsInCollectionNode:collectionNode]; if (sectionCount == 0) { return NO; } // If they implement shouldBatchFetch, call it. Otherwise, just say YES if they implement beginBatchFetch. ASIGSectionController *ctrl = [self sectionControllerForSection:sectionCount - 1]; ASSectionControllerOverrides o = [ASIGListAdapterBasedDataSource overridesForSectionControllerClass:ctrl.class]; BOOL result = (o.shouldBatchFetch ? [ctrl shouldBatchFetch] : o.beginBatchFetchWithContext); if (result) { self.sectionControllerForBatchFetching = ctrl; } return result; } - (void)collectionNode:(ASCollectionNode *)collectionNode willBeginBatchFetchWithContext:(ASBatchContext *)context { if ([_collectionDelegate respondsToSelector:@selector(collectionNode:willBeginBatchFetchWithContext:)]) { [_collectionDelegate collectionNode:collectionNode willBeginBatchFetchWithContext:context]; return; } ASIGSectionController *ctrl = self.sectionControllerForBatchFetching; self.sectionControllerForBatchFetching = nil; [ctrl beginBatchFetchWithContext:context]; } /** * Note: It is not documented that ASCollectionNode will forward these UIKit delegate calls if they are implemented. * It is not considered harmful to do so, and adding them to documentation will confuse most users, who should * instead using the ASCollectionDelegate callbacks. */ #pragma mark - ASCollectionDelegateInterop - (void)collectionView:(UICollectionView *)collectionView willDisplayCell:(UICollectionViewCell *)cell forItemAtIndexPath:(NSIndexPath *)indexPath { [self.delegate collectionView:collectionView willDisplayCell:cell forItemAtIndexPath:indexPath]; } - (void)collectionView:(UICollectionView *)collectionView didEndDisplayingCell:(UICollectionViewCell *)cell forItemAtIndexPath:(NSIndexPath *)indexPath { [self.delegate collectionView:collectionView didEndDisplayingCell:cell forItemAtIndexPath:indexPath]; } #pragma mark - ASCollectionDelegateFlowLayout - (ASSizeRange)collectionNode:(ASCollectionNode *)collectionNode sizeRangeForHeaderInSection:(NSInteger)section { id src = [self supplementaryElementSourceForSection:section]; if ([ASIGListAdapterBasedDataSource overridesForSupplementarySourceClass:[src class]].sizeRangeForSupplementary) { return [src sizeRangeForSupplementaryElementOfKind:UICollectionElementKindSectionHeader atIndex:0]; } else { return ASSizeRangeZero; } } - (ASSizeRange)collectionNode:(ASCollectionNode *)collectionNode sizeRangeForFooterInSection:(NSInteger)section { id src = [self supplementaryElementSourceForSection:section]; if ([ASIGListAdapterBasedDataSource overridesForSupplementarySourceClass:[src class]].sizeRangeForSupplementary) { return [src sizeRangeForSupplementaryElementOfKind:UICollectionElementKindSectionFooter atIndex:0]; } else { return ASSizeRangeZero; } } - (UIEdgeInsets)collectionView:(UICollectionView *)collectionView layout:(UICollectionViewLayout *)collectionViewLayout insetForSectionAtIndex:(NSInteger)section { return [self.delegate collectionView:collectionView layout:collectionViewLayout insetForSectionAtIndex:section]; } - (CGFloat)collectionView:(UICollectionView *)collectionView layout:(UICollectionViewLayout *)collectionViewLayout minimumLineSpacingForSectionAtIndex:(NSInteger)section { return [self.delegate collectionView:collectionView layout:collectionViewLayout minimumLineSpacingForSectionAtIndex:section]; } - (CGFloat)collectionView:(UICollectionView *)collectionView layout:(UICollectionViewLayout *)collectionViewLayout minimumInteritemSpacingForSectionAtIndex:(NSInteger)section { return [self.delegate collectionView:collectionView layout:collectionViewLayout minimumInteritemSpacingForSectionAtIndex:section]; } #pragma mark - ASCollectionDataSource - (NSInteger)collectionNode:(ASCollectionNode *)collectionNode numberOfItemsInSection:(NSInteger)section { return [self.dataSource collectionView:collectionNode.view numberOfItemsInSection:section]; } - (NSInteger)numberOfSectionsInCollectionNode:(ASCollectionNode *)collectionNode { return [self.dataSource numberOfSectionsInCollectionView:collectionNode.view]; } - (ASCellNodeBlock)collectionNode:(ASCollectionNode *)collectionNode nodeBlockForItemAtIndexPath:(NSIndexPath *)indexPath { ASIGSectionController *ctrl = [self sectionControllerForSection:indexPath.section]; ASDisplayNodeAssert([ctrl respondsToSelector:@selector(nodeBlockForItemAtIndex:)], @"Expected section controller to respond to to %@. Controller: %@", NSStringFromSelector(@selector(nodeBlockForItemAtIndex:)), ctrl); return [ctrl nodeBlockForItemAtIndex:indexPath.item]; } - (ASCellNode *)collectionNode:(ASCollectionNode *)collectionNode nodeForItemAtIndexPath:(NSIndexPath *)indexPath { ASIGSectionController *ctrl = [self sectionControllerForSection:indexPath.section]; ASDisplayNodeAssert([ctrl respondsToSelector:@selector(nodeForItemAtIndex:)], @"Expected section controller to respond to to %@. Controller: %@", NSStringFromSelector(@selector(nodeForItemAtIndex:)), ctrl); return [ctrl nodeForItemAtIndex:indexPath.item]; } - (ASSizeRange)collectionNode:(ASCollectionNode *)collectionNode constrainedSizeForItemAtIndexPath:(NSIndexPath *)indexPath { ASIGSectionController *ctrl = [self sectionControllerForSection:indexPath.section]; if ([ASIGListAdapterBasedDataSource overridesForSectionControllerClass:ctrl.class].sizeRangeForItem) { return [ctrl sizeRangeForItemAtIndex:indexPath.item]; } else { return ASSizeRangeUnconstrained; } } - (ASCellNodeBlock)collectionNode:(ASCollectionNode *)collectionNode nodeBlockForSupplementaryElementOfKind:(NSString *)kind atIndexPath:(NSIndexPath *)indexPath { id ctrl = [self supplementaryElementSourceForSection:indexPath.section]; ASDisplayNodeAssert([ctrl respondsToSelector:@selector(nodeBlockForSupplementaryElementOfKind:atIndex:)], @"Expected section controller to respond to to %@. Controller: %@", NSStringFromSelector(@selector(nodeBlockForSupplementaryElementOfKind:atIndex:)), ctrl); return [ctrl nodeBlockForSupplementaryElementOfKind:kind atIndex:indexPath.item]; } - (ASCellNode *)collectionNode:(ASCollectionNode *)collectionNode nodeForSupplementaryElementOfKind:(NSString *)kind atIndexPath:(NSIndexPath *)indexPath { id ctrl = [self supplementaryElementSourceForSection:indexPath.section]; ASDisplayNodeAssert([ctrl respondsToSelector:@selector(nodeForSupplementaryElementOfKind:atIndex:)], @"Expected section controller to respond to to %@. Controller: %@", NSStringFromSelector(@selector(nodeForSupplementaryElementOfKind:atIndex:)), ctrl); return [ctrl nodeForSupplementaryElementOfKind:kind atIndex:indexPath.item]; } - (NSArray *)collectionNode:(ASCollectionNode *)collectionNode supplementaryElementKindsInSection:(NSInteger)section { return [[self supplementaryElementSourceForSection:section] supportedElementKinds]; } #pragma mark - ASCollectionDataSourceInterop - (__kindof UICollectionViewCell *)collectionView:(UICollectionView *)collectionView cellForItemAtIndexPath:(nonnull NSIndexPath *)indexPath { return [self.dataSource collectionView:collectionView cellForItemAtIndexPath:indexPath]; } - (UICollectionReusableView *)collectionView:(UICollectionView *)collectionView viewForSupplementaryElementOfKind:(NSString *)kind atIndexPath:(NSIndexPath *)indexPath { return [self.dataSource collectionView:collectionView viewForSupplementaryElementOfKind:kind atIndexPath:indexPath]; } + (BOOL)dequeuesCellsForNodeBackedItems { return YES; } #pragma mark - Helpers - (id)supplementaryElementSourceForSection:(NSInteger)section { ASIGSectionController *ctrl = [self sectionControllerForSection:section]; id src = (id)ctrl.supplementaryViewSource; ASDisplayNodeAssert(src == nil || [src conformsToProtocol:@protocol(ASSupplementaryNodeSource)], @"Supplementary view source should conform to %@", NSStringFromProtocol(@protocol(ASSupplementaryNodeSource))); return src; } - (ASIGSectionController *)sectionControllerForSection:(NSInteger)section { id object = [_listAdapter objectAtSection:section]; ASIGSectionController *ctrl = (ASIGSectionController *)[_listAdapter sectionControllerForObject:object]; ASDisplayNodeAssert([ctrl conformsToProtocol:@protocol(ASSectionController)], @"Expected section controller to conform to %@. Controller: %@", NSStringFromProtocol(@protocol(ASSectionController)), ctrl); return ctrl; } /// If needed, set ASCollectionView's superclass to IGListCollectionView (IGListKit < 3.0). #if IG_LIST_COLLECTION_VIEW + (void)setASCollectionViewSuperclass { #pragma clang diagnostic push #pragma clang diagnostic ignored "-Wdeprecated-declarations" static dispatch_once_t onceToken; dispatch_once(&onceToken, ^{ class_setSuperclass([ASCollectionView class], [IGListCollectionView class]); }); #pragma clang diagnostic pop } #endif /// Ensure updater won't call reloadData on us. + (void)configureUpdater:(id)updater { // Cast to NSObject will be removed after https://github.com/Instagram/IGListKit/pull/435 if ([(id)updater isKindOfClass:[IGListAdapterUpdater class]]) { [(IGListAdapterUpdater *)updater setAllowsBackgroundReloading:NO]; } else { static dispatch_once_t onceToken; dispatch_once(&onceToken, ^{ NSLog(@"WARNING: Use of non-%@ updater with AsyncDisplayKit is discouraged. Updater: %@", NSStringFromClass([IGListAdapterUpdater class]), updater); }); } } + (ASSupplementarySourceOverrides)overridesForSupplementarySourceClass:(Class)c { static NSCache *cache; static dispatch_once_t onceToken; dispatch_once(&onceToken, ^{ cache = [[NSCache alloc] init]; }); NSValue *obj = [cache objectForKey:c]; ASSupplementarySourceOverrides o; if (obj == nil) { o.sizeRangeForSupplementary = [c instancesRespondToSelector:@selector(sizeRangeForSupplementaryElementOfKind:atIndex:)]; obj = [NSValue valueWithBytes:&o objCType:@encode(ASSupplementarySourceOverrides)]; [cache setObject:obj forKey:c]; } else { [obj getValue:&o]; } return o; } + (ASSectionControllerOverrides)overridesForSectionControllerClass:(Class)c { static NSCache *cache; static dispatch_once_t onceToken; dispatch_once(&onceToken, ^{ cache = [[NSCache alloc] init]; }); NSValue *obj = [cache objectForKey:c]; ASSectionControllerOverrides o; if (obj == nil) { o.sizeRangeForItem = [c instancesRespondToSelector:@selector(sizeRangeForItemAtIndex:)]; o.beginBatchFetchWithContext = [c instancesRespondToSelector:@selector(beginBatchFetchWithContext:)]; o.shouldBatchFetch = [c instancesRespondToSelector:@selector(shouldBatchFetch)]; obj = [NSValue valueWithBytes:&o objCType:@encode(ASSectionControllerOverrides)]; [cache setObject:obj forKey:c]; } else { [obj getValue:&o]; } return o; } @end #endif // AS_IG_LIST_KIT