ComponentKit/TransactionalDataSources/Common/Internal/CKDataSourceChangesetModification.mm (333 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 "CKDataSourceChangesetModification.h"
#import <map>
#import <mutex>
#import <ComponentKit/CKExceptionInfo.h>
#import "CKDataSourceConfigurationInternal.h"
#import "CKDataSourceStateInternal.h"
#import "CKDataSourceChange.h"
#import "CKDataSourceChangesetInternal.h"
#import "CKDataSourceItemInternal.h"
#import "CKDataSourceAppliedChanges.h"
#import "CKBuildComponent.h"
#import "CKComponentControllerEvents.h"
#import "CKComponentControllerHelper.h"
#import "CKComponentEvents.h"
#import "CKComponentLayout.h"
#import "CKComponentProvider.h"
#import "CKComponentScopeRoot.h"
#import "CKComponentScopeRootFactory.h"
#import "CKDataSourceModificationHelper.h"
#import "CKIndexSetDescription.h"
#import "CKInvalidChangesetOperationType.h"
#import "CKFatal.h"
using namespace CKComponentControllerHelper;
@implementation CKDataSourceChangesetModification
{
__weak id<CKComponentStateListener> _stateListener;
NSDictionary *_userInfo;
CKDataSourceQOS _qos;
__weak id<CKDataSourceChangesetModificationItemGenerator> _itemGenerator;
std::shared_ptr<CKTreeLayoutCache> _treeLayoutCache;
}
- (instancetype)initWithChangeset:(CKDataSourceChangeset *)changeset
stateListener:(id<CKComponentStateListener>)stateListener
userInfo:(NSDictionary *)userInfo
qos:(CKDataSourceQOS)qos
{
return [self initWithChangeset:changeset stateListener:stateListener userInfo:userInfo qos:qos treeLayoutCache:nullptr];
}
- (instancetype)initWithChangeset:(CKDataSourceChangeset *)changeset
stateListener:(id<CKComponentStateListener>)stateListener
userInfo:(NSDictionary *)userInfo
qos:(CKDataSourceQOS)qos
treeLayoutCache:(std::shared_ptr<CKTreeLayoutCache>)treeLayoutCache
{
if (self = [super init]) {
_changeset = changeset;
_stateListener = stateListener;
_userInfo = [userInfo copy];
_qos = qos;
_treeLayoutCache = std::move(treeLayoutCache);
}
return self;
}
- (void)setItemGenerator:(id<CKDataSourceChangesetModificationItemGenerator>)itemGenerator
{
_itemGenerator = itemGenerator;
}
- (CKDataSourceChange *)changeFromState:(CKDataSourceState *)oldState
{
@try {
return [self __changeFromState:oldState];
} @catch (NSException *exception) {
CKExceptionInfoSetValueForKey(@"ck_changeset", _changeset.description);
CKExceptionInfoSetValueForKey(@"ck_changeset_origin", _changeset.originName);
CKExceptionInfoSetValueForKey(@"ck_user_info", _userInfo.description);
CKExceptionInfoSetValueForKey(@"ck_data_source_state", oldState.description);
CKExceptionInfoSetValueForKey(
@"assert_message",
([NSString stringWithFormat:@"<force_category:%@:force_category> Raised an exception applying modification",
oldState.contentsFingerprint])
);
[exception raise];
} @catch (...) {
CKExceptionInfoSetValueForKey(@"ck_changeset", _changeset.description);
CKExceptionInfoSetValueForKey(@"ck_changeset_origin", _changeset.originName);
CKExceptionInfoSetValueForKey(@"ck_user_info", _userInfo.description);
CKExceptionInfoSetValueForKey(@"ck_data_source_state", oldState.description);
CKExceptionInfoSetValueForKey(
@"assert_message",
([NSString stringWithFormat:@"<force_category:%@:force_category> Raised an unknown c++ exception applying modification",
oldState.contentsFingerprint])
);
throw;
}
}
- (CKDataSourceChange *)__changeFromState:(CKDataSourceState *)oldState
{
CKDataSourceConfiguration *configuration = [oldState configuration];
id<NSObject> context = [configuration context];
const CKSizeRange sizeRange = [configuration sizeRange];
NSMutableArray<CKComponentController *> *addedComponentControllers = [NSMutableArray array];
NSMutableArray<CKComponentController *> *invalidComponentControllers = [NSMutableArray array];
const auto newSections = [NSMutableArray<NSMutableArray *> array];
[[oldState sections] enumerateObjectsUsingBlock:^(NSArray *items, NSUInteger sectionIdx, BOOL *sectionStop) {
[newSections addObject:[items mutableCopy]];
}];
// Update items
NSDictionary<NSIndexPath *, id> *const updatedItems = [_changeset updatedItems];
void(^processUpdatedItem)(NSIndexPath *indexPath, id model) = ^(NSIndexPath *indexPath, id model) {
if (indexPath.section >= newSections.count) {
CKCFatalWithCategory(CKHumanReadableInvalidChangesetOperationType(CKInvalidChangesetOperationTypeUpdate),
@"Invalid section: %lu (>= %lu). Changeset: %@, user info: %@, state: %@",
(unsigned long)indexPath.section,
(unsigned long)newSections.count,
_changeset,
_userInfo,
oldState);
}
NSMutableArray *const section = newSections[indexPath.section];
if (indexPath.item >= section.count) {
CKCFatalWithCategory(CKHumanReadableInvalidChangesetOperationType(CKInvalidChangesetOperationTypeUpdate),
@"Invalid item: %lu (>= %lu). Changeset: %@, user info: %@, state: %@",
(unsigned long)indexPath.item,
(unsigned long)section.count,
_changeset,
_userInfo,
oldState);
}
CKDataSourceItem *const oldItem = section[indexPath.item];
CKDataSourceItem *const item = [self _buildDataSourceItemForPreviousRoot:[oldItem scopeRoot]
stateUpdates:{}
sizeRange:sizeRange
configuration:configuration
model:model
context:context
layoutCache:_treeLayoutCache ? _treeLayoutCache->find([[oldItem scopeRoot] globalIdentifier]) : nullptr
itemType:CKDataSourceChangesetModificationItemTypeUpdate];
[section replaceObjectAtIndex:indexPath.item withObject:item];
for (auto componentController : addedControllersFromPreviousScopeRootMatchingPredicate(item.scopeRoot,
oldItem.scopeRoot,
&CKComponentControllerInitializeEventPredicate)) {
[addedComponentControllers addObject:componentController];
}
for (auto componentController : removedControllersFromPreviousScopeRootMatchingPredicate(item.scopeRoot,
oldItem.scopeRoot,
&CKComponentControllerInvalidateEventPredicate)) {
[invalidComponentControllers addObject:componentController];
}
};
[updatedItems enumerateKeysAndObjectsUsingBlock:^(NSIndexPath *indexPath, id model, BOOL *stop) {
processUpdatedItem(indexPath, model);
}];
__block std::unordered_map<NSUInteger, std::map<NSUInteger, CKDataSourceItem *>> insertedItemsBySection;
__block std::unordered_map<NSUInteger, NSMutableIndexSet *> removedItemsBySection;
void (^addRemovedIndexPath)(NSIndexPath *) = ^(NSIndexPath *ip){
const auto &element = removedItemsBySection.find(ip.section);
if (element == removedItemsBySection.end()) {
removedItemsBySection.insert({ip.section, [NSMutableIndexSet indexSetWithIndex:ip.item]});
} else {
[element->second addIndex:ip.item];
}
};
// Moves: first record as inserts for later processing
[[_changeset movedItems] enumerateKeysAndObjectsUsingBlock:^(NSIndexPath *from, NSIndexPath *to, BOOL *stop) {
if (from.section >= newSections.count) {
CKCFatalWithCategory(CKHumanReadableInvalidChangesetOperationType(CKInvalidChangesetOperationTypeMoveRow),
@"Invalid section: %lu (>= %lu) while processing moved items. Changeset: %@, user info: %@, state: %@",
(unsigned long)from.section,
(unsigned long)newSections.count,
CK::changesetDescription(_changeset),
_userInfo,
oldState);
}
const auto fromSection = static_cast<NSArray *>(newSections[from.section]);
if (from.item >= fromSection.count) {
CKCFatalWithCategory(CKHumanReadableInvalidChangesetOperationType(CKInvalidChangesetOperationTypeMoveRow),
@"Invalid item: %lu (>= %lu) while processing moved items. Changeset: %@, user info: %@, state: %@",
(unsigned long)from.item,
(unsigned long)fromSection.count,
CK::changesetDescription(_changeset),
_userInfo,
oldState);
}
insertedItemsBySection[to.section][to.row] = fromSection[from.item];
}];
// Moves: then record as removals
[[_changeset movedItems] enumerateKeysAndObjectsUsingBlock:^(NSIndexPath *from, NSIndexPath *to, BOOL *stop) {
addRemovedIndexPath(from);
}];
// Remove items
for (NSIndexPath *removedItem in [_changeset removedItems]) {
addRemovedIndexPath(removedItem);
}
for (const auto &it : removedItemsBySection) {
NSMutableArray *sectionItems = nil;
@try {
sectionItems = newSections[it.first];
} @catch (NSException *exception) {
CKExceptionInfoSetValueForKey(@"ck_changeset_operation", CKHumanReadableInvalidChangesetOperationType(CKInvalidChangesetOperationTypeRemoveRow));
[exception raise];
}
@try {
[sectionItems removeObjectsAtIndexes:it.second];
} @catch (NSException *exception) {
CKExceptionInfoSetValueForKey(@"ck_changeset_operation", CKHumanReadableInvalidChangesetOperationType(CKInvalidChangesetOperationTypeRemoveRow));
CKExceptionInfoSetValueForKey(@"ck_invalid_indexes", CK::indexSetDescription(CK::invalidIndexesForRemovalFromArray(sectionItems, it.second), @"", 0));
CKExceptionInfoSetValueForKey(@"ck_section", ([NSString stringWithFormat:@"%lu", (unsigned long)it.first]));
[exception raise];
}
}
// Remove sections
NSIndexSet *const removedSections = [_changeset removedSections];
if ([removedSections count] > 0) {
[newSections removeObjectsAtIndexes:removedSections];
}
// Insert sections
@try {
[newSections insertObjects:emptyMutableArrays([[_changeset insertedSections] count]) atIndexes:[_changeset insertedSections]];
} @catch (NSException *exception) {
CKExceptionInfoSetValueForKey(@"ck_changeset_operation", CKHumanReadableInvalidChangesetOperationType(CKInvalidChangesetOperationTypeInsertSection));
CKExceptionInfoSetValueForKey(@"ck_invalid_indexes", CK::indexSetDescription(CK::invalidIndexesForInsertionInArray(newSections, [_changeset insertedSections]), @"", 0));
[exception raise];
}
// Insert items
const auto buildItem = ^CKDataSourceItem *(id model) {
const auto scopeRoot = CKComponentScopeRootWithPredicates(_stateListener,
configuration.analyticsListener,
configuration.componentPredicates,
configuration.componentControllerPredicates);
return [self _buildDataSourceItemForPreviousRoot:scopeRoot
stateUpdates:{}
sizeRange:sizeRange
configuration:configuration
model:model
context:context
layoutCache:_treeLayoutCache ? _treeLayoutCache->find([scopeRoot globalIdentifier]) : nullptr
itemType:CKDataSourceChangesetModificationItemTypeInsert];
};
NSDictionary<NSIndexPath *, id> *const insertedItems = [_changeset insertedItems];
[insertedItems enumerateKeysAndObjectsUsingBlock:^(NSIndexPath *indexPath, id model, BOOL *stop) {
insertedItemsBySection[indexPath.section][indexPath.item] = buildItem(model);
}];
for (const auto §ionIt : insertedItemsBySection) {
NSMutableIndexSet *indexes = [NSMutableIndexSet indexSet];
NSMutableArray *items = [NSMutableArray array];
// Note this enumeration is ordered by virtue of std::map, which is crucial (we need items to match indexes):
for (const auto &itemIt : sectionIt.second) {
[indexes addIndex:itemIt.first];
[items addObject:itemIt.second];
}
NSMutableArray *sectionItems = nil;
@try {
sectionItems = [newSections objectAtIndex:sectionIt.first];
} @catch (NSException *exception) {
CKExceptionInfoSetValueForKey(@"ck_changeset_operation", CKHumanReadableInvalidChangesetOperationType(CKInvalidChangesetOperationTypeInsertRow));
[exception raise];
}
@try {
[sectionItems insertObjects:items atIndexes:indexes];
} @catch (NSException *exception) {
CKExceptionInfoSetValueForKey(@"ck_changeset_operation", CKHumanReadableInvalidChangesetOperationType(CKInvalidChangesetOperationTypeInsertRow));
CKExceptionInfoSetValueForKey(@"ck_invalid_indexes", CK::indexSetDescription(CK::invalidIndexesForInsertionInArray(sectionItems, indexes), @"", 0));
CKExceptionInfoSetValueForKey(@"ck_section", ([NSString stringWithFormat:@"%lu", (unsigned long)sectionIt.first]));
[exception raise];
}
}
CKDataSourceState *newState =
[[CKDataSourceState alloc] initWithConfiguration:configuration
sections:newSections];
CKDataSourceAppliedChanges *appliedChanges =
[[CKDataSourceAppliedChanges alloc] initWithUpdatedIndexPaths:[NSSet setWithArray:[updatedItems allKeys]]
removedIndexPaths:[_changeset removedItems]
removedSections:[_changeset removedSections]
movedIndexPaths:[_changeset movedItems]
insertedSections:[_changeset insertedSections]
insertedIndexPaths:[NSSet setWithArray:[insertedItems allKeys]]
userInfo:_userInfo];
return [[CKDataSourceChange alloc] initWithState:newState
previousState:oldState
appliedChanges:appliedChanges
appliedChangeset:_changeset
deferredChangeset:nil
addedComponentControllers:addedComponentControllers
invalidComponentControllers:invalidComponentControllers];
}
- (CKDataSourceItem *)_buildDataSourceItemForPreviousRoot:(CK::NonNull<CKComponentScopeRoot *>)previousRoot
stateUpdates:(const CKComponentStateUpdateMap &)stateUpdates
sizeRange:(const CKSizeRange &)sizeRange
configuration:(CKDataSourceConfiguration *)configuration
model:(id)model
context:(id)context
layoutCache:(std::shared_ptr<RCLayoutCache>)layoutCache
itemType:(CKDataSourceChangesetModificationItemType)itemType
{
if (_itemGenerator) {
return [_itemGenerator buildDataSourceItemForPreviousRoot:previousRoot
stateUpdates:stateUpdates
sizeRange:sizeRange
configuration:configuration
model:model
context:context
itemType:itemType];
} else {
return CKBuildDataSourceItem(previousRoot, stateUpdates, sizeRange, configuration, model, context, layoutCache);
}
}
- (NSDictionary *)userInfo
{
return _userInfo;
}
- (NSString *)description
{
return [_changeset description];
}
static NSArray *emptyMutableArrays(NSUInteger count)
{
NSMutableArray *arrays = [NSMutableArray array];
for (NSUInteger i = 0; i < count; i++) {
[arrays addObject:[NSMutableArray array]];
}
return arrays;
}
- (CKDataSourceQOS)qos
{
return _qos;
}
@end
namespace CK {
auto invalidIndexesForInsertionInArray(NSArray *const a, NSIndexSet *const is) -> NSIndexSet *
{
auto r = [NSMutableIndexSet new];
__block auto arrayCount = a.count;
[is enumerateIndexesUsingBlock:^(NSUInteger idx, BOOL * _Nonnull) {
if (idx > arrayCount) {
[r addIndex:idx];
}
arrayCount++;
}];
return r;
}
auto invalidIndexesForRemovalFromArray(NSArray *const a, NSIndexSet *const is) -> NSIndexSet *
{
auto r = [NSMutableIndexSet new];
[is enumerateIndexesUsingBlock:^(NSUInteger idx, BOOL * _Nonnull) {
if (idx >= a.count) {
[r addIndex:idx];
}
}];
return r;
}
}