Swiftgram/AsyncDisplayKit/Private/_ASHierarchyChangeSet.m
2016-06-24 13:56:08 -07:00

480 lines
20 KiB
Objective-C

//
// _ASHierarchyChangeSet.m
// AsyncDisplayKit
//
// Created by Adlai Holler on 9/29/15.
//
// Copyright (c) 2014-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 "_ASHierarchyChangeSet.h"
#import "ASInternalHelpers.h"
#import "NSIndexSet+ASHelpers.h"
#import "ASAssert.h"
NSString *NSStringFromASHierarchyChangeType(_ASHierarchyChangeType changeType)
{
switch (changeType) {
case _ASHierarchyChangeTypeInsert:
return @"Insert";
case _ASHierarchyChangeTypeDelete:
return @"Delete";
case _ASHierarchyChangeTypeReload:
return @"Reload";
default:
return @"(invalid)";
}
}
@interface _ASHierarchySectionChange ()
- (instancetype)initWithChangeType:(_ASHierarchyChangeType)changeType indexSet:(NSIndexSet *)indexSet animationOptions:(ASDataControllerAnimationOptions)animationOptions;
/**
On return `changes` is sorted according to the change type with changes coalesced by animationOptions
Assumes: `changes` is [_ASHierarchySectionChange] all with the same changeType
*/
+ (void)sortAndCoalesceChanges:(NSMutableArray *)changes;
/// Returns all the indexes from all the `indexSet`s of the given `_ASHierarchySectionChange` objects.
+ (NSMutableIndexSet *)allIndexesInSectionChanges:(NSArray *)changes;
@end
@interface _ASHierarchyItemChange ()
- (instancetype)initWithChangeType:(_ASHierarchyChangeType)changeType indexPaths:(NSArray *)indexPaths animationOptions:(ASDataControllerAnimationOptions)animationOptions presorted:(BOOL)presorted;
/**
On return `changes` is sorted according to the change type with changes coalesced by animationOptions
Assumes: `changes` is [_ASHierarchyItemChange] all with the same changeType
*/
+ (void)sortAndCoalesceChanges:(NSMutableArray *)changes ignoringChangesInSections:(NSIndexSet *)sections;
@end
@interface _ASHierarchyChangeSet ()
@property (nonatomic, strong, readonly) NSMutableArray<_ASHierarchyItemChange *> *insertItemChanges;
@property (nonatomic, strong, readonly) NSMutableArray<_ASHierarchyItemChange *> *deleteItemChanges;
@property (nonatomic, strong, readonly) NSMutableArray<_ASHierarchyItemChange *> *reloadItemChanges;
@property (nonatomic, strong, readonly) NSMutableArray<_ASHierarchySectionChange *> *insertSectionChanges;
@property (nonatomic, strong, readonly) NSMutableArray<_ASHierarchySectionChange *> *deleteSectionChanges;
@property (nonatomic, strong, readonly) NSMutableArray<_ASHierarchySectionChange *> *reloadSectionChanges;
@end
@implementation _ASHierarchyChangeSet
- (instancetype)init
{
self = [super init];
if (self) {
_insertItemChanges = [NSMutableArray new];
_deleteItemChanges = [NSMutableArray new];
_reloadItemChanges = [NSMutableArray new];
_insertSectionChanges = [NSMutableArray new];
_deleteSectionChanges = [NSMutableArray new];
_reloadSectionChanges = [NSMutableArray new];
}
return self;
}
#pragma mark External API
- (void)markCompleted
{
NSAssert(!_completed, @"Attempt to mark already-completed changeset as completed.");
_completed = YES;
[self _sortAndCoalesceChangeArrays];
}
- (NSArray *)sectionChangesOfType:(_ASHierarchyChangeType)changeType
{
[self _ensureCompleted];
switch (changeType) {
case _ASHierarchyChangeTypeInsert:
return _insertSectionChanges;
case _ASHierarchyChangeTypeReload:
return _reloadSectionChanges;
case _ASHierarchyChangeTypeDelete:
return _deleteSectionChanges;
default:
NSAssert(NO, @"Request for section changes with invalid type: %lu", (long)changeType);
}
}
- (NSArray *)itemChangesOfType:(_ASHierarchyChangeType)changeType
{
[self _ensureCompleted];
switch (changeType) {
case _ASHierarchyChangeTypeInsert:
return _insertItemChanges;
case _ASHierarchyChangeTypeReload:
return _reloadItemChanges;
case _ASHierarchyChangeTypeDelete:
return _deleteItemChanges;
default:
NSAssert(NO, @"Request for item changes with invalid type: %lu", (long)changeType);
}
}
- (NSIndexSet *)indexesForItemChangesOfType:(_ASHierarchyChangeType)changeType inSection:(NSUInteger)section
{
[self _ensureCompleted];
NSMutableIndexSet *result = [NSMutableIndexSet indexSet];
for (_ASHierarchyItemChange *change in [self itemChangesOfType:changeType]) {
[result addIndexes:[NSIndexSet as_indexSetFromIndexPaths:change.indexPaths inSection:section]];
}
return result;
}
- (NSUInteger)newSectionForOldSection:(NSUInteger)oldSection
{
ASDisplayNodeAssertNotNil(_deletedSections, @"Cannot call %@ before `markCompleted` returns.", NSStringFromSelector(_cmd));
ASDisplayNodeAssertNotNil(_insertedSections, @"Cannot call %@ before `markCompleted` returns.", NSStringFromSelector(_cmd));
[self _ensureCompleted];
if ([_deletedSections containsIndex:oldSection]) {
return NSNotFound;
}
NSUInteger newIndex = oldSection - [_deletedSections countOfIndexesInRange:NSMakeRange(0, oldSection)];
newIndex += [_insertedSections as_indexChangeByInsertingItemsBelowIndex:newIndex];
return newIndex;
}
- (void)deleteItems:(NSArray *)indexPaths animationOptions:(ASDataControllerAnimationOptions)options
{
[self _ensureNotCompleted];
_ASHierarchyItemChange *change = [[_ASHierarchyItemChange alloc] initWithChangeType:_ASHierarchyChangeTypeDelete indexPaths:indexPaths animationOptions:options presorted:NO];
[_deleteItemChanges addObject:change];
}
- (void)deleteSections:(NSIndexSet *)sections animationOptions:(ASDataControllerAnimationOptions)options
{
[self _ensureNotCompleted];
_ASHierarchySectionChange *change = [[_ASHierarchySectionChange alloc] initWithChangeType:_ASHierarchyChangeTypeDelete indexSet:sections animationOptions:options];
[_deleteSectionChanges addObject:change];
}
- (void)insertItems:(NSArray *)indexPaths animationOptions:(ASDataControllerAnimationOptions)options
{
[self _ensureNotCompleted];
_ASHierarchyItemChange *change = [[_ASHierarchyItemChange alloc] initWithChangeType:_ASHierarchyChangeTypeInsert indexPaths:indexPaths animationOptions:options presorted:NO];
[_insertItemChanges addObject:change];
}
- (void)insertSections:(NSIndexSet *)sections animationOptions:(ASDataControllerAnimationOptions)options
{
[self _ensureNotCompleted];
_ASHierarchySectionChange *change = [[_ASHierarchySectionChange alloc] initWithChangeType:_ASHierarchyChangeTypeInsert indexSet:sections animationOptions:options];
[_insertSectionChanges addObject:change];
}
- (void)reloadItems:(NSArray *)indexPaths animationOptions:(ASDataControllerAnimationOptions)options
{
[self _ensureNotCompleted];
_ASHierarchyItemChange *change = [[_ASHierarchyItemChange alloc] initWithChangeType:_ASHierarchyChangeTypeReload indexPaths:indexPaths animationOptions:options presorted:NO];
[_reloadItemChanges addObject:change];
}
- (void)reloadSections:(NSIndexSet *)sections animationOptions:(ASDataControllerAnimationOptions)options
{
[self _ensureNotCompleted];
_ASHierarchySectionChange *change = [[_ASHierarchySectionChange alloc] initWithChangeType:_ASHierarchyChangeTypeReload indexSet:sections animationOptions:options];
[_reloadSectionChanges addObject:change];
}
#pragma mark Private
- (BOOL)_ensureNotCompleted
{
NSAssert(!_completed, @"Attempt to modify completed changeset %@", self);
return !_completed;
}
- (BOOL)_ensureCompleted
{
NSAssert(_completed, @"Attempt to process incomplete changeset %@", self);
return _completed;
}
- (void)_sortAndCoalesceChangeArrays
{
@autoreleasepool {
// Split reloaded sections into [delete(oldIndex), insert(newIndex)]
// Give these their "pre-reloads" values. Once we add in the reloads we'll re-process them.
_deletedSections = [_ASHierarchySectionChange allIndexesInSectionChanges:_deleteSectionChanges];
_insertedSections = [_ASHierarchySectionChange allIndexesInSectionChanges:_insertSectionChanges];
for (_ASHierarchySectionChange *change in _reloadSectionChanges) {
NSIndexSet *newSections = [change.indexSet as_indexesByMapping:^(NSUInteger idx) {
NSUInteger newSec = [self newSectionForOldSection:idx];
NSAssert(newSec != NSNotFound, @"Request to reload deleted section %lu", (unsigned long)idx);
return newSec;
}];
_ASHierarchySectionChange *deleteChange = [[_ASHierarchySectionChange alloc] initWithChangeType:_ASHierarchyChangeTypeDelete indexSet:change.indexSet animationOptions:change.animationOptions];
[_deleteSectionChanges addObject:deleteChange];
_ASHierarchySectionChange *insertChange = [[_ASHierarchySectionChange alloc] initWithChangeType:_ASHierarchyChangeTypeInsert indexSet:newSections animationOptions:change.animationOptions];
[_insertSectionChanges addObject:insertChange];
}
_reloadSectionChanges = nil;
[_ASHierarchySectionChange sortAndCoalesceChanges:_deleteSectionChanges];
[_ASHierarchySectionChange sortAndCoalesceChanges:_insertSectionChanges];
_deletedSections = [_ASHierarchySectionChange allIndexesInSectionChanges:_deleteSectionChanges];
_insertedSections = [_ASHierarchySectionChange allIndexesInSectionChanges:_insertSectionChanges];
// Split reloaded items into [delete(oldIndexPath), insert(newIndexPath)]
NSDictionary *insertedIndexPathsMap = [_ASHierarchyItemChange sectionToIndexSetMapFromChanges:_insertItemChanges ofType:_ASHierarchyChangeTypeInsert];
NSDictionary *deletedIndexPathsMap = [_ASHierarchyItemChange sectionToIndexSetMapFromChanges:_deleteItemChanges ofType:_ASHierarchyChangeTypeDelete];
for (_ASHierarchyItemChange *change in _reloadItemChanges) {
NSAssert(change.changeType == _ASHierarchyChangeTypeReload, @"It must be a reload change to be in here");
NSMutableArray *newIndexPaths = [NSMutableArray arrayWithCapacity:change.indexPaths.count];
// Every indexPaths in the change need to update its section and/or row
// depending on all the deletions and insertions
// For reference, when batching reloads/deletes/inserts:
// - delete/reload indexPaths that are passed in should all be their current indexPaths
// - insert indexPaths that are passed in should all be their future indexPaths after deletions
for (NSIndexPath *indexPath in change.indexPaths) {
NSUInteger section = [self newSectionForOldSection:indexPath.section];
NSUInteger item = indexPath.item;
// Update row number based on deletions that are above the current row in the current section
NSIndexSet *indicesDeletedInSection = deletedIndexPathsMap[@(indexPath.section)];
item -= [indicesDeletedInSection countOfIndexesInRange:NSMakeRange(0, item)];
// Update row number based on insertions that are above the current row in the future section
NSIndexSet *indicesInsertedInSection = insertedIndexPathsMap[@(section)];
item += [indicesInsertedInSection as_indexChangeByInsertingItemsBelowIndex:item];
NSIndexPath *newIndexPath = [NSIndexPath indexPathForItem:item inSection:section];
[newIndexPaths addObject:newIndexPath];
}
// All reload changes are translated into deletes and inserts
// We delete the items that needs reload together with other deleted items, at their original index
_ASHierarchyItemChange *deleteItemChangeFromReloadChange = [[_ASHierarchyItemChange alloc] initWithChangeType:_ASHierarchyChangeTypeDelete indexPaths:change.indexPaths animationOptions:change.animationOptions presorted:NO];
[_deleteItemChanges addObject:deleteItemChangeFromReloadChange];
// We insert the items that needs reload together with other inserted items, at their future index
_ASHierarchyItemChange *insertItemChangeFromReloadChange = [[_ASHierarchyItemChange alloc] initWithChangeType:_ASHierarchyChangeTypeInsert indexPaths:newIndexPaths animationOptions:change.animationOptions presorted:NO];
[_insertItemChanges addObject:insertItemChangeFromReloadChange];
}
_reloadItemChanges = nil;
// Ignore item deletes in reloaded/deleted sections.
[_ASHierarchyItemChange sortAndCoalesceChanges:_deleteItemChanges ignoringChangesInSections:_deletedSections];
// Ignore item inserts in reloaded(new)/inserted sections.
[_ASHierarchyItemChange sortAndCoalesceChanges:_insertItemChanges ignoringChangesInSections:_insertedSections];
}
}
- (NSString *)description
{
return [NSString stringWithFormat:@"<%@ %p: deletedSections=%@, insertedSections=%@, deletedItems=%@, insertedItems=%@>", NSStringFromClass(self.class), self, _deletedSections, _insertedSections, _deleteItemChanges, _insertItemChanges];
}
@end
@implementation _ASHierarchySectionChange
- (instancetype)initWithChangeType:(_ASHierarchyChangeType)changeType indexSet:(NSIndexSet *)indexSet animationOptions:(ASDataControllerAnimationOptions)animationOptions
{
self = [super init];
if (self) {
ASDisplayNodeAssert(indexSet.count > 0, @"Request to create _ASHierarchySectionChange with no sections!");
_changeType = changeType;
_indexSet = indexSet;
_animationOptions = animationOptions;
}
return self;
}
+ (void)sortAndCoalesceChanges:(NSMutableArray *)changes
{
if (changes.count < 1) {
return;
}
_ASHierarchyChangeType type = [changes.firstObject changeType];
// Lookup table [Int: AnimationOptions]
NSMutableDictionary *animationOptions = [NSMutableDictionary new];
// All changed indexes, sorted
NSMutableIndexSet *allIndexes = [NSMutableIndexSet new];
for (_ASHierarchySectionChange *change in changes) {
[change.indexSet enumerateIndexesUsingBlock:^(NSUInteger idx, __unused BOOL *stop) {
animationOptions[@(idx)] = @(change.animationOptions);
}];
[allIndexes addIndexes:change.indexSet];
}
// Create new changes by grouping sorted changes by animation option
NSMutableArray *result = [NSMutableArray new];
__block ASDataControllerAnimationOptions currentOptions = 0;
NSMutableIndexSet *currentIndexes = [NSMutableIndexSet indexSet];
NSEnumerationOptions options = type == _ASHierarchyChangeTypeDelete ? NSEnumerationReverse : kNilOptions;
[allIndexes enumerateIndexesWithOptions:options usingBlock:^(NSUInteger idx, __unused BOOL * stop) {
ASDataControllerAnimationOptions options = [animationOptions[@(idx)] integerValue];
// End the previous group if needed.
if (options != currentOptions && currentIndexes.count > 0) {
_ASHierarchySectionChange *change = [[_ASHierarchySectionChange alloc] initWithChangeType:type indexSet:[currentIndexes copy] animationOptions:currentOptions];
[result addObject:change];
[currentIndexes removeAllIndexes];
}
// Start a new group if needed.
if (currentIndexes.count == 0) {
currentOptions = options;
}
[currentIndexes addIndex:idx];
}];
// Finish up the last group.
if (currentIndexes.count > 0) {
_ASHierarchySectionChange *change = [[_ASHierarchySectionChange alloc] initWithChangeType:type indexSet:[currentIndexes copy] animationOptions:currentOptions];
[result addObject:change];
}
[changes setArray:result];
}
+ (NSMutableIndexSet *)allIndexesInSectionChanges:(NSArray<_ASHierarchySectionChange *> *)changes
{
NSMutableIndexSet *indexes = [NSMutableIndexSet indexSet];
for (_ASHierarchySectionChange *change in changes) {
[indexes addIndexes:change.indexSet];
}
return indexes;
}
- (NSString *)description
{
return [NSString stringWithFormat:@"<%@: anim=%lu, type=%@, indexes=%@>", NSStringFromClass(self.class), (unsigned long)_animationOptions, NSStringFromASHierarchyChangeType(_changeType), [self.indexSet as_smallDescription]];
}
@end
@implementation _ASHierarchyItemChange
- (instancetype)initWithChangeType:(_ASHierarchyChangeType)changeType indexPaths:(NSArray *)indexPaths animationOptions:(ASDataControllerAnimationOptions)animationOptions presorted:(BOOL)presorted
{
self = [super init];
if (self) {
ASDisplayNodeAssert(indexPaths.count > 0, @"Request to create _ASHierarchyItemChange with no items!");
_changeType = changeType;
if (presorted) {
_indexPaths = indexPaths;
} else {
SEL sorting = changeType == _ASHierarchyChangeTypeDelete ? @selector(asdk_inverseCompare:) : @selector(compare:);
_indexPaths = [indexPaths sortedArrayUsingSelector:sorting];
}
_animationOptions = animationOptions;
}
return self;
}
// Create a mapping out of changes indexPaths to a {@section : [indexSet]} fashion
// e.g. changes: (0 - 0), (0 - 1), (2 - 5)
// will become: {@0 : [0, 1], @2 : [5]}
+ (NSDictionary *)sectionToIndexSetMapFromChanges:(NSArray *)changes ofType:(_ASHierarchyChangeType)changeType
{
NSMutableDictionary *sectionToIndexSetMap = [NSMutableDictionary dictionary];
for (_ASHierarchyItemChange *change in changes) {
NSAssert(change.changeType == changeType, @"The map we created must all be of the same changeType as of now");
for (NSIndexPath *indexPath in change.indexPaths) {
NSNumber *sectionKey = @(indexPath.section);
NSMutableIndexSet *indexSet = sectionToIndexSetMap[sectionKey];
if (indexSet) {
[indexSet addIndex:indexPath.item];
} else {
indexSet = [NSMutableIndexSet indexSetWithIndex:indexPath.item];
sectionToIndexSetMap[sectionKey] = indexSet;
}
}
}
return sectionToIndexSetMap;
}
+ (void)sortAndCoalesceChanges:(NSMutableArray *)changes ignoringChangesInSections:(NSIndexSet *)ignoredSections
{
if (changes.count < 1) {
return;
}
_ASHierarchyChangeType type = [changes.firstObject changeType];
// Lookup table [NSIndexPath: AnimationOptions]
NSMutableDictionary *animationOptions = [NSMutableDictionary new];
// All changed index paths, sorted
NSMutableArray *allIndexPaths = [NSMutableArray new];
for (_ASHierarchyItemChange *change in changes) {
for (NSIndexPath *indexPath in change.indexPaths) {
if (![ignoredSections containsIndex:indexPath.section]) {
animationOptions[indexPath] = @(change.animationOptions);
[allIndexPaths addObject:indexPath];
}
}
}
SEL sorting = type == _ASHierarchyChangeTypeDelete ? @selector(asdk_inverseCompare:) : @selector(compare:);
[allIndexPaths sortUsingSelector:sorting];
// Create new changes by grouping sorted changes by animation option
NSMutableArray *result = [NSMutableArray new];
ASDataControllerAnimationOptions currentOptions = 0;
NSMutableArray *currentIndexPaths = [NSMutableArray array];
for (NSIndexPath *indexPath in allIndexPaths) {
ASDataControllerAnimationOptions options = [animationOptions[indexPath] integerValue];
// End the previous group if needed.
if (options != currentOptions && currentIndexPaths.count > 0) {
_ASHierarchyItemChange *change = [[_ASHierarchyItemChange alloc] initWithChangeType:type indexPaths:[currentIndexPaths copy] animationOptions:currentOptions presorted:YES];
[result addObject:change];
[currentIndexPaths removeAllObjects];
}
// Start a new group if needed.
if (currentIndexPaths.count == 0) {
currentOptions = options;
}
[currentIndexPaths addObject:indexPath];
}
// Finish up the last group.
if (currentIndexPaths.count > 0) {
_ASHierarchyItemChange *change = [[_ASHierarchyItemChange alloc] initWithChangeType:type indexPaths:[currentIndexPaths copy] animationOptions:currentOptions presorted:YES];
[result addObject:change];
}
[changes setArray:result];
}
- (NSString *)description
{
return [NSString stringWithFormat:@"<%@: anim=%lu, type=%@, indexPaths=%@>", NSStringFromClass(self.class), (unsigned long)_animationOptions, NSStringFromASHierarchyChangeType(_changeType), self.indexPaths];
}
@end