ComponentKit/TransactionalDataSources/Common/CKDataSourceAppliedChanges.mm (201 lines of code) (raw):
/*
* 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 "CKDataSourceAppliedChanges.h"
#import <ComponentKit/RCEqualityHelpers.h>
#import <ComponentKit/CKMacros.h>
#import "CKIndexSetDescription.h"
#import "ComponentUtilities.h"
@implementation CKDataSourceAppliedChanges
- (instancetype)init
{
return [self initWithUpdatedIndexPaths:nil
removedIndexPaths:nil
removedSections:nil
movedIndexPaths:nil
insertedSections:nil
insertedIndexPaths:nil
userInfo:nil];
}
- (instancetype)initWithUpdatedIndexPaths:(NSSet *)updatedIndexPaths
removedIndexPaths:(NSSet *)removedIndexPaths
removedSections:(NSIndexSet *)removedSections
movedIndexPaths:(NSDictionary *)movedIndexPaths
insertedSections:(NSIndexSet *)insertedSections
insertedIndexPaths:(NSSet *)insertedIndexPaths
userInfo:(NSDictionary *)userInfo
{
if (self = [super init]) {
_updatedIndexPaths = [updatedIndexPaths copy] ?: [NSSet set];
_removedIndexPaths = [removedIndexPaths copy] ?: [NSSet set];
_removedSections = [removedSections copy] ?: [NSIndexSet indexSet];
_movedIndexPaths = [movedIndexPaths copy] ?: @{};
_insertedSections = [insertedSections copy] ?: [NSIndexSet indexSet];
_insertedIndexPaths = [insertedIndexPaths copy] ?: [NSSet set];
_userInfo = [userInfo copy];
_finalUpdatedIndexPaths = finalUpdatedIndexPaths(_updatedIndexPaths,
_removedIndexPaths,
_removedSections,
_movedIndexPaths,
_insertedSections,
_insertedIndexPaths);
}
return self;
}
- (BOOL)isEmpty
{
return [_updatedIndexPaths count] == 0 && [_removedIndexPaths count] == 0 &&
[_removedSections count] == 0 && [_movedIndexPaths count] == 0 &&
[_insertedSections count] == 0 && [_insertedIndexPaths count] == 0;
}
- (NSString *)description
{
if ([self isEmpty]) {
return @"";
}
auto const description = [NSMutableString new];
[description appendString:@"{\n"];
[description appendString:indexPathsDescriptionWithTitle(_updatedIndexPaths, @"Updated Items")];
[description appendString:indexPathsDescriptionWithTitle(_removedIndexPaths, @"Removed Items")];
[description appendString:withNewLineIfNotEmpty(CK::indexSetDescription(_removedSections, @"Removed Sections", 2))];
[description appendString:indexPathToIndexPathMapDescriptionWithTitle(_movedIndexPaths, @"Moved Items")];
[description appendString:withNewLineIfNotEmpty(CK::indexSetDescription(_insertedSections, @"Inserted Sections", 2))];
[description appendString:indexPathsDescriptionWithTitle(_insertedIndexPaths, @"Inserted Items")];
[description appendString:@"}"];
return description;
}
- (BOOL)isEqual:(id)object
{
return RCCompareObjectEquality(self, object, ^BOOL(CKDataSourceAppliedChanges *a, CKDataSourceAppliedChanges *b) {
return RCObjectIsEqual(a.updatedIndexPaths, b.updatedIndexPaths)
&& RCObjectIsEqual(a.removedIndexPaths, b.removedIndexPaths)
&& RCObjectIsEqual(a.removedSections, b.removedSections)
&& RCObjectIsEqual(a.movedIndexPaths, b.movedIndexPaths)
&& RCObjectIsEqual(a.insertedSections, b.insertedSections)
&& RCObjectIsEqual(a.insertedIndexPaths, b.insertedIndexPaths)
&& RCObjectIsEqual(a.userInfo, b.userInfo);
});
}
- (NSUInteger)hash
{
NSUInteger subhashes[] = {
[_updatedIndexPaths hash],
[_removedIndexPaths hash],
[_removedSections hash],
[_movedIndexPaths hash],
[_insertedSections hash],
[_insertedIndexPaths hash],
[_userInfo hash],
};
return RCIntegerArrayHash(subhashes, CK_ARRAY_COUNT(subhashes));
}
static auto withNewLineIfNotEmpty(NSString *s) -> NSString *
{
return s.length > 0 ? [s stringByAppendingString:@"\n"] : @"";
}
static auto indexPathsDescriptionWithTitle(NSSet<NSIndexPath *> *indexPaths, NSString *title) -> NSString *
{
if ([indexPaths count] == 0) {
return @"";
}
auto description = [NSMutableString new];
[description appendFormat:@" %@: {\n", title];
auto const sortedIndexPaths = [[indexPaths allObjects] sortedArrayUsingSelector:@selector(compare:)];
auto const indexPathStrs = [NSMutableArray<NSString *> new];
for (NSIndexPath *const indexPath : sortedIndexPaths) {
auto const ipStr = [NSString stringWithFormat:@" (%ld-%ld)", (long)indexPath.section, (long)indexPath.item];
[indexPathStrs addObject:ipStr];
}
[description appendString:[indexPathStrs componentsJoinedByString:@",\n"]];
[description appendString:@"\n }\n"];
return description;
}
static auto indexPathToIndexPathMapDescriptionWithTitle(NSDictionary<NSIndexPath *, NSIndexPath *> *map, NSString *title) -> NSString *
{
if ([map count] == 0) {
return @"";
}
auto description = [NSMutableString new];
[description appendFormat:@" %@: {\n", title];
auto const sortedIndexPaths = [[map allKeys] sortedArrayUsingSelector:@selector(compare:)];
auto const indexPathStrs = [NSMutableArray<NSString *> new];
for (NSIndexPath *const indexPath : sortedIndexPaths) {
auto const toIndexPath = map[indexPath];
auto const ipStr = [NSString stringWithFormat:@" (%ld-%ld) -> (%ld-%ld)",
(long)indexPath.section, (long)indexPath.item,
(long)toIndexPath.section, (long)toIndexPath.item];
[indexPathStrs addObject:ipStr];
}
[description appendString:[indexPathStrs componentsJoinedByString:@",\n"]];
[description appendString:@"\n }\n"];
return description;
}
static NSArray *sortedIndexPaths(NSArray<NSIndexPath *> *indexPaths, BOOL reverse)
{
return [indexPaths sortedArrayUsingComparator:^NSComparisonResult(NSIndexPath *_Nonnull obj1, NSIndexPath *_Nonnull obj2) {
NSComparisonResult sectionComparison = [@(obj1.section) compare:@(obj2.section)];
if (sectionComparison != NSOrderedSame) {
return (NSComparisonResult)(sectionComparison * (reverse ? -1 : 1));
} else {
return (NSComparisonResult)([@(obj1.row) compare:@(obj2.row)] * (reverse ? -1 : 1));
}
}];
}
static NSIndexPath *indexPathWithDeltas(NSIndexPath *indexPath, NSUInteger sectionDelta, NSUInteger rowDelta)
{
return [NSIndexPath indexPathForRow:(indexPath.row + rowDelta) inSection:(indexPath.section + sectionDelta)];
}
static NSDictionary<NSIndexPath *, NSIndexPath *> *finalUpdatedIndexPaths(NSSet *updatedIndexPaths,
NSSet *removedIndexPaths,
NSIndexSet *removedSections,
NSDictionary *movedIndexPaths,
NSIndexSet *insertedSections,
NSSet *insertedIndexPaths)
{
// The dictionary that will be returned will have the structure:
// (old update index path) -> (new update index path)
NSMutableDictionary<NSIndexPath *, NSIndexPath *> *finalUpdatedIndexPaths = [NSMutableDictionary new];
// Initialize with updated index paths mapping to themselves
for (NSIndexPath *indexPath in updatedIndexPaths) {
finalUpdatedIndexPaths[indexPath] = indexPath;
}
// Translate moves into removals and insertions
NSArray *moveRemovals = [movedIndexPaths allKeys];
NSArray *moveInsertions = [movedIndexPaths allValues];
// Removed rows
// Reverse sort (hence the -1 coefficient) so we don't end up in a situation like the following
// Updating (0, 5) and removing (0,2) (0,3) (0,4). Since the removed index paths are unordered (NSSet) we could end up in a situation where we remove (0,2) and (0,3), so (0,5) -> (0,3) and when we remove (0,4) the updated row is assumed unaffected
NSArray *allRemovals = [[removedIndexPaths allObjects] arrayByAddingObjectsFromArray:moveRemovals];
for (NSIndexPath *removedIndexPath in sortedIndexPaths(allRemovals, YES)) {
for (NSIndexPath *sourceUpdatePath in updatedIndexPaths) {
NSIndexPath *destinationUpdatePath = finalUpdatedIndexPaths[sourceUpdatePath];
if (removedIndexPath.section == destinationUpdatePath.section &&
removedIndexPath.row < destinationUpdatePath.row) {
finalUpdatedIndexPaths[sourceUpdatePath] = indexPathWithDeltas(destinationUpdatePath, 0, -1);
}
}
}
// Removed sections
[removedSections enumerateIndexesWithOptions:NSEnumerationReverse usingBlock:^(NSUInteger removedSection, BOOL *_Nonnull removedSectionsStop) {
for (NSIndexPath *sourceUpdatePath in updatedIndexPaths) {
NSIndexPath *destinationUpdatePath = finalUpdatedIndexPaths[sourceUpdatePath];
if (removedSection < destinationUpdatePath.section) {
finalUpdatedIndexPaths[sourceUpdatePath] = indexPathWithDeltas(destinationUpdatePath, -1, 0);
}
}
}];
// Inserted sections
[insertedSections enumerateIndexesUsingBlock:^(NSUInteger insertedSection, BOOL *_Nonnull insertedSectionsStop) {
for (NSIndexPath *sourceUpdatePath in updatedIndexPaths) {
NSIndexPath *destinationUpdatePath = finalUpdatedIndexPaths[sourceUpdatePath];
if (insertedSection <= destinationUpdatePath.section) {
finalUpdatedIndexPaths[sourceUpdatePath] = indexPathWithDeltas(destinationUpdatePath, 1, 0);
}
}
}];
// Inserted rows
// Sort for the same reason as above when we sorted the removed index paths
// First take care of the "normal" insertions, where we check the destination update path
NSArray *allInsertions = [[insertedIndexPaths allObjects] arrayByAddingObjectsFromArray:moveInsertions];
for (NSIndexPath *insertedIndexPath in sortedIndexPaths(allInsertions, NO)) {
for (NSIndexPath *sourceUpdatePath in updatedIndexPaths) {
NSIndexPath *destinationUpdatePath = finalUpdatedIndexPaths[sourceUpdatePath];
if (insertedIndexPath.section == destinationUpdatePath.section &&
insertedIndexPath.row <= destinationUpdatePath.row) {
finalUpdatedIndexPaths[sourceUpdatePath] = indexPathWithDeltas(destinationUpdatePath, 0, 1);
}
}
}
// The destination index path of a move is *always* correct and we have it take the precedence over all above.
// If one inserts a section at the front, even a move within an existing section
// has to provide the correct final section index.
[movedIndexPaths enumerateKeysAndObjectsUsingBlock:^(NSIndexPath *sourceIndexPath, NSIndexPath *destinationIndexPath, BOOL *_Nonnull stop) {
if (finalUpdatedIndexPaths[sourceIndexPath]) {
finalUpdatedIndexPaths[sourceIndexPath] = destinationIndexPath;
}
}];
return finalUpdatedIndexPaths;
}
@end