IGListKit/Source/IGListStackedSectionController.m
Ryan Nystrom 4dec244416 Add item-level moves to IGListCollectionContext
Summary:
Adding an API to do item-level (cell) moves on the collection view. This complicates things a little bit because of all the issues that moving sections have while in batch updates (e.g. simultaneous animation UICV bugs). Thankfully we use pretty strict types so the compiler does most of the work for us.

Closes #145

- [x] Tests build and pass
- [x] Add `IGListBatchUpdateData` tests to check moves during
  - [x] ~~Moving within a reloaded section (no op)~~ can't reload sections
  - [x] Moving within a deleted section (no op)
  - [x] Moving within a moved section (convert section ops to delete+insert)
  - [x] Moving an index path that is also reloaded (convert to delete+insert path)
- [x] Add move unit tests to `IGListAdapterUpdater`
- [x] Add move unit tests to `IGListReloadDataUpdater` (mostly for code coverage...)
- [x] Add move unit tests to `IGListStackedSectionController`
- [x] Add `CHANGELOG.md` entry for 3.0.0
- [x] Test moving without batch
Closes https://github.com/Instagram/IGListKit/pull/418

Reviewed By: jessesquires

Differential Revision: D4521732

Pulled By: rnystrom

fbshipit-source-id: 99a46d1cbb0cc1f857a62ff6ca257aff6e8b7f25
2017-02-10 18:01:18 -08:00

387 lines
21 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 "IGListStackedSectionControllerInternal.h"
#import <objc/runtime.h>
#import <IGListKit/IGListAssert.h>
#import <IGListKit/IGListSupplementaryViewSource.h>
#import "IGListSectionControllerInternal.h"
@interface UICollectionViewCell (IGListStackedSectionController)
@end
@implementation UICollectionViewCell (IGListStackedSectionController)
static void * kStackedSectionControllerKey = &kStackedSectionControllerKey;
- (void)ig_setStackedSectionController:(id)stackedSectionController {
objc_setAssociatedObject(self, kStackedSectionControllerKey, stackedSectionController, OBJC_ASSOCIATION_RETAIN_NONATOMIC);
}
- (id)ig_stackedSectionController {
return objc_getAssociatedObject(self, kStackedSectionControllerKey);
}
static void * kStackedSectionControllerIndexKey = &kStackedSectionControllerIndexKey;
- (void)ig_setStackedSectionControllerIndex:(NSInteger)stackedSectionControllerIndex {
objc_setAssociatedObject(self, kStackedSectionControllerIndexKey, @(stackedSectionControllerIndex), OBJC_ASSOCIATION_COPY_NONATOMIC);
}
- (NSInteger)ig_stackedSectionControllerIndex {
return [objc_getAssociatedObject(self, kStackedSectionControllerIndexKey) integerValue];
}
@end
@implementation IGListStackedSectionController
- (instancetype)initWithSectionControllers:(NSArray <IGListSectionController<IGListSectionType> *> *)sectionControllers {
if (self = [super init]) {
for (IGListSectionController<IGListSectionType> *sectionController in sectionControllers) {
sectionController.collectionContext = self;
sectionController.viewController = self.viewController;
}
_visibleSectionControllers = [[NSCountedSet alloc] init];
_sectionControllers = [NSOrderedSet orderedSetWithArray:sectionControllers];
self.displayDelegate = self;
self.scrollDelegate = self;
self.workingRangeDelegate = self;
}
return self;
}
#pragma mark - Private API
- (void)reloadData {
NSMutableArray *sectionControllers = [[NSMutableArray alloc] init];
NSMutableArray *offsets = [[NSMutableArray alloc] init];
NSInteger numberOfItems = 0;
for (IGListSectionController<IGListSectionType> *sectionController in self.sectionControllers) {
[offsets addObject:@(numberOfItems)];
const NSInteger items = [sectionController numberOfItems];
for (NSInteger i = 0; i < items; i++) {
[sectionControllers addObject:sectionController];
}
numberOfItems += items;
}
self.sectionControllerOffsets = offsets;
self.flattenedNumberOfItems = numberOfItems;
self.sectionControllersForItems = sectionControllers;
IGAssert(self.sectionControllerOffsets.count == self.sectionControllers.count, @"Not enough offsets for section controllers");
IGAssert(self.sectionControllersForItems.count == self.flattenedNumberOfItems, @"Controller map does not equal total number of items");
}
- (IGListSectionController <IGListSectionType> *)sectionControllerForObjectIndex:(NSInteger)itemIndex {
return self.sectionControllersForItems[itemIndex];
}
- (NSInteger)offsetForSectionController:(IGListSectionController<IGListSectionType> *)sectionController {
const NSInteger index = [self.sectionControllers indexOfObject:sectionController];
IGAssert(index != NSNotFound, @"Querying offset for an undocumented section controller");
return [self.sectionControllerOffsets[index] integerValue];
}
- (NSInteger)localIndexForSectionController:(IGListSectionController<IGListSectionType> *)sectionController index:(NSInteger)index {
const NSInteger offset = [self offsetForSectionController:sectionController];
IGAssert(offset <= index, @"Section controller offset must be less than or equal to the item index");
return index - offset;
}
- (NSInteger)relativeIndexForSectionController:(IGListSectionController<IGListSectionType> *)sectionController fromLocalIndex:(NSInteger)index {
const NSInteger offset = [self offsetForSectionController:sectionController];
return index + offset;
}
- (NSIndexSet *)itemIndexesForSectionController:(IGListSectionController<IGListSectionType> *)sectionController indexes:(NSIndexSet *)indexes {
const NSInteger offset = [self offsetForSectionController:sectionController];
NSMutableIndexSet *itemIndexes = [[NSMutableIndexSet alloc] init];
[indexes enumerateIndexesUsingBlock:^(NSUInteger idx, BOOL *stop) {
[itemIndexes addIndex:(idx + offset)];
}];
return itemIndexes;
}
- (id<IGListSupplementaryViewSource>)supplementaryViewSource {
for (IGListSectionController *sectionController in self.sectionControllers) {
id<IGListSupplementaryViewSource> supplementaryViewSource = sectionController.supplementaryViewSource;
if (supplementaryViewSource != nil) {
return supplementaryViewSource;
}
}
return nil;
}
#pragma mark - IGListSectionType
- (NSInteger)numberOfItems {
return self.flattenedNumberOfItems;
}
- (CGSize)sizeForItemAtIndex:(NSInteger)index {
IGListSectionController<IGListSectionType> *sectionController = [self sectionControllerForObjectIndex:index];
const NSInteger localIndex = [self localIndexForSectionController:sectionController index:index];
return [sectionController sizeForItemAtIndex:localIndex];
}
- (UICollectionViewCell *)cellForItemAtIndex:(NSInteger)index {
IGListSectionController<IGListSectionType> *sectionController = [self sectionControllerForObjectIndex:index];
const NSInteger localIndex = [self localIndexForSectionController:sectionController index:index];
return [sectionController cellForItemAtIndex:localIndex];
}
- (void)didUpdateToObject:(id)object {
for (IGListSectionController<IGListSectionType> *sectionController in self.sectionControllers) {
[sectionController didUpdateToObject:object];
}
[self reloadData];
}
- (void)didSelectItemAtIndex:(NSInteger)index {
IGListSectionController<IGListSectionType> *sectionController = [self sectionControllerForObjectIndex:index];
const NSInteger localIndex = [self localIndexForSectionController:sectionController index:index];
[sectionController didSelectItemAtIndex:localIndex];
}
#pragma mark - IGListCollectionContext
- (CGSize)containerSize {
return [self.collectionContext containerSize];
}
- (NSInteger)indexForCell:(UICollectionViewCell *)cell sectionController:(IGListSectionController<IGListSectionType> *)sectionController {
const NSInteger index = [self.collectionContext indexForCell:cell sectionController:self];
return [self localIndexForSectionController:sectionController index:index];
}
- (UICollectionViewCell *)cellForItemAtIndex:(NSInteger)index sectionController:(IGListSectionController<IGListSectionType> *)sectionController {
return [self.collectionContext cellForItemAtIndex: [self relativeIndexForSectionController:sectionController fromLocalIndex:index] sectionController:self];
}
- (NSArray<UICollectionViewCell *> *)visibleCellsForSectionController:(IGListSectionController<IGListSectionType> *)sectionController {
NSMutableArray *cells = [NSMutableArray new];
id<IGListCollectionContext> collectionContext = self.collectionContext;
NSArray *visibleCells = [collectionContext visibleCellsForSectionController:self];
for (UICollectionViewCell *cell in visibleCells) {
const NSInteger index = [collectionContext indexForCell:cell sectionController:self];
if (self.sectionControllersForItems[index] == sectionController) {
[cells addObject:cell];
}
}
return cells;
}
- (void)deselectItemAtIndex:(NSInteger)index sectionController:(IGListSectionController<IGListSectionType> *)sectionController animated:(BOOL)animated {
const NSInteger offsetIndex = [self relativeIndexForSectionController:sectionController fromLocalIndex:index];
[self.collectionContext deselectItemAtIndex:offsetIndex sectionController:self animated:animated];
}
- (NSInteger)sectionForSectionController:(IGListSectionController<IGListSectionType> *)sectionController {
return [self.collectionContext sectionForSectionController:self];
}
- (UICollectionViewCell *)dequeueReusableCellOfClass:(Class)cellClass
forSectionController:(IGListSectionController<IGListSectionType> *)sectionController
atIndex:(NSInteger)index {
const NSInteger offsetIndex = [self relativeIndexForSectionController:sectionController fromLocalIndex:index];
return (UICollectionViewCell *_Nonnull)[self.collectionContext dequeueReusableCellOfClass:cellClass
forSectionController:self
atIndex:offsetIndex];
}
- (UICollectionViewCell *)dequeueReusableCellWithNibName:(NSString *)nibName
bundle:(NSBundle *)bundle
forSectionController:(IGListSectionController<IGListSectionType> *)sectionController
atIndex:(NSInteger)index {
const NSInteger offsetIndex = [self relativeIndexForSectionController:sectionController fromLocalIndex:index];
return (UICollectionViewCell *_Nonnull)[self.collectionContext dequeueReusableCellWithNibName:nibName
bundle:bundle
forSectionController:self
atIndex:offsetIndex];
}
- (UICollectionViewCell *)dequeueReusableCellFromStoryboardWithIdentifier:(NSString *)identifier
forSectionController:(IGListSectionController <IGListSectionType> *)sectionController
atIndex:(NSInteger)index {
const NSInteger offsetIndex = [self relativeIndexForSectionController:sectionController fromLocalIndex:index];
return (UICollectionViewCell *_Nonnull)[self.collectionContext dequeueReusableCellFromStoryboardWithIdentifier:identifier
forSectionController:self
atIndex:offsetIndex];
}
- (UICollectionReusableView *)dequeueReusableSupplementaryViewOfKind:(NSString *)elementKind
forSectionController:(IGListSectionController<IGListSectionType> *)sectionController
class:(Class)viewClass
atIndex:(NSInteger)index {
const NSInteger offsetIndex = [self relativeIndexForSectionController:sectionController fromLocalIndex:index];
return (UICollectionViewCell *_Nonnull)[self.collectionContext dequeueReusableSupplementaryViewOfKind:elementKind
forSectionController:self
class:viewClass
atIndex:offsetIndex];
}
- (UICollectionReusableView *)dequeueReusableSupplementaryViewFromStoryboardOfKind:(NSString *)elementKind
withIdentifier:(NSString *)identifier
forSectionController:(IGListSectionController<IGListSectionType> *)sectionController
atIndex:(NSInteger)index {
const NSInteger offsetIndex = [self relativeIndexForSectionController:sectionController fromLocalIndex:index];
return (UICollectionViewCell *_Nonnull)[self.collectionContext dequeueReusableSupplementaryViewFromStoryboardOfKind:elementKind
withIdentifier:identifier
forSectionController:self
atIndex:offsetIndex];
}
- (UICollectionReusableView *)dequeueReusableSupplementaryViewOfKind:(NSString *)elementKind
forSectionController:(IGListSectionController<IGListSectionType> *)sectionController
nibName:(NSString *)nibName
bundle:(NSBundle *)bundle
atIndex:(NSInteger)index {
const NSInteger offsetIndex = [self relativeIndexForSectionController:sectionController fromLocalIndex:index];
return (UICollectionViewCell *_Nonnull)[self.collectionContext dequeueReusableSupplementaryViewOfKind:elementKind
forSectionController:self
nibName:nibName
bundle:bundle
atIndex:offsetIndex];
}
- (void)reloadInSectionController:(IGListSectionController<IGListSectionType> *)sectionController atIndexes:(NSIndexSet *)indexes {
NSIndexSet *itemIndexes = [self itemIndexesForSectionController:sectionController indexes:indexes];
[self.collectionContext reloadInSectionController:self atIndexes:itemIndexes];
}
- (void)insertInSectionController:(IGListSectionController<IGListSectionType> *)sectionController atIndexes:(NSIndexSet *)indexes {
[self reloadData];
NSIndexSet *itemIndexes = [self itemIndexesForSectionController:sectionController indexes:indexes];
[self.collectionContext insertInSectionController:self atIndexes:itemIndexes];
}
- (void)deleteInSectionController:(IGListSectionController<IGListSectionType> *)sectionController atIndexes:(NSIndexSet *)indexes {
[self reloadData];
NSIndexSet *itemIndexes = [self itemIndexesForSectionController:sectionController indexes:indexes];
[self.collectionContext deleteInSectionController:self atIndexes:itemIndexes];
}
- (void)moveInSectionController:(IGListSectionController<IGListSectionType> *)sectionController fromIndex:(NSInteger)fromIndex toIndex:(NSInteger)toIndex {
[self reloadData];
const NSInteger fromRelativeIndex = [self relativeIndexForSectionController:sectionController fromLocalIndex:fromIndex];
const NSInteger toRelativeIndex = [self relativeIndexForSectionController:sectionController fromLocalIndex:toIndex];
[self.collectionContext moveInSectionController:self fromIndex:fromRelativeIndex toIndex:toRelativeIndex];
}
- (void)reloadSectionController:(IGListSectionController<IGListSectionType> *)sectionController {
[self reloadData];
[self.collectionContext reloadSectionController:self];
}
- (void)performBatchAnimated:(BOOL)animated updates:(void (^)())updates completion:(void (^)(BOOL))completion {
[self.collectionContext performBatchAnimated:animated updates:^{
updates();
} completion:^(BOOL finished) {
[self reloadData];
if (completion) {
completion(finished);
}
}];
}
- (void)scrollToSectionController:(IGListSectionController<IGListSectionType> *)sectionController
atIndex:(NSInteger)index
scrollPosition:(UICollectionViewScrollPosition)scrollPosition
animated:(BOOL)animated {
const NSInteger offsetIndex = [self relativeIndexForSectionController:sectionController fromLocalIndex:index];
[self.collectionContext scrollToSectionController:self
atIndex:offsetIndex
scrollPosition:scrollPosition
animated:animated];
}
#pragma mark - IGListDisplayDelegate
- (void)listAdapter:(IGListAdapter *)listAdapter willDisplaySectionController:(IGListSectionController<IGListSectionType> *)sectionController cell:(UICollectionViewCell *)cell atIndex:(NSInteger)index {
IGListSectionController<IGListSectionType> *childSectionController = [self sectionControllerForObjectIndex:index];
const NSInteger localIndex = [self localIndexForSectionController:childSectionController index:index];
// update the assoc objects for use in didEndDisplay
[cell ig_setStackedSectionController:childSectionController];
[cell ig_setStackedSectionControllerIndex:localIndex];
NSCountedSet *visibleSectionControllers = self.visibleSectionControllers;
id<IGListDisplayDelegate> displayDelegate = [childSectionController displayDelegate];
if ([visibleSectionControllers countForObject:childSectionController] == 0) {
[displayDelegate listAdapter:listAdapter willDisplaySectionController:childSectionController];
}
[displayDelegate listAdapter:listAdapter willDisplaySectionController:childSectionController cell:cell atIndex:localIndex];
[visibleSectionControllers addObject:childSectionController];
}
- (void)listAdapter:(IGListAdapter *)listAdapter didEndDisplayingSectionController:(IGListSectionController<IGListSectionType> *)sectionController cell:(UICollectionViewCell *)cell atIndex:(NSInteger)index {
const NSInteger localIndex = [cell ig_stackedSectionControllerIndex];
IGListSectionController<IGListSectionType> *childSectionController = [cell ig_stackedSectionController];
NSCountedSet *visibleSectionControllers = self.visibleSectionControllers;
id<IGListDisplayDelegate> displayDelegate = [childSectionController displayDelegate];
[displayDelegate listAdapter:listAdapter didEndDisplayingSectionController:childSectionController cell:cell atIndex:localIndex];
[visibleSectionControllers removeObject:childSectionController];
if ([visibleSectionControllers countForObject:childSectionController] == 0) {
[displayDelegate listAdapter:listAdapter didEndDisplayingSectionController:childSectionController];
}
}
- (void)listAdapter:(IGListAdapter *)listAdapter willDisplaySectionController:(IGListSectionController<IGListSectionType> *)sectionController {}
- (void)listAdapter:(IGListAdapter *)listAdapter didEndDisplayingSectionController:(IGListSectionController<IGListSectionType> *)sectionController {}
#pragma mark - IGListScrollDelegate
- (void)listAdapter:(IGListAdapter *)listAdapter didScrollSectionController:(IGListSectionController<IGListSectionType> *)sectionController {
for (IGListSectionController<IGListSectionType> *childSectionController in self.sectionControllers) {
[[childSectionController scrollDelegate] listAdapter:listAdapter didScrollSectionController:childSectionController];
}
}
- (void)listAdapter:(IGListAdapter *)listAdapter willBeginDraggingSectionController:(IGListSectionController<IGListSectionType> *)sectionController {
for (IGListSectionController<IGListSectionType> *childSectionController in self.sectionControllers) {
[[childSectionController scrollDelegate] listAdapter:listAdapter willBeginDraggingSectionController:childSectionController];
}
}
- (void)listAdapter:(IGListAdapter *)listAdapter didEndDraggingSectionController:(IGListSectionController<IGListSectionType> *)sectionController willDecelerate:(BOOL)decelerate {
for (IGListSectionController<IGListSectionType> *childSectionController in self.sectionControllers) {
[[childSectionController scrollDelegate] listAdapter:listAdapter didEndDraggingSectionController:childSectionController willDecelerate:decelerate];
}
}
#pragma mark - IGListWorkingRangeDelegate
- (void)listAdapter:(IGListAdapter *)listAdapter sectionControllerWillEnterWorkingRange:(IGListSectionController<IGListSectionType> *)sectionController {
for (IGListSectionController<IGListSectionType> *childSectionController in self.sectionControllers) {
[[childSectionController workingRangeDelegate] listAdapter:listAdapter sectionControllerWillEnterWorkingRange:childSectionController];
}
}
- (void)listAdapter:(IGListAdapter *)listAdapter sectionControllerDidExitWorkingRange:(IGListSectionController<IGListSectionType> *)sectionController {
for (IGListSectionController<IGListSectionType> *childSectionController in self.sectionControllers) {
[[childSectionController workingRangeDelegate] listAdapter:listAdapter sectionControllerDidExitWorkingRange:childSectionController];
}
}
@end