mirror of
https://github.com/Instagram/IGListKit
synced 2026-05-06 06:58:26 +00:00
Summary: Miss this in the previous PR #92. Will add unit test soon. Closes https://github.com/Instagram/IGListKit/pull/123 Differential Revision: D4101594 Pulled By: rnystrom fbshipit-source-id: 820030358532b0878f6d9e9092834266c9260a38
906 lines
43 KiB
Objective-C
906 lines
43 KiB
Objective-C
/**
|
|
* Copyright (c) 2016-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 "IGListAdapterInternal.h"
|
|
|
|
#import <IGListKit/IGListAssert.h>
|
|
#import <IGListKit/IGListAdapterUpdater.h>
|
|
#import <IGListKit/IGListDisplayDelegate.h>
|
|
#import <IGListKit/IGListSupplementaryViewSource.h>
|
|
|
|
#import "IGListSectionControllerInternal.h"
|
|
#import "NSIndexSet+PrettyDescription.h"
|
|
|
|
@implementation IGListAdapter {
|
|
NSMapTable<UICollectionViewCell *, IGListSectionController<IGListSectionType> *> *_cellSectionControllerMap;
|
|
BOOL _isDequeuingCell;
|
|
}
|
|
|
|
- (void)dealloc {
|
|
// on iOS 9 setting the dataSource has side effects that can invalidate the layout and seg fault
|
|
if ([[[UIDevice currentDevice] systemVersion] floatValue] < 9.0) {
|
|
// properties are assign for <iOS 9
|
|
_collectionView.dataSource = nil;
|
|
_collectionView.delegate = nil;
|
|
}
|
|
}
|
|
|
|
|
|
#pragma mark - Init
|
|
|
|
- (instancetype)initWithUpdater:(id <IGListUpdatingDelegate>)updatingDelegate
|
|
viewController:(UIViewController *)viewController
|
|
workingRangeSize:(NSUInteger)workingRangeSize {
|
|
IGAssertMainThread();
|
|
IGParameterAssert(updatingDelegate);
|
|
|
|
if (self = [super init]) {
|
|
NSPointerFunctions *keyFunctions = [updatingDelegate objectLookupPointerFunctions];
|
|
NSPointerFunctions *valueFunctions = [NSPointerFunctions pointerFunctionsWithOptions:NSPointerFunctionsStrongMemory];
|
|
NSMapTable *table = [[NSMapTable alloc] initWithKeyPointerFunctions:keyFunctions valuePointerFunctions:valueFunctions capacity:0];
|
|
_sectionMap = [[IGListSectionMap alloc] initWithMapTable:table];
|
|
|
|
_displayHandler = [[IGListDisplayHandler alloc] init];
|
|
_workingRangeHandler = [[IGListWorkingRangeHandler alloc] initWithWorkingRangeSize:workingRangeSize];
|
|
|
|
_cellSectionControllerMap = [NSMapTable mapTableWithKeyOptions:NSMapTableObjectPointerPersonality | NSMapTableStrongMemory
|
|
valueOptions:NSMapTableStrongMemory];
|
|
|
|
_updatingDelegate = updatingDelegate;
|
|
_viewController = viewController;
|
|
}
|
|
return self;
|
|
}
|
|
|
|
- (IGListCollectionView *)collectionView {
|
|
return (IGListCollectionView *)_collectionView;
|
|
}
|
|
|
|
- (void)setCollectionView:(IGListCollectionView *)collectionView {
|
|
IGAssertMainThread();
|
|
IGParameterAssert([collectionView isKindOfClass:[IGListCollectionView class]]);
|
|
|
|
// if collection view has been used by a different list adapter, treat it as if we were using a new collection view
|
|
// this happens when embedding a IGListCollectionView inside a UICollectionViewCell that is reused
|
|
if (_collectionView != collectionView || _collectionView.dataSource != self) {
|
|
// dump old registered section controllers in the case that we are changing collection views or setting for
|
|
// the first time
|
|
_registeredCellClasses = [NSMutableSet new];
|
|
_registeredNibNames = [NSMutableSet new];
|
|
_registeredSupplementaryViewIdentifiers = [NSMutableSet new];
|
|
|
|
_collectionView = collectionView;
|
|
_collectionView.dataSource = self;
|
|
|
|
[self updateCollectionViewDelegate];
|
|
[self updateAfterPublicSettingsChange];
|
|
}
|
|
}
|
|
|
|
- (void)setDataSource:(id<IGListAdapterDataSource>)dataSource {
|
|
if (_dataSource != dataSource) {
|
|
_dataSource = dataSource;
|
|
[self updateAfterPublicSettingsChange];
|
|
}
|
|
}
|
|
|
|
// reset and configure the delegate proxy whenever this property is set
|
|
- (void)setCollectionViewDelegate:(id<UICollectionViewDelegate>)collectionViewDelegate {
|
|
IGAssertMainThread();
|
|
IGAssert(![collectionViewDelegate conformsToProtocol:@protocol(UICollectionViewDelegateFlowLayout)],
|
|
@"UICollectionViewDelegateFlowLayout conformance is automatically handled by IGListAdapter.");
|
|
|
|
if (_collectionViewDelegate != collectionViewDelegate) {
|
|
_collectionViewDelegate = collectionViewDelegate;
|
|
[self createProxyAndUpdateCollectionViewDelegate];
|
|
}
|
|
}
|
|
|
|
- (void)setScrollViewDelegate:(id<UIScrollViewDelegate>)scrollViewDelegate {
|
|
IGAssertMainThread();
|
|
|
|
if (_scrollViewDelegate != scrollViewDelegate) {
|
|
_scrollViewDelegate = scrollViewDelegate;
|
|
[self createProxyAndUpdateCollectionViewDelegate];
|
|
}
|
|
}
|
|
|
|
- (void)updateAfterPublicSettingsChange {
|
|
if (_collectionView != nil && _dataSource != nil) {
|
|
[self updateObjects:[[_dataSource objectsForListAdapter:self] copy]];
|
|
|
|
if (IGListExperimentEnabled(self.experiments, IGListExperimentUICVReloadedInSetter)) {
|
|
[_collectionView reloadData];
|
|
}
|
|
}
|
|
}
|
|
|
|
- (void)createProxyAndUpdateCollectionViewDelegate {
|
|
// there is a known bug with accessibility and using an NSProxy as the delegate that will cause EXC_BAD_ACCESS
|
|
// when voiceover is enabled. it will hold an unsafe ref to the delegate
|
|
_collectionView.delegate = nil;
|
|
|
|
self.delegateProxy = [[IGListAdapterProxy alloc] initWithCollectionViewTarget:_collectionViewDelegate
|
|
scrollViewTarget:_scrollViewDelegate
|
|
interceptor:self];
|
|
[self updateCollectionViewDelegate];
|
|
}
|
|
|
|
- (void)updateCollectionViewDelegate {
|
|
// set up the delegate to the proxy so the adapter can intercept events
|
|
// default to the adapter simply being the delegate
|
|
_collectionView.delegate = (id<UICollectionViewDelegate>)self.delegateProxy ?: self;
|
|
}
|
|
|
|
|
|
#pragma mark - Scrolling
|
|
|
|
- (void)scrollToObject:(id)object
|
|
supplementaryKinds:(NSArray<NSString *> *)supplementaryKinds
|
|
scrollDirection:(UICollectionViewScrollDirection)scrollDirection
|
|
animated:(BOOL)animated {
|
|
IGAssertMainThread();
|
|
IGParameterAssert(object != nil);
|
|
|
|
const NSUInteger section = [self sectionForObject:object];
|
|
if (section == NSNotFound) {
|
|
return;
|
|
}
|
|
|
|
UICollectionView *collectionView = self.collectionView;
|
|
const NSUInteger numberOfItems = [collectionView numberOfItemsInSection:section];
|
|
if (numberOfItems == 0) {
|
|
return;
|
|
}
|
|
|
|
// force layout before continuing
|
|
// this method is typcially called before pushing a view controller
|
|
// thus, before the layout process has actually happened
|
|
[collectionView layoutIfNeeded];
|
|
|
|
// collect the layout attributes for the cell and supplementary views for the first index
|
|
// this will break if there are supplementary views beyond item 0
|
|
NSIndexPath *indexPath = [NSIndexPath indexPathForItem:0 inSection:section];
|
|
NSArray *attributes = [self layoutAttributesForIndexPath:indexPath supplementaryKinds:supplementaryKinds];
|
|
|
|
CGFloat offset = 0.0;
|
|
for (UICollectionViewLayoutAttributes *attribute in attributes) {
|
|
const CGRect frame = attribute.frame;
|
|
CGFloat origin;
|
|
switch (scrollDirection) {
|
|
case UICollectionViewScrollDirectionHorizontal:
|
|
origin = CGRectGetMinX(frame);
|
|
break;
|
|
case UICollectionViewScrollDirectionVertical:
|
|
origin = CGRectGetMinY(frame);
|
|
break;
|
|
}
|
|
|
|
// find the minimum origin value of all the layout attributes
|
|
if (attribute == attributes.firstObject || origin < offset) {
|
|
offset = origin;
|
|
}
|
|
}
|
|
|
|
const UIEdgeInsets contentInset = collectionView.contentInset;
|
|
CGPoint contentOffset = collectionView.contentOffset;
|
|
switch (scrollDirection) {
|
|
case UICollectionViewScrollDirectionHorizontal:
|
|
contentOffset.x = offset - contentInset.left;
|
|
break;
|
|
case UICollectionViewScrollDirectionVertical:
|
|
contentOffset.y = offset - contentInset.top;
|
|
break;
|
|
}
|
|
|
|
[collectionView setContentOffset:contentOffset animated:animated];
|
|
}
|
|
|
|
|
|
#pragma mark - Editing
|
|
|
|
- (void)performUpdatesAnimated:(BOOL)animated completion:(IGListUpdaterCompletion)completion {
|
|
IGAssertMainThread();
|
|
|
|
id<IGListAdapterDataSource> dataSource = self.dataSource;
|
|
UICollectionView *collectionView = self.collectionView;
|
|
if (dataSource == nil || collectionView == nil) {
|
|
if (completion) {
|
|
completion(NO);
|
|
}
|
|
return;
|
|
}
|
|
|
|
NSArray *fromObjects = [self.sectionMap.objects copy];
|
|
NSArray *newItems = [[dataSource objectsForListAdapter:self] copy];
|
|
|
|
__weak __typeof__(self) weakSelf = self;
|
|
[self.updatingDelegate performUpdateWithCollectionView:collectionView
|
|
fromObjects:fromObjects
|
|
toObjects:newItems
|
|
animated:animated
|
|
objectTransitionBlock:^(NSArray *toObjects) {
|
|
// temporarily capture the item map that we are transitioning from in case
|
|
// there are any item deletes at the same
|
|
weakSelf.previoussectionMap = [weakSelf.sectionMap copy];
|
|
|
|
[weakSelf updateObjects:toObjects];
|
|
} completion:^(BOOL finished) {
|
|
// release the previous items
|
|
weakSelf.previoussectionMap = nil;
|
|
|
|
if (completion) {
|
|
completion(finished);
|
|
}
|
|
}];
|
|
}
|
|
|
|
- (void)reloadDataWithCompletion:(nullable IGListUpdaterCompletion)completion {
|
|
IGAssertMainThread();
|
|
|
|
id<IGListAdapterDataSource> dataSource = self.dataSource;
|
|
UICollectionView *collectionView = self.collectionView;
|
|
if (dataSource == nil || collectionView == nil) {
|
|
if (completion) {
|
|
completion(NO);
|
|
}
|
|
return;
|
|
}
|
|
|
|
NSArray *newItems = [[dataSource objectsForListAdapter:self] copy];
|
|
|
|
__weak __typeof__(self) weakSelf = self;
|
|
[self.updatingDelegate reloadDataWithCollectionView:collectionView reloadUpdateBlock:^{
|
|
// purge all section controllers from the item map so that they are regenerated
|
|
[weakSelf.sectionMap reset];
|
|
[weakSelf updateObjects:newItems];
|
|
} completion:completion];
|
|
}
|
|
|
|
- (void)reloadObjects:(NSArray *)objects {
|
|
IGAssertMainThread();
|
|
IGParameterAssert(objects);
|
|
|
|
NSMutableIndexSet *sections = [[NSMutableIndexSet alloc] init];
|
|
|
|
// use the item map based on whether or not we're in an update block
|
|
IGListSectionMap *map = [self sectionMapAdjustForUpdateBlock:YES];
|
|
|
|
for (id object in objects) {
|
|
// look up the item using the map's lookup function. might not be the same item
|
|
NSUInteger section = [map sectionForObject:object];
|
|
IGAssert(section != NSNotFound, @"Did not find a section for item %@", object);
|
|
[sections addIndex:section];
|
|
|
|
// reverse lookup the item using the section. if the pointer has changed the trigger update events and swap items
|
|
if (object != [map objectForSection:section]) {
|
|
[map updateObject:object];
|
|
[[map sectionControllerForSection:section] didUpdateToObject:object];
|
|
}
|
|
}
|
|
|
|
UICollectionView *collectionView = self.collectionView;
|
|
IGAssert(collectionView != nil, @"Tried to reload the adapter without a collection view");
|
|
|
|
[self.updatingDelegate reloadCollectionView:collectionView sections:sections];
|
|
}
|
|
|
|
|
|
#pragma mark - List Items & Sections
|
|
|
|
- (NSUInteger)sectionForSectionController:(IGListSectionController <IGListSectionType> *)sectionController {
|
|
IGAssertMainThread();
|
|
IGParameterAssert(sectionController != nil);
|
|
|
|
return [self.sectionMap sectionForSectionController:sectionController];
|
|
}
|
|
|
|
- (id <IGListSectionType>)sectionControllerForObject:(id)object {
|
|
IGAssertMainThread();
|
|
IGParameterAssert(object != nil);
|
|
|
|
return [self.sectionMap sectionControllerForObject:object];
|
|
}
|
|
|
|
- (id)objectAtSection:(NSUInteger)section {
|
|
IGAssertMainThread();
|
|
|
|
return [self.sectionMap objectForSection:section];
|
|
}
|
|
|
|
- (NSUInteger)sectionForObject:(id)item {
|
|
IGAssertMainThread();
|
|
IGParameterAssert(item != nil);
|
|
|
|
return [self.sectionMap sectionForObject:item];
|
|
}
|
|
|
|
- (NSArray *)objects {
|
|
IGAssertMainThread();
|
|
|
|
return [self.sectionMap.objects copy];
|
|
}
|
|
|
|
- (id<IGListSupplementaryViewSource>)supplementaryViewSourceAtIndexPath:(NSIndexPath *)indexPath {
|
|
IGListSectionController<IGListSectionType> *sectionController = [self.sectionMap sectionControllerForSection:indexPath.section];
|
|
return [sectionController supplementaryViewSource];
|
|
}
|
|
|
|
- (NSArray<IGListSectionController<IGListSectionType> *> *)visibleSectionControllers {
|
|
IGAssertMainThread();
|
|
NSArray<UICollectionViewCell *> *visibleCells = [self.collectionView visibleCells];
|
|
NSMutableSet *visibleSectionControllers = [NSMutableSet new];
|
|
for (UICollectionViewCell *cell in visibleCells) {
|
|
IGListSectionController *sectionController = [self sectionControllerForCell:cell];
|
|
IGAssert(sectionController != nil, @"Section controller nil for cell %@", cell);
|
|
if (sectionController) {
|
|
[visibleSectionControllers addObject:sectionController];
|
|
}
|
|
}
|
|
return [visibleSectionControllers allObjects];
|
|
}
|
|
|
|
|
|
#pragma mark - Layout
|
|
|
|
- (CGSize)sizeForItemAtIndexPath:(NSIndexPath *)indexPath {
|
|
IGAssertMainThread();
|
|
|
|
IGListSectionController <IGListSectionType> *sectionController = [self.sectionMap sectionControllerForSection:indexPath.section];
|
|
return [sectionController sizeForItemAtIndex:indexPath.item];
|
|
}
|
|
|
|
- (CGSize)sizeForSupplementaryViewOfKind:(NSString *)elementKind atIndexPath:(NSIndexPath *)indexPath {
|
|
IGAssertMainThread();
|
|
id <IGListSupplementaryViewSource> supplementaryViewSource = [self supplementaryViewSourceAtIndexPath:indexPath];
|
|
if ([[supplementaryViewSource supportedElementKinds] containsObject:elementKind]) {
|
|
return [supplementaryViewSource sizeForSupplementaryViewOfKind:elementKind atIndex:indexPath.item];
|
|
}
|
|
return CGSizeZero;
|
|
}
|
|
|
|
|
|
#pragma mark - Private API
|
|
|
|
// this method is what updates the "source of truth"
|
|
// this should only be called just before the collection view is updated
|
|
- (void)updateObjects:(NSArray *)objects {
|
|
#if DEBUG
|
|
for (id object in objects) {
|
|
IGAssert([object isEqual:object], @"Object instance %@ not equal to itself. This will break infra map tables.", object);
|
|
}
|
|
#endif
|
|
|
|
NSMutableArray<IGListSectionController <IGListSectionType> *> *sectionControllers = [[NSMutableArray alloc] init];
|
|
IGListSectionMap *map = self.sectionMap;
|
|
|
|
// collect items that have changed since the last update
|
|
NSMutableSet *updatedObjects = [NSMutableSet new];
|
|
|
|
// push the view controller and collection context into a local thread container so they are available on init
|
|
// for IGListSectionController subclasses after calling [super init]
|
|
IGListSectionControllerPushThread(self.viewController, self);
|
|
|
|
id firstObject = objects.firstObject;
|
|
id lastObject = objects.lastObject;
|
|
|
|
for (id object in objects) {
|
|
// infra checks to see if a controller exists
|
|
IGListSectionController <IGListSectionType> *sectionController = [map sectionControllerForObject:object];
|
|
|
|
// if not, query the data source for a new one
|
|
if (sectionController == nil) {
|
|
sectionController = [self.dataSource listAdapter:self sectionControllerForObject:object];
|
|
}
|
|
|
|
IGAssert(sectionController != nil, @"Data source <%@> cannot return a nil section controller.", self.dataSource);
|
|
if (sectionController == nil) {
|
|
break;
|
|
}
|
|
|
|
// in case the section controller was created outside of -listAdapter:sectionControllerForObject:
|
|
sectionController.collectionContext = self;
|
|
sectionController.viewController = self.viewController;
|
|
sectionController.isFirstSection = (object == firstObject);
|
|
sectionController.isLastSection = (object == lastObject);
|
|
|
|
// check if the item has changed instances or is new
|
|
const NSUInteger oldSection = [map sectionForObject:object];
|
|
if (oldSection == NSNotFound || [map objectForSection:oldSection] != object) {
|
|
[updatedObjects addObject:object];
|
|
}
|
|
|
|
[sectionControllers addObject:sectionController];
|
|
}
|
|
|
|
// clear the view controller and collection context
|
|
IGListSectionControllerPopThread();
|
|
|
|
[map updateWithObjects:objects sectionControllers:[sectionControllers copy]];
|
|
|
|
// now that the maps have been created and contexts are assigned, we consider the section controller "fully loaded"
|
|
for (id object in updatedObjects) {
|
|
[[map sectionControllerForObject:object] didUpdateToObject:object];
|
|
}
|
|
|
|
NSUInteger itemCount = 0;
|
|
for (IGListSectionController<IGListSectionType> *sectionController in sectionControllers) {
|
|
itemCount += [sectionController numberOfItems];
|
|
}
|
|
|
|
[self updateBackgroundViewWithItemCount:itemCount];
|
|
}
|
|
|
|
- (void)updateBackgroundViewWithItemCount:(NSUInteger)itemCount {
|
|
UIView *backgroundView = [self.dataSource emptyViewForListAdapter:self];
|
|
// don't do anything if the client is using the same view
|
|
if (backgroundView != _collectionView.backgroundView) {
|
|
// collection view will just stack the background views underneath each other if we do not remove the previous
|
|
// one first. also fine if it is nil
|
|
[_collectionView.backgroundView removeFromSuperview];
|
|
_collectionView.backgroundView = backgroundView;
|
|
}
|
|
_collectionView.backgroundView.hidden = itemCount > 0;
|
|
}
|
|
|
|
- (IGListSectionMap *)sectionMapAdjustForUpdateBlock:(BOOL)adjustForUpdateBlock {
|
|
// if we are inside an update block, we may have to use the /previous/ item map for some operations
|
|
if (adjustForUpdateBlock && self.isInUpdateBlock && self.previoussectionMap != nil) {
|
|
return self.previoussectionMap;
|
|
} else {
|
|
return self.sectionMap;
|
|
}
|
|
}
|
|
|
|
- (NSArray<NSIndexPath *> *)indexPathsFromSectionController:(IGListSectionController <IGListSectionType> *)sectionController
|
|
indexes:(NSIndexSet *)indexes
|
|
adjustForUpdateBlock:(BOOL)adjustForUpdateBlock {
|
|
NSMutableArray<NSIndexPath *> *indexPaths = [[NSMutableArray alloc] init];
|
|
|
|
IGListSectionMap *map = [self sectionMapAdjustForUpdateBlock:adjustForUpdateBlock];
|
|
|
|
const NSUInteger section = [map sectionForSectionController:sectionController];
|
|
if (section != NSNotFound) {
|
|
[indexes enumerateIndexesUsingBlock:^(NSUInteger idx, BOOL *stop) {
|
|
[indexPaths addObject:[NSIndexPath indexPathForItem:idx inSection:section]];
|
|
}];
|
|
}
|
|
return [indexPaths copy];
|
|
}
|
|
|
|
- (NSIndexPath *)indexPathForSectionController:(IGListSectionController *)controller index:(NSInteger)index {
|
|
const NSUInteger section = [self.sectionMap sectionForSectionController:controller];
|
|
if (section == NSNotFound) {
|
|
return nil;
|
|
} else {
|
|
return [NSIndexPath indexPathForItem:index inSection:section];
|
|
}
|
|
}
|
|
|
|
- (NSArray<UICollectionViewLayoutAttributes *> *)layoutAttributesForIndexPath:(NSIndexPath *)indexPath
|
|
supplementaryKinds:(NSArray<NSString *> *)supplementaryKinds {
|
|
UICollectionViewLayout *layout = self.collectionView.collectionViewLayout;
|
|
NSMutableArray<UICollectionViewLayoutAttributes *> *attributes = [[NSMutableArray alloc] init];
|
|
|
|
UICollectionViewLayoutAttributes *cellAttributes = [layout layoutAttributesForItemAtIndexPath:indexPath];
|
|
if (cellAttributes) {
|
|
[attributes addObject:cellAttributes];
|
|
}
|
|
|
|
for (NSString *kind in supplementaryKinds) {
|
|
UICollectionViewLayoutAttributes *supplementaryAttributes = [layout layoutAttributesForSupplementaryViewOfKind:kind atIndexPath:indexPath];
|
|
if (supplementaryAttributes) {
|
|
[attributes addObject:supplementaryAttributes];
|
|
}
|
|
}
|
|
|
|
return [attributes copy];
|
|
}
|
|
|
|
- (void)mapCell:(UICollectionViewCell *)cell toSectionController:(IGListSectionController<IGListSectionType> *)sectionController {
|
|
IGAssertMainThread();
|
|
IGParameterAssert(cell != nil);
|
|
IGParameterAssert(sectionController != nil);
|
|
[_cellSectionControllerMap setObject:sectionController forKey:cell];
|
|
}
|
|
|
|
- (nullable IGListSectionController<IGListSectionType> *)sectionControllerForCell:(UICollectionViewCell *)cell {
|
|
IGAssertMainThread();
|
|
return [_cellSectionControllerMap objectForKey:cell];
|
|
}
|
|
|
|
- (void)removeMapForCell:(UICollectionViewCell *)cell {
|
|
IGAssertMainThread();
|
|
[_cellSectionControllerMap removeObjectForKey:cell];
|
|
}
|
|
|
|
#pragma mark - UICollectionViewDataSource
|
|
|
|
- (NSInteger)numberOfSectionsInCollectionView:(UICollectionView *)collectionView {
|
|
return self.sectionMap.objects.count;
|
|
}
|
|
|
|
- (NSInteger)collectionView:(UICollectionView *)collectionView numberOfItemsInSection:(NSInteger)section {
|
|
IGListSectionController <IGListSectionType> * sectionController = [self.sectionMap sectionControllerForSection:section];
|
|
IGAssert(sectionController != nil, @"Nil section controller for section %zi for item %@. Check your -diffIdentifier and -isEqual: implementations.",
|
|
section, [self.sectionMap objectForSection:section]);
|
|
const NSInteger numberOfItems = [sectionController numberOfItems];
|
|
IGAssert(numberOfItems >= 0, @"Cannot return negative number of items %zi for section controller %@.", numberOfItems, sectionController);
|
|
return numberOfItems;
|
|
}
|
|
|
|
- (UICollectionViewCell *)collectionView:(UICollectionView *)collectionView cellForItemAtIndexPath:(NSIndexPath *)indexPath {
|
|
IGListSectionController<IGListSectionType> *sectionController = [self.sectionMap sectionControllerForSection:indexPath.section];
|
|
|
|
// flag that a cell is being dequeued in case it tries to access a cell in the process
|
|
_isDequeuingCell = YES;
|
|
UICollectionViewCell *cell = [sectionController cellForItemAtIndex:indexPath.item];
|
|
_isDequeuingCell = NO;
|
|
|
|
IGAssert(cell != nil, @"Returned a nil cell at indexPath <%@> from section controller: <%@>", indexPath, sectionController);
|
|
|
|
// associate the section controller with the cell so that we know which section controller is using it
|
|
[self mapCell:cell toSectionController:sectionController];
|
|
|
|
return cell;
|
|
}
|
|
|
|
- (UICollectionReusableView *)collectionView:(UICollectionView *)collectionView viewForSupplementaryElementOfKind:(NSString *)kind atIndexPath:(NSIndexPath *)indexPath {
|
|
IGListSectionController<IGListSectionType> *sectionController = [self.sectionMap sectionControllerForSection:indexPath.section];
|
|
id <IGListSupplementaryViewSource> supplementarySource = [sectionController supplementaryViewSource];
|
|
UICollectionReusableView *view = [supplementarySource viewForSupplementaryElementOfKind:kind atIndex:indexPath.item];
|
|
IGAssert(view != nil, @"Returned a nil supplementary view at indexPath <%@> from section controller: <%@>, supplementary source: <%@>", indexPath, sectionController, supplementarySource);
|
|
return view;
|
|
}
|
|
|
|
|
|
#pragma mark - UICollectionViewDelegate
|
|
|
|
- (void)collectionView:(UICollectionView *)collectionView didSelectItemAtIndexPath:(NSIndexPath *)indexPath {
|
|
// forward this method to the delegate b/c this implementation will steal the message from the proxy
|
|
id<UICollectionViewDelegate> collectionViewDelegate = self.collectionViewDelegate;
|
|
if ([collectionViewDelegate respondsToSelector:@selector(collectionView:didSelectItemAtIndexPath:)]) {
|
|
[collectionViewDelegate collectionView:collectionView didSelectItemAtIndexPath:indexPath];
|
|
}
|
|
|
|
IGListSectionController <IGListSectionType> * sectionController = [self.sectionMap sectionControllerForSection:indexPath.section];
|
|
[sectionController didSelectItemAtIndex:indexPath.item];
|
|
}
|
|
|
|
- (void)collectionView:(UICollectionView *)collectionView willDisplayCell:(UICollectionViewCell *)cell forItemAtIndexPath:(NSIndexPath *)indexPath {
|
|
// forward this method to the delegate b/c this implementation will steal the message from the proxy
|
|
id<UICollectionViewDelegate> collectionViewDelegate = self.collectionViewDelegate;
|
|
if ([collectionViewDelegate respondsToSelector:@selector(collectionView:willDisplayCell:forItemAtIndexPath:)]) {
|
|
[collectionViewDelegate collectionView:collectionView willDisplayCell:cell forItemAtIndexPath:indexPath];
|
|
}
|
|
|
|
IGListSectionController <IGListSectionType> *sectionController = [self sectionControllerForCell:cell];
|
|
// if the section controller relationship was destroyed, reconnect it
|
|
// this happens with iOS 10 UICollectionView display range changes
|
|
if (sectionController == nil) {
|
|
sectionController = [self.sectionMap sectionControllerForSection:indexPath.section];
|
|
[self mapCell:cell toSectionController:sectionController];
|
|
}
|
|
|
|
id object = [self.sectionMap objectForSection:indexPath.section];
|
|
[self.displayHandler willDisplayCell:cell forListAdapter:self sectionController:sectionController object:object indexPath:indexPath];
|
|
[self.workingRangeHandler willDisplayItemAtIndexPath:indexPath forListAdapter:self];
|
|
}
|
|
|
|
- (void)collectionView:(UICollectionView *)collectionView didEndDisplayingCell:(UICollectionViewCell *)cell forItemAtIndexPath:(NSIndexPath *)indexPath {
|
|
// forward this method to the delegate b/c this implementation will steal the message from the proxy
|
|
id<UICollectionViewDelegate> collectionViewDelegate = self.collectionViewDelegate;
|
|
if ([collectionViewDelegate respondsToSelector:@selector(collectionView:didEndDisplayingCell:forItemAtIndexPath:)]) {
|
|
[collectionViewDelegate collectionView:collectionView didEndDisplayingCell:cell forItemAtIndexPath:indexPath];
|
|
}
|
|
|
|
IGListSectionController <IGListSectionType> *sectionController = [self sectionControllerForCell:cell];
|
|
[self.displayHandler didEndDisplayingCell:cell forListAdapter:self sectionController:sectionController indexPath:indexPath];
|
|
[self.workingRangeHandler didEndDisplayingItemAtIndexPath:indexPath forListAdapter:self];
|
|
|
|
// break the association between the cell and the section controller
|
|
[self removeMapForCell:cell];
|
|
}
|
|
|
|
#pragma mark - UIScrollViewDelegate
|
|
|
|
- (void)scrollViewDidScroll:(UIScrollView *)scrollView {
|
|
// forward this method to the delegate b/c this implementation will steal the message from the proxy
|
|
id<UIScrollViewDelegate> scrollViewDelegate = self.scrollViewDelegate;
|
|
if ([scrollViewDelegate respondsToSelector:@selector(scrollViewDidScroll:)]) {
|
|
[scrollViewDelegate scrollViewDidScroll:scrollView];
|
|
}
|
|
NSArray<IGListSectionController<IGListSectionType> *> *visibleSectionControllers = [self visibleSectionControllers];
|
|
for (IGListSectionController<IGListSectionType> *sectionController in visibleSectionControllers) {
|
|
[[sectionController scrollDelegate] listAdapter:self didScrollSectionController:sectionController];
|
|
}
|
|
}
|
|
|
|
- (void)scrollViewWillBeginDragging:(UIScrollView *)scrollView {
|
|
// forward this method to the delegate b/c this implementation will steal the message from the proxy
|
|
id<UIScrollViewDelegate> scrollViewDelegate = self.scrollViewDelegate;
|
|
if ([scrollViewDelegate respondsToSelector:@selector(scrollViewWillBeginDragging:)]) {
|
|
[scrollViewDelegate scrollViewWillBeginDragging:scrollView];
|
|
}
|
|
NSArray<IGListSectionController<IGListSectionType> *> *visibleSectionControllers = [self visibleSectionControllers];
|
|
for (IGListSectionController<IGListSectionType> *sectionController in visibleSectionControllers) {
|
|
[[sectionController scrollDelegate] listAdapter:self willBeginDraggingSectionController:sectionController];
|
|
}
|
|
}
|
|
|
|
- (void)scrollViewDidEndDragging:(UIScrollView *)scrollView willDecelerate:(BOOL)decelerate {
|
|
// forward this method to the delegate b/c this implementation will steal the message from the proxy
|
|
id<UIScrollViewDelegate> scrollViewDelegate = self.scrollViewDelegate;
|
|
if ([scrollViewDelegate respondsToSelector:@selector(scrollViewDidEndDragging:willDecelerate:)]) {
|
|
[scrollViewDelegate scrollViewDidEndDragging:scrollView willDecelerate:decelerate];
|
|
}
|
|
NSArray<IGListSectionController<IGListSectionType> *> *visibleSectionControllers = [self visibleSectionControllers];
|
|
for (IGListSectionController<IGListSectionType> *sectionController in visibleSectionControllers) {
|
|
[[sectionController scrollDelegate] listAdapter:self didEndDraggingSectionController:sectionController willDecelerate:decelerate];
|
|
}
|
|
}
|
|
|
|
|
|
#pragma mark - IGListCollectionContext
|
|
|
|
- (CGSize)containerSize {
|
|
return UIEdgeInsetsInsetRect(self.collectionView.bounds, self.collectionView.contentInset).size;
|
|
}
|
|
|
|
- (NSUInteger)indexForCell:(UICollectionViewCell *)cell sectionController:(nonnull IGListSectionController<IGListSectionType> *)sectionController {
|
|
IGAssertMainThread();
|
|
IGParameterAssert(cell != nil);
|
|
IGParameterAssert(sectionController != nil);
|
|
NSIndexPath *indexPath = [self.collectionView indexPathForCell:cell];
|
|
IGAssert(indexPath.section == [self sectionForSectionController:sectionController],
|
|
@"Requesting a cell from another section controller is not allowed.");
|
|
return indexPath != nil ? indexPath.item : NSNotFound;
|
|
}
|
|
|
|
- (__kindof UICollectionViewCell *)cellForItemAtIndex:(NSInteger)index
|
|
sectionController:(IGListSectionController <IGListSectionType> *)sectionController {
|
|
IGAssertMainThread();
|
|
IGParameterAssert(sectionController != nil);
|
|
|
|
// if this is accessed while a cell is being dequeued, just return nil
|
|
if (_isDequeuingCell) {
|
|
return nil;
|
|
}
|
|
|
|
NSIndexPath *indexPath = [self indexPathForSectionController:sectionController index:index];
|
|
// prevent querying the collection view if it isn't fully reloaded yet for the current data set
|
|
if (indexPath != nil
|
|
&& indexPath.section < [self.collectionView numberOfSections]) {
|
|
// only return a cell if it belongs to the section controller
|
|
// this association is created in -collectionView:cellForItemAtIndexPath:
|
|
UICollectionViewCell *cell = [self.collectionView cellForItemAtIndexPath:indexPath];
|
|
if ([self sectionControllerForCell:cell] == sectionController) {
|
|
return cell;
|
|
}
|
|
}
|
|
return nil;
|
|
}
|
|
|
|
- (NSArray<UICollectionViewCell *> *)visibleCellsForSectionController:(IGListSectionController<IGListSectionType> *)sectionController {
|
|
NSMutableArray *cells = [NSMutableArray new];
|
|
UICollectionView *collectionView = self.collectionView;
|
|
NSArray *visibleCells = [collectionView visibleCells];
|
|
const NSUInteger section = [self sectionForSectionController:sectionController];
|
|
for (UICollectionViewCell *cell in visibleCells) {
|
|
if ([collectionView indexPathForCell:cell].section == section) {
|
|
[cells addObject:cell];
|
|
}
|
|
}
|
|
return [cells copy];
|
|
}
|
|
|
|
- (void)deselectItemAtIndex:(NSInteger)index
|
|
sectionController:(IGListSectionController<IGListSectionType> *)sectionController
|
|
animated:(BOOL)animated {
|
|
IGAssertMainThread();
|
|
IGParameterAssert(sectionController != nil);
|
|
NSIndexPath *indexPath = [self indexPathForSectionController:sectionController index:index];
|
|
[self.collectionView deselectItemAtIndexPath:indexPath animated:animated];
|
|
}
|
|
|
|
- (__kindof UICollectionViewCell *)dequeueReusableCellOfClass:(Class)cellClass
|
|
forSectionController:(IGListSectionController <IGListSectionType> *)sectionController
|
|
atIndex:(NSInteger)index {
|
|
IGAssertMainThread();
|
|
IGParameterAssert(sectionController != nil);
|
|
IGParameterAssert(cellClass != nil);
|
|
IGParameterAssert(index >= 0);
|
|
UICollectionView *collectionView = self.collectionView;
|
|
IGAssert(collectionView != nil, @"Dequeueing cell of class %@ from section controller %@ without a collection view at index %zi", NSStringFromClass(cellClass), sectionController, index);
|
|
NSString *identifier = IGListReusableViewIdentifier(cellClass, nil, nil);
|
|
NSIndexPath *indexPath = [self indexPathForSectionController:sectionController index:index];
|
|
if (![self.registeredCellClasses containsObject:cellClass]) {
|
|
[self.registeredCellClasses addObject:cellClass];
|
|
[collectionView registerClass:cellClass forCellWithReuseIdentifier:identifier];
|
|
}
|
|
return [collectionView dequeueReusableCellWithReuseIdentifier:identifier forIndexPath:indexPath];
|
|
}
|
|
|
|
- (__kindof UICollectionViewCell *)dequeueReusableCellFromStoryboardWithIdentifier:(NSString *)identifier
|
|
forSectionController:(IGListSectionController <IGListSectionType> *)sectionController
|
|
atIndex:(NSInteger)index {
|
|
IGAssertMainThread();
|
|
IGParameterAssert(sectionController != nil);
|
|
IGParameterAssert(identifier.length > 0);
|
|
UICollectionView *collectionView = self.collectionView;
|
|
IGAssert(collectionView != nil, @"Reloading adapter without a collection view.");
|
|
NSIndexPath *indexPath = [self indexPathForSectionController:sectionController index:index];
|
|
return [collectionView dequeueReusableCellWithReuseIdentifier:identifier forIndexPath:indexPath];
|
|
}
|
|
|
|
- (UICollectionViewCell *)dequeueReusableCellWithNibName:(NSString *)nibName
|
|
bundle:(NSBundle *)bundle
|
|
forSectionController:(IGListSectionController<IGListSectionType> *)sectionController
|
|
atIndex:(NSInteger)index {
|
|
IGAssertMainThread();
|
|
IGParameterAssert(nibName != nil);
|
|
IGParameterAssert(sectionController != nil);
|
|
IGParameterAssert(index >= 0);
|
|
UICollectionView *collectionView = self.collectionView;
|
|
IGAssert(collectionView != nil, @"Dequeueing cell with nib name %@ and bundle %@ from section controller %@ without a collection view at index %zi.", nibName, bundle, sectionController, index);
|
|
NSIndexPath *indexPath = [self indexPathForSectionController:sectionController index:index];
|
|
if (![self.registeredNibNames containsObject:nibName]) {
|
|
[self.registeredNibNames addObject:nibName];
|
|
UINib *nib = [UINib nibWithNibName:nibName bundle:bundle];
|
|
[collectionView registerNib:nib forCellWithReuseIdentifier:nibName];
|
|
}
|
|
return [collectionView dequeueReusableCellWithReuseIdentifier:nibName forIndexPath:indexPath];
|
|
}
|
|
|
|
- (__kindof UICollectionReusableView *)dequeueReusableSupplementaryViewOfKind:(NSString *)elementKind
|
|
forSectionController:(IGListSectionController <IGListSectionType> *)sectionController
|
|
class:(Class)viewClass
|
|
atIndex:(NSInteger)index {
|
|
IGAssertMainThread();
|
|
IGParameterAssert(elementKind.length > 0);
|
|
IGParameterAssert(sectionController != nil);
|
|
IGParameterAssert(viewClass != nil);
|
|
IGParameterAssert(index >= 0);
|
|
UICollectionView *collectionView = self.collectionView;
|
|
IGAssert(collectionView != nil, @"Dequeueing cell of class %@ from section controller %@ without a collection view at index %zi with supplementary view %@", NSStringFromClass(viewClass), sectionController, index, elementKind);
|
|
NSString *identifier = IGListReusableViewIdentifier(viewClass, nil, elementKind);
|
|
NSIndexPath *indexPath = [self indexPathForSectionController:sectionController index:index];
|
|
if (![self.registeredSupplementaryViewIdentifiers containsObject:identifier]) {
|
|
[self.registeredSupplementaryViewIdentifiers addObject:identifier];
|
|
[collectionView registerClass:viewClass forSupplementaryViewOfKind:elementKind withReuseIdentifier:identifier];
|
|
}
|
|
return [collectionView dequeueReusableSupplementaryViewOfKind:elementKind withReuseIdentifier:identifier forIndexPath:indexPath];
|
|
}
|
|
|
|
- (__kindof UICollectionReusableView *)dequeueReusableSupplementaryViewFromStoryboardOfKind:(NSString *)elementKind
|
|
withIdentifier:(NSString *)identifier
|
|
forSectionController:(IGListSectionController <IGListSectionType> *)sectionController
|
|
atIndex:(NSInteger)index {
|
|
IGAssertMainThread();
|
|
IGParameterAssert(elementKind.length > 0);
|
|
IGParameterAssert(identifier.length > 0);
|
|
IGParameterAssert(sectionController != nil);
|
|
IGParameterAssert(index >= 0);
|
|
UICollectionView *collectionView = self.collectionView;
|
|
IGAssert(collectionView != nil, @"Dequeueing Supplementary View from storyboard of kind %@ with identifier %@ for section controller %@ without a collection view at index %zi", elementKind, identifier, sectionController, index);
|
|
NSIndexPath *indexPath = [self indexPathForSectionController:sectionController index:index];
|
|
return [collectionView dequeueReusableSupplementaryViewOfKind:elementKind withReuseIdentifier:identifier forIndexPath:indexPath];
|
|
}
|
|
|
|
- (void)reloadInSectionController:(IGListSectionController<IGListSectionType> *)sectionController atIndexes:(NSIndexSet *)indexes {
|
|
IGAssertMainThread();
|
|
IGParameterAssert(indexes != nil);
|
|
IGParameterAssert(sectionController != nil);
|
|
UICollectionView *collectionView = self.collectionView;
|
|
IGAssert(collectionView != nil, @"Tried to reload the adapter from %@ without a collection view at indexes %@.", sectionController, indexes);
|
|
|
|
if (indexes.count == 0) {
|
|
return;
|
|
}
|
|
|
|
NSArray *indexPaths = [self indexPathsFromSectionController:sectionController indexes:indexes adjustForUpdateBlock:YES];
|
|
[self.updatingDelegate reloadItemsInCollectionView:collectionView indexPaths:indexPaths];
|
|
}
|
|
|
|
- (void)insertInSectionController:(IGListSectionController<IGListSectionType> *)sectionController atIndexes:(NSIndexSet *)indexes {
|
|
IGAssertMainThread();
|
|
IGParameterAssert(indexes != nil);
|
|
IGParameterAssert(sectionController != nil);
|
|
UICollectionView *collectionView = self.collectionView;
|
|
IGAssert(collectionView != nil, @"Inserting items from %@ without a collection view at indexes %@.", sectionController, indexes);
|
|
|
|
if (indexes.count == 0) {
|
|
return;
|
|
}
|
|
|
|
NSArray *indexPaths = [self indexPathsFromSectionController:sectionController indexes:indexes adjustForUpdateBlock:NO];
|
|
[self.updatingDelegate insertItemsIntoCollectionView:collectionView indexPaths:indexPaths];
|
|
}
|
|
|
|
- (void)deleteInSectionController:(IGListSectionController<IGListSectionType> *)sectionController atIndexes:(NSIndexSet *)indexes {
|
|
IGAssertMainThread();
|
|
IGParameterAssert(indexes != nil);
|
|
IGParameterAssert(sectionController != nil);
|
|
UICollectionView *collectionView = self.collectionView;
|
|
IGAssert(collectionView != nil, @"Deleting items from %@ without a collection view at indexes %@.", sectionController, indexes);
|
|
|
|
if (indexes.count == 0) {
|
|
return;
|
|
}
|
|
|
|
NSArray *indexPaths = [self indexPathsFromSectionController:sectionController indexes:indexes adjustForUpdateBlock:YES];
|
|
[self.updatingDelegate deleteItemsFromCollectionView:collectionView indexPaths:indexPaths];
|
|
}
|
|
|
|
- (void)reloadSectionController:(IGListSectionController <IGListSectionType> *)sectionController {
|
|
IGAssertMainThread();
|
|
IGParameterAssert(sectionController != nil);
|
|
UICollectionView *collectionView = self.collectionView;
|
|
IGAssert(collectionView != nil, @"Reloading items from %@ without a collection view.", sectionController);
|
|
|
|
IGListSectionMap *map = [self sectionMapAdjustForUpdateBlock:YES];
|
|
const NSInteger section = [map sectionForSectionController:sectionController];
|
|
if (section == NSNotFound) {
|
|
return;
|
|
}
|
|
|
|
NSIndexSet *sections = [NSIndexSet indexSetWithIndex:section];
|
|
[self.updatingDelegate reloadCollectionView:collectionView sections:sections];
|
|
}
|
|
|
|
- (void)performBatchAnimated:(BOOL)animated updates:(void (^)())updates completion:(void (^)(BOOL))completion {
|
|
IGAssertMainThread();
|
|
IGParameterAssert(updates != nil);
|
|
UICollectionView *collectionView = self.collectionView;
|
|
IGAssert(collectionView != nil, @"Performing batch updates without a collection view.");
|
|
|
|
__weak __typeof__(self) weakSelf = self;
|
|
[self.updatingDelegate performUpdateWithCollectionView:collectionView animated:animated itemUpdates:^{
|
|
weakSelf.isInUpdateBlock = YES;
|
|
updates();
|
|
weakSelf.isInUpdateBlock = NO;
|
|
} completion:completion];
|
|
}
|
|
|
|
|
|
#pragma mark - UICollectionViewDelegateFlowLayout
|
|
|
|
- (CGSize)collectionView:(UICollectionView *)collectionView layout:(UICollectionViewLayout *)collectionViewLayout sizeForItemAtIndexPath:(NSIndexPath *)indexPath {
|
|
IGAssert(![self.collectionViewDelegate respondsToSelector:_cmd], @"IGListAdapter is consuming method also implemented by the collectionViewDelegate: %@", NSStringFromSelector(_cmd));
|
|
return [self sizeForItemAtIndexPath:indexPath];
|
|
}
|
|
|
|
- (UIEdgeInsets)collectionView:(UICollectionView *)collectionView layout:(UICollectionViewLayout *)collectionViewLayout insetForSectionAtIndex:(NSInteger)section {
|
|
IGAssert(![self.collectionViewDelegate respondsToSelector:_cmd], @"IGListAdapter is consuming method also implemented by the collectionViewDelegate: %@", NSStringFromSelector(_cmd));
|
|
return [[self.sectionMap sectionControllerForSection:section] inset];
|
|
}
|
|
|
|
- (CGFloat)collectionView:(UICollectionView *)collectionView layout:(UICollectionViewLayout *)collectionViewLayout minimumLineSpacingForSectionAtIndex:(NSInteger)section {
|
|
IGAssert(![self.collectionViewDelegate respondsToSelector:_cmd], @"IGListAdapter is consuming method also implemented by the collectionViewDelegate: %@", NSStringFromSelector(_cmd));
|
|
return [[self.sectionMap sectionControllerForSection:section] minimumLineSpacing];
|
|
}
|
|
|
|
- (CGFloat)collectionView:(UICollectionView *)collectionView layout:(UICollectionViewLayout *)collectionViewLayout minimumInteritemSpacingForSectionAtIndex:(NSInteger)section {
|
|
IGAssert(![self.collectionViewDelegate respondsToSelector:_cmd], @"IGListAdapter is consuming method also implemented by the collectionViewDelegate: %@", NSStringFromSelector(_cmd));
|
|
return [[self.sectionMap sectionControllerForSection:section] minimumInteritemSpacing];
|
|
}
|
|
|
|
- (CGSize)collectionView:(UICollectionView *)collectionView layout:(UICollectionViewLayout *)collectionViewLayout referenceSizeForHeaderInSection:(NSInteger)section {
|
|
IGAssert(![self.collectionViewDelegate respondsToSelector:_cmd], @"IGListAdapter is consuming method also implemented by the collectionViewDelegate: %@", NSStringFromSelector(_cmd));
|
|
NSIndexPath *indexPath = [NSIndexPath indexPathForItem:0 inSection:section];
|
|
return [self sizeForSupplementaryViewOfKind:UICollectionElementKindSectionHeader atIndexPath:indexPath];
|
|
}
|
|
|
|
- (CGSize)collectionView:(UICollectionView *)collectionView layout:(UICollectionViewLayout*)collectionViewLayout referenceSizeForFooterInSection:(NSInteger)section {
|
|
IGAssert(![self.collectionViewDelegate respondsToSelector:_cmd], @"IGListAdapter is consuming method also implemented by the collectionViewDelegate: %@", NSStringFromSelector(_cmd));
|
|
NSIndexPath *indexPath = [NSIndexPath indexPathForItem:0 inSection:section];
|
|
return [self sizeForSupplementaryViewOfKind:UICollectionElementKindSectionFooter atIndexPath:indexPath];
|
|
}
|
|
|
|
@end
|