mirror of
https://github.com/Instagram/IGListKit
synced 2026-05-15 21:38:18 +00:00
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
160 lines
8 KiB
Text
160 lines
8 KiB
Text
/**
|
|
* 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 "IGListBatchUpdateData.h"
|
|
#import <IGListKit/IGListCompatibility.h>
|
|
|
|
#import <unordered_map>
|
|
|
|
#import <IGListKit/IGListAssert.h>
|
|
|
|
// Filters indexPaths removing all paths that have a section in sections.
|
|
static NSMutableSet *indexPathsMinusSections(NSSet<NSIndexPath *> *indexPaths, NSIndexSet *sections) {
|
|
NSMutableSet *filteredIndexPaths = [indexPaths mutableCopy];
|
|
for (NSIndexPath *indexPath in indexPaths) {
|
|
const NSInteger section = indexPath.section;
|
|
if ([sections containsIndex:section]) {
|
|
[filteredIndexPaths removeObject:indexPath];
|
|
}
|
|
}
|
|
return filteredIndexPaths;
|
|
}
|
|
|
|
// Plucks the given move from available moves and turns it into a delete + insert
|
|
static void convertMoveToDeleteAndInsert(NSMutableSet<IGListMoveIndex *> *moves,
|
|
IGListMoveIndex *move,
|
|
NSMutableIndexSet *deletes,
|
|
NSMutableIndexSet *inserts) {
|
|
[moves removeObject:move];
|
|
|
|
// add a delete and insert respecting the move's from and to sections
|
|
// delete + insert will result in reloading the entire section
|
|
[deletes addIndex:move.from];
|
|
[inserts addIndex:move.to];
|
|
}
|
|
|
|
@implementation IGListBatchUpdateData
|
|
|
|
// Converts all section moves that have index path operations into a section delete + insert.
|
|
+ (void)cleanIndexPathsWithMap:(const std::unordered_map<NSInteger, IGListMoveIndex*> &)map
|
|
moves:(NSMutableSet<IGListMoveIndex *> *)moves
|
|
indexPaths:(NSMutableSet<NSIndexPath *> *)indexPaths
|
|
deletes:(NSMutableIndexSet *)deletes
|
|
inserts:(NSMutableIndexSet *)inserts {
|
|
for (NSIndexPath *path in [indexPaths copy]) {
|
|
const auto it = map.find(path.section);
|
|
if (it != map.end() && it->second != nil) {
|
|
[indexPaths removeObject:path];
|
|
convertMoveToDeleteAndInsert(moves, it->second, deletes, inserts);
|
|
}
|
|
}
|
|
}
|
|
|
|
/**
|
|
Converts all section moves that are also reloaded, or have index path inserts, deletes, or reloads into a section
|
|
delete + insert in order to avoid UICollectionView heap corruptions, exceptions, and animation/snapshot bugs.
|
|
*/
|
|
- (instancetype)initWithInsertSections:(NSIndexSet *)insertSections
|
|
deleteSections:(NSIndexSet *)deleteSections
|
|
moveSections:(NSSet<IGListMoveIndex *> *)moveSections
|
|
insertIndexPaths:(NSSet<NSIndexPath *> *)insertIndexPaths
|
|
deleteIndexPaths:(NSSet<NSIndexPath *> *)deleteIndexPaths
|
|
moveIndexPaths:(NSSet<IGListMoveIndexPath *> *)moveIndexPaths
|
|
reloadIndexPaths:(NSSet<NSIndexPath *> *)reloadIndexPaths {
|
|
IGParameterAssert(insertSections != nil);
|
|
IGParameterAssert(deleteSections != nil);
|
|
IGParameterAssert(moveSections != nil);
|
|
IGParameterAssert(insertIndexPaths != nil);
|
|
IGParameterAssert(deleteIndexPaths != nil);
|
|
IGParameterAssert(moveIndexPaths != nil);
|
|
IGParameterAssert(reloadIndexPaths != nil);
|
|
if (self = [super init]) {
|
|
NSMutableSet<IGListMoveIndex *> *mMoveSections = [moveSections mutableCopy];
|
|
NSMutableIndexSet *mDeleteSections = [deleteSections mutableCopy];
|
|
NSMutableIndexSet *mInsertSections = [insertSections mutableCopy];
|
|
NSMutableSet<IGListMoveIndexPath *> *mMoveIndexPaths = [moveIndexPaths mutableCopy];
|
|
|
|
// these collections should NEVER be mutated during cleanup passes, otherwise sections that have multiple item
|
|
// changes (e.g. a moved section that has a delete + reload on different index paths w/in the section) will only
|
|
// convert one of the item changes into a section delete+insert. this will fail hard and be VERY difficult to
|
|
// debug
|
|
const NSInteger moveCount = [moveSections count];
|
|
std::unordered_map<NSInteger, IGListMoveIndex*> fromMap(moveCount);
|
|
std::unordered_map<NSInteger, IGListMoveIndex*> toMap(moveCount);
|
|
for (IGListMoveIndex *move in moveSections) {
|
|
const NSInteger from = move.from;
|
|
const NSInteger to = move.to;
|
|
|
|
// if the move is already deleted or inserted, discard it and use delete+insert instead
|
|
if ([deleteSections containsIndex:from] || [insertSections containsIndex:to]) {
|
|
convertMoveToDeleteAndInsert(mMoveSections, move, mDeleteSections, mInsertSections);
|
|
} else {
|
|
fromMap[from] = move;
|
|
toMap[to] = move;
|
|
}
|
|
}
|
|
|
|
NSMutableSet<NSIndexPath *> *mInsertIndexPaths = [insertIndexPaths mutableCopy];
|
|
NSMutableSet<NSIndexPath *> *mDeleteIndexPaths = [deleteIndexPaths mutableCopy];
|
|
|
|
// UICollectionView will throw if reloading an index path in a section that is also deleted
|
|
NSMutableSet<NSIndexPath *> *mReloadIndexPaths = indexPathsMinusSections(reloadIndexPaths, deleteSections);
|
|
|
|
// UICollectionView will throw about simultaneous animations when reloading and moving cells at the same time
|
|
[IGListBatchUpdateData cleanIndexPathsWithMap:fromMap moves:mMoveSections indexPaths:mReloadIndexPaths deletes:mDeleteSections inserts:mInsertSections];
|
|
|
|
// avoids a bug where a cell is animated twice and one of the snapshot cells is never removed from the hierarchy
|
|
[IGListBatchUpdateData cleanIndexPathsWithMap:fromMap moves:mMoveSections indexPaths:mDeleteIndexPaths deletes:mDeleteSections inserts:mInsertSections];
|
|
|
|
// prevents a bug where UICollectionView corrupts the heap memory when inserting into a section that is moved
|
|
[IGListBatchUpdateData cleanIndexPathsWithMap:toMap moves:mMoveSections indexPaths:mInsertIndexPaths deletes:mDeleteSections inserts:mInsertSections];
|
|
|
|
for (IGListMoveIndexPath *move in moveIndexPaths) {
|
|
// if the section w/ an index path move is deleted, just drop the move
|
|
if ([deleteSections containsIndex:move.from.section]) {
|
|
[mMoveIndexPaths removeObject:move];
|
|
}
|
|
|
|
// if an index path is moved and reloaded, convert it into a delete+insert
|
|
if ([mReloadIndexPaths containsObject:move.from]) {
|
|
[mReloadIndexPaths removeObject:move.from];
|
|
[mMoveIndexPaths removeObject:move];
|
|
[mDeleteIndexPaths addObject:move.from];
|
|
[mInsertIndexPaths addObject:move.to];
|
|
}
|
|
|
|
// if a move is inside a section that is moved, convert the section move to a delete+insert
|
|
const auto it = fromMap.find(move.from.section);
|
|
if (it != fromMap.end() && it->second != nil) {
|
|
IGListMoveIndex *sectionMove = it->second;
|
|
[mMoveIndexPaths removeObject:move];
|
|
[mMoveSections removeObject:sectionMove];
|
|
[mDeleteSections addIndex:sectionMove.from];
|
|
[mInsertSections addIndex:sectionMove.to];
|
|
}
|
|
}
|
|
|
|
_deleteSections = [mDeleteSections copy];
|
|
_insertSections = [mInsertSections copy];
|
|
_moveSections = [mMoveSections copy];
|
|
_deleteIndexPaths = [mDeleteIndexPaths copy];
|
|
_insertIndexPaths = [mInsertIndexPaths copy];
|
|
_moveIndexPaths = [mMoveIndexPaths copy];
|
|
_reloadIndexPaths = [mReloadIndexPaths copy];
|
|
}
|
|
return self;
|
|
}
|
|
|
|
- (NSString *)description {
|
|
return [NSString stringWithFormat:@"<%@ %p; deleteSections: %zi; insertSections: %zi; moveSections: %zi; deleteIndexPaths: %zi; insertIndexPaths: %zi; reloadIndexPaths: %zi;>",
|
|
NSStringFromClass(self.class), self, self.deleteSections.count, self.insertSections.count, self.moveSections.count,
|
|
self.deleteIndexPaths.count, self.insertIndexPaths.count, self.reloadIndexPaths.count];
|
|
}
|
|
|
|
@end
|