ComponentKit/TransactionalDataSources/Common/Internal/CKDataSourceSplitChangesetModification.mm (564 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 "CKDataSourceSplitChangesetModification.h" #import <map> #import <mutex> #import "CKDataSourceConfigurationInternal.h" #import "CKDataSourceStateInternal.h" #import "CKDataSourceChange.h" #import "CKDataSourceChangesetInternal.h" #import "CKDataSourceChangesetModification.h" #import "CKDataSourceItemInternal.h" #import "CKDataSourceAppliedChanges.h" #import "CKBuildComponent.h" #import "CKComponentControllerEvents.h" #import "CKComponentEvents.h" #import "CKComponentControllerHelper.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 CKDataSourceSplitChangesetModification { __weak id<CKComponentStateListener> _stateListener; NSDictionary *_userInfo; CKDataSourceViewport _viewport; CKDataSourceQOS _qos; std::shared_ptr<CKTreeLayoutCache> _treeLayoutCache; } - (instancetype)initWithChangeset:(CKDataSourceChangeset *)changeset stateListener:(id<CKComponentStateListener>)stateListener userInfo:(NSDictionary *)userInfo viewport:(CKDataSourceViewport)viewport qos:(CKDataSourceQOS)qos { return [self initWithChangeset:changeset stateListener:stateListener userInfo:userInfo viewport:viewport qos:qos treeLayoutCache:nullptr]; } - (instancetype)initWithChangeset:(CKDataSourceChangeset *)changeset stateListener:(id<CKComponentStateListener>)stateListener userInfo:(NSDictionary *)userInfo viewport:(CKDataSourceViewport)viewport qos:(CKDataSourceQOS)qos treeLayoutCache:(std::shared_ptr<CKTreeLayoutCache>)treeLayoutCache { if (self = [super init]) { _changeset = changeset; _stateListener = stateListener; _userInfo = [userInfo copy]; _viewport = viewport; _qos = qos; _treeLayoutCache = std::move(treeLayoutCache); } return self; } - (CKDataSourceChange *)changeFromState:(CKDataSourceState *)oldState { CKDataSourceConfiguration *configuration = [oldState configuration]; id<NSObject> context = [configuration context]; const CKSizeRange sizeRange = [configuration sizeRange]; const auto splitChangesetOptions = [configuration options].splitChangesetOptions; const BOOL enableChangesetSplitting = splitChangesetOptions.enabled; const CGSize viewportSize = (_viewport.size.width == 0.0 || _viewport.size.height == 0.0) ? splitChangesetOptions.viewportBoundingSize : _viewport.size; NSMutableArray<CKComponentController *> *addedComponentControllers = [NSMutableArray array]; NSMutableArray<CKComponentController *> *invalidComponentControllers = [NSMutableArray array]; NSMutableArray *newSections = [NSMutableArray array]; [[oldState sections] enumerateObjectsUsingBlock:^(NSArray *items, NSUInteger sectionIdx, BOOL *sectionStop) { [newSections addObject:[items mutableCopy]]; }]; // Update items NSDictionary<NSIndexPath *, id> *const updatedItems = [_changeset updatedItems]; NSDictionary<NSIndexPath *, id> *initialUpdatedItems = nil; NSDictionary<NSIndexPath *, id> *deferredUpdatedItems = nil; if (enableChangesetSplitting && splitChangesetOptions.splitUpdates) { const CKDataSourceSplitUpdateResult result = splitUpdatedItems(newSections, updatedItems, addedComponentControllers, invalidComponentControllers, sizeRange, configuration, context, _changeset, _userInfo, oldState, viewportSize, splitChangesetOptions.layoutAxis, _viewport.contentOffset); initialUpdatedItems = result.splitItems.initialChangesetItems; deferredUpdatedItems = result.splitItems.deferredChangesetItems; [result.computedItems enumerateKeysAndObjectsUsingBlock:^(NSIndexPath *indexPath, CKDataSourceItem *item, BOOL *stop) { NSMutableArray *const section = newSections[indexPath.section]; [section replaceObjectAtIndex:indexPath.item withObject:item]; }]; } else { initialUpdatedItems = updatedItems; [updatedItems enumerateKeysAndObjectsUsingBlock:^(NSIndexPath *indexPath, id model, BOOL *stop) { 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]; const auto layoutCache = _treeLayoutCache ? _treeLayoutCache->find([oldItem.scopeRoot globalIdentifier]) : nullptr; CKDataSourceItem *const item = CKBuildDataSourceItem([oldItem scopeRoot], {}, sizeRange, configuration, model, context, layoutCache); [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]; } }]; } __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); } // Used to keep track of the items in each section that have been marked for a deferred update, // once removals are processed we use this to compute the final set of deferred updates using the correct indices. NSMutableArray<NSMutableArray<id> *> *sectionsForDeferredUpdatedItems = nil; if (deferredUpdatedItems.count != 0) { sectionsForDeferredUpdatedItems = [NSMutableArray<NSMutableArray<id> *> arrayWithCapacity:newSections.count]; [sectionsForDeferredUpdatedItems addObjectsFromArray:emptyMutableArrays(newSections.count)]; [newSections enumerateObjectsUsingBlock:^(NSArray<CKDataSourceItem *> *items, NSUInteger idx, BOOL *stop) { [sectionsForDeferredUpdatedItems[idx] addObjectsFromArray:nullPlaceholderArray(items.count)]; }]; [deferredUpdatedItems enumerateKeysAndObjectsUsingBlock:^(NSIndexPath *indexPath, id obj, BOOL *stop) { sectionsForDeferredUpdatedItems[indexPath.section][indexPath.item] = obj; }]; } for (const auto &it : removedItemsBySection) { if (it.first >= newSections.count) { CKCFatalWithCategory(CKHumanReadableInvalidChangesetOperationType(CKInvalidChangesetOperationTypeRemoveRow), @"Invalid section: %lu (>= %lu) while processing moved items. Changeset: %@, user info: %@, state: %@", (unsigned long)it.first, (unsigned long)newSections.count, CK::changesetDescription(_changeset), _userInfo, oldState); } const auto section = static_cast<NSMutableArray *>(newSections[it.first]); #if CK_ASSERTIONS_ENABLED const auto invalidIndexes = CK::invalidIndexesForRemovalFromArray(section, it.second); if (invalidIndexes.count > 0) { CKCFatalWithCategory(CKHumanReadableInvalidChangesetOperationType(CKInvalidChangesetOperationTypeRemoveRow), @"%@ (>= %lu) in section: %lu. Changeset: %@, user info: %@, state: %@", CK::indexSetDescription(invalidIndexes, @"Invalid indexes", 0), (unsigned long)section.count, (unsigned long)it.first, CK::changesetDescription(_changeset), _userInfo, oldState); } #endif [section removeObjectsAtIndexes:it.second]; [sectionsForDeferredUpdatedItems[it.first] removeObjectsAtIndexes:it.second]; } // Remove sections NSIndexSet *const removedSections = [_changeset removedSections]; if ([removedSections count] > 0) { [newSections removeObjectsAtIndexes:removedSections]; [sectionsForDeferredUpdatedItems removeObjectsAtIndexes:removedSections]; } // Insert sections // Quick validation to make sure the locations specified by indexes do not exceed the bounds of the receiving array. if ([[_changeset insertedSections] count] > 0 && ([[_changeset insertedSections] firstIndex] > newSections.count)) { CKCFatalWithCategory(CKHumanReadableInvalidChangesetOperationType(CKInvalidChangesetOperationTypeInsertSection), @"Invalid first index location: %lu (> %lu) while processing inserted sections. Changeset: %@, user info: %@, state: %@", (unsigned long)[[_changeset insertedSections] firstIndex], (unsigned long)newSections.count, CK::changesetDescription(_changeset), _userInfo, oldState); } #if CK_ASSERTIONS_ENABLED // Deep validation of the indexes we are going to insert for better logging. auto const invalidInsertedSectionsIndexes = CK::invalidIndexesForInsertionInArray(newSections, [_changeset insertedSections]); if (invalidInsertedSectionsIndexes.count) { CKCFatalWithCategory(CKHumanReadableInvalidChangesetOperationType(CKInvalidChangesetOperationTypeInsertSection), @"%@ for range: %@ in sections: %@. Changeset: %@, user info: %@, state: %@", CK::indexSetDescription(invalidInsertedSectionsIndexes, @"Invalid indexes", 0), NSStringFromRange({0, newSections.count}), newSections, CK::changesetDescription(_changeset), _userInfo, oldState); } #endif [newSections insertObjects:emptyMutableArrays([[_changeset insertedSections] count]) atIndexes:[_changeset insertedSections]]; if (sectionsForDeferredUpdatedItems != nil) { [sectionsForDeferredUpdatedItems insertObjects:emptyMutableArrays([[_changeset insertedSections] count]) atIndexes:[_changeset insertedSections]]; } // Insert items const auto buildItem = ^CKDataSourceItem *(id model) { return CKBuildDataSourceItem(CKComponentScopeRootWithPredicates(_stateListener, configuration.analyticsListener, configuration.componentPredicates, configuration.componentControllerPredicates), {}, sizeRange, configuration, model, context); }; NSDictionary<NSIndexPath *, id> *const insertedItems = [_changeset insertedItems]; NSDictionary<NSIndexPath *, id> *initialInsertedItems = nil; NSDictionary<NSIndexPath *, id> *deferredInsertedItems = nil; if (enableChangesetSplitting) { // Compute the height of the existing content (after updates and removals) -- if changeset splitting is // enabled and the content is already overflowing the viewport, we won't split the changeset. __block CGSize contentSize = computeTotalHeightOfSections(newSections); if (!contentSizeOverflowsViewportAtTail(contentSize, _viewport.contentOffset, viewportSize, splitChangesetOptions.layoutAxis)) { NSArray<NSIndexPath *> *const sortedIndexPaths = [[insertedItems allKeys] sortedArrayUsingSelector:@selector(compare:)]; if (indexPathsAreContiguousAtTail(sortedIndexPaths, newSections)) { __block NSUInteger endIndex = sortedIndexPaths.count; [sortedIndexPaths enumerateObjectsUsingBlock:^(NSIndexPath *indexPath, NSUInteger idx, BOOL *stop) { CKDataSourceItem *const item = buildItem(insertedItems[indexPath]); insertedItemsBySection[indexPath.section][indexPath.item] = item; contentSize = addSizeToSize(contentSize, item.rootLayout.size()); if (contentSizeOverflowsViewportAtTail(contentSize, _viewport.contentOffset, viewportSize, splitChangesetOptions.layoutAxis)) { *stop = YES; endIndex = idx + 1; } }]; const CKDataSourceSplitChangesetItems splitChangesetItems = splitItemsAtIndex(endIndex, sortedIndexPaths, insertedItems); initialInsertedItems = splitChangesetItems.initialChangesetItems; deferredInsertedItems = splitChangesetItems.deferredChangesetItems; } } } if (initialInsertedItems == nil) { [insertedItems enumerateKeysAndObjectsUsingBlock:^(NSIndexPath *indexPath, id model, BOOL *stop) { insertedItemsBySection[indexPath.section][indexPath.item] = buildItem(model); }]; initialInsertedItems = insertedItems; } for (const auto &sectionIt : 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]; } if (sectionIt.first >= newSections.count) { CKCFatalWithCategory(CKHumanReadableInvalidChangesetOperationType(CKInvalidChangesetOperationTypeInsertRow), @"Invalid section: %lu (>= %lu) while processing inserted items. Changeset: %@, user info: %@, state: %@", (unsigned long)sectionIt.first, (unsigned long)newSections.count, CK::changesetDescription(_changeset), _userInfo, oldState); } #if CK_ASSERTIONS_ENABLED const auto sectionItems = static_cast<NSArray *>([newSections objectAtIndex:sectionIt.first]); const auto invalidIndexes = CK::invalidIndexesForInsertionInArray(sectionItems, indexes); if (invalidIndexes.count > 0) { CKCFatalWithCategory(CKHumanReadableInvalidChangesetOperationType(CKInvalidChangesetOperationTypeInsertRow), @"%@ for range: %@ in section: %lu. Changeset: %@, user info: %@, state: %@", CK::indexSetDescription(invalidIndexes, @"Invalid indexes", 0), NSStringFromRange({0, sectionItems.count}), (unsigned long)sectionIt.first, CK::changesetDescription(_changeset), _userInfo, oldState); } #endif [[newSections objectAtIndex:sectionIt.first] insertObjects:items atIndexes:indexes]; [[sectionsForDeferredUpdatedItems objectAtIndex:sectionIt.first] insertObjects:nullPlaceholderArray(indexes.count) atIndexes:indexes]; } CKDataSourceState *newState = [[CKDataSourceState alloc] initWithConfiguration:configuration sections:newSections]; CKDataSourceAppliedChanges *appliedChanges = [[CKDataSourceAppliedChanges alloc] initWithUpdatedIndexPaths:[NSSet setWithArray:[initialUpdatedItems allKeys]] removedIndexPaths:[_changeset removedItems] removedSections:[_changeset removedSections] movedIndexPaths:[_changeset movedItems] insertedSections:[_changeset insertedSections] insertedIndexPaths:[NSSet setWithArray:[initialInsertedItems allKeys]] userInfo:_userInfo]; CKDataSourceChangeset *appliedChangeset = [[[[[[[[CKDataSourceChangesetBuilder dataSourceChangesetWithOriginName:@"data_source_split_changeset_modification"] withUpdatedItems:initialUpdatedItems] withRemovedItems:[_changeset removedItems]] withRemovedSections:[_changeset removedSections]] withMovedItems:[_changeset movedItems]] withInsertedSections:[_changeset insertedSections]] withInsertedItems:initialInsertedItems] build]; return [[CKDataSourceChange alloc] initWithState:newState previousState:oldState appliedChanges:appliedChanges appliedChangeset:appliedChangeset deferredChangeset:createDeferredChangeset(deferredInsertedItems, computeDeferredItems(sectionsForDeferredUpdatedItems)) addedComponentControllers:addedComponentControllers invalidComponentControllers:invalidComponentControllers]; } - (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; } static NSArray<NSNull *> *nullPlaceholderArray(NSUInteger count) { NSMutableArray<NSNull *> *const array = [NSMutableArray<NSNull *> arrayWithCapacity:count]; for (NSUInteger i = 0; i < count; i++) { [array addObject:[NSNull null]]; } return array; } static CGSize computeTotalHeightOfSections(NSArray<NSArray<CKDataSourceItem *> *> *sections) { CGSize contentSize = CGSizeZero; for (NSArray<CKDataSourceItem *> *items in sections) { for (CKDataSourceItem *item in items) { contentSize = addSizeToSize(contentSize, item.rootLayout.size()); } } return contentSize; } struct CKDataSourceSplitChangesetItems { NSDictionary<NSIndexPath *, id> *initialChangesetItems; NSDictionary<NSIndexPath *, id> *deferredChangesetItems; }; static CKDataSourceSplitChangesetItems splitItemsAtIndex(NSUInteger splitIndex, NSArray<NSIndexPath *> *indexPaths, NSDictionary<NSIndexPath *, id> *allItems) { NSDictionary<NSIndexPath *, id> *initialChangesetItems = nil; NSDictionary<NSIndexPath *, id> *deferredChangesetItems = nil; if (splitIndex < indexPaths.count) { initialChangesetItems = dictionaryWithValuesForKeys(allItems, [indexPaths subarrayWithRange:NSMakeRange(0, splitIndex)]); deferredChangesetItems = dictionaryWithValuesForKeys(allItems, [indexPaths subarrayWithRange:NSMakeRange(splitIndex, indexPaths.count - splitIndex)]); } else { initialChangesetItems = allItems; } return { .initialChangesetItems = initialChangesetItems, .deferredChangesetItems = deferredChangesetItems, }; } struct CKDataSourceSplitUpdateResult { CKDataSourceSplitChangesetItems splitItems; NSDictionary<NSIndexPath *, CKDataSourceItem *> *computedItems; }; static CKDataSourceSplitUpdateResult splitUpdatedItems(NSArray<NSArray<CKDataSourceItem *> *> *sections, NSDictionary<NSIndexPath *, id> *updatedItems, NSMutableArray<CKComponentController *> *addedComponentControllers, NSMutableArray<CKComponentController *> *invalidComponentControllers, const CKSizeRange &sizeRange, CKDataSourceConfiguration *configuration, id<NSObject> context, CKDataSourceChangeset *changeset, NSDictionary *userInfo, CKDataSourceState *oldState, CGSize viewportSize, CKDataSourceLayoutAxis layoutAxis, CGPoint contentOffset) { if (updatedItems.count == 0) { return {}; } NSMutableDictionary<NSIndexPath *, CKDataSourceItem *> *const computedItems = [NSMutableDictionary<NSIndexPath *, CKDataSourceItem *> dictionary]; NSMutableDictionary<NSIndexPath *, id> *mutableUpdatedItems = [updatedItems mutableCopy]; NSMutableDictionary<NSIndexPath *, id> *initialUpdatedItems = [NSMutableDictionary<NSIndexPath *, id> dictionary]; NSMutableDictionary<NSIndexPath *, id> *deferredUpdatedItems = [NSMutableDictionary<NSIndexPath *, id> dictionary]; __block CGSize contentSize = CGSizeZero; [sections enumerateObjectsUsingBlock:^(NSArray<CKDataSourceItem *> *items, NSUInteger sectionIdx, BOOL *stop) { [items enumerateObjectsUsingBlock:^(CKDataSourceItem *item, NSUInteger itemIdx, BOOL *stop1) { NSIndexPath *const indexPath = [NSIndexPath indexPathForItem:itemIdx inSection:sectionIdx]; id const updatedModel = mutableUpdatedItems[indexPath]; [mutableUpdatedItems removeObjectForKey:indexPath]; if (updatedModel == nil) { contentSize = addSizeToSize(contentSize, [item rootLayout].size()); } else if (contentSizeOverflowsViewport(contentSize, contentOffset, viewportSize, layoutAxis)) { // If the item was already out of the viewport, we assume that it will still be out // of the viewport once the item is updated. This assumption may not hold true // if the update *reduces* the size of the item enough such that it now is inside // the viewport. In this scenario, we under-render and there is a potential performance // regression. deferredUpdatedItems[indexPath] = updatedModel; contentSize = addSizeToSize(contentSize, [item rootLayout].size()); } else { // If the item was in the viewport before the update, we assume that the item will still // be in the viewport after the update. This assumption may not hold true if the update // *increases* the size of the item such that it is now outside the viewport. In this // scenario, we over-render, which is not a problem since that is not a regression over // the original behavior. CKDataSourceItem *const newItem = CKBuildDataSourceItem([item scopeRoot], {}, sizeRange, configuration, updatedModel, context); computedItems[indexPath] = newItem; initialUpdatedItems[indexPath] = updatedModel; contentSize = addSizeToSize(contentSize, [newItem rootLayout].size()); for (auto componentController : addedControllersFromPreviousScopeRootMatchingPredicate(newItem.scopeRoot, item.scopeRoot, &CKComponentControllerInitializeEventPredicate)) { [addedComponentControllers addObject:componentController]; } for (auto componentController : removedControllersFromPreviousScopeRootMatchingPredicate(newItem.scopeRoot, item.scopeRoot, &CKComponentControllerInvalidateEventPredicate)) { [invalidComponentControllers addObject:componentController]; } } }]; }]; // Anything that is left has an invalid index path. [mutableUpdatedItems enumerateKeysAndObjectsUsingBlock:^(NSIndexPath *indexPath, id model, BOOL *stop) { if (indexPath.section >= sections.count) { CKCFatalWithCategory(CKHumanReadableInvalidChangesetOperationType(CKInvalidChangesetOperationTypeUpdate), @"Invalid section: %lu (>= %lu). Changeset: %@, user info: %@, state: %@", (unsigned long)indexPath.section, (unsigned long)sections.count, changeset, userInfo, oldState); } NSArray<CKDataSourceItem *> *const section = sections[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); } }]; return { .splitItems = { .initialChangesetItems = initialUpdatedItems, .deferredChangesetItems = deferredUpdatedItems, }, .computedItems = computedItems, }; } static NSDictionary *dictionaryWithValuesForKeys(NSDictionary *dictionary, NSArray<id<NSCopying>> *keys) { NSMutableDictionary *const subdictionary = [NSMutableDictionary dictionaryWithCapacity:keys.count]; for (id<NSCopying> key in keys) { subdictionary[key] = dictionary[key]; } return subdictionary; } static NSDictionary<NSIndexPath *, id> *computeDeferredItems(NSArray<NSArray<id> *> *sectionsForDeferredItems) { NSMutableDictionary<NSIndexPath *, id> *const deferredItems = [NSMutableDictionary<NSIndexPath *, id> new]; [sectionsForDeferredItems enumerateObjectsUsingBlock:^(NSArray<id> *items, NSUInteger sectionIdx, BOOL *stop) { [items enumerateObjectsUsingBlock:^(id obj, NSUInteger itemIdx, BOOL *stop1) { if (obj != [NSNull null]) { deferredItems[[NSIndexPath indexPathForItem:itemIdx inSection:sectionIdx]] = obj; } }]; }]; return deferredItems; } static CKDataSourceChangeset *createDeferredChangeset(NSDictionary<NSIndexPath *, id> *insertedItems, NSDictionary<NSIndexPath *, id> *updatedItems) { if (insertedItems.count == 0 && updatedItems.count == 0) { return nil; } return [[[[CKDataSourceChangesetBuilder dataSourceChangesetWithOriginName:@"data_source_split_changeset_modification_deffered"] withUpdatedItems:updatedItems] withInsertedItems:insertedItems] build]; } static BOOL contentSizeOverflowsViewport(CGSize contentSize, CGPoint contentOffset, CGSize viewportSize, CKDataSourceLayoutAxis layoutAxis) { switch (layoutAxis) { case CKDataSourceLayoutAxisVertical: return (contentSize.height < contentOffset.y) || (contentSize.height >= (contentOffset.y + viewportSize.height)); case CKDataSourceLayoutAxisHorizontal: return (contentSize.width < contentOffset.x) || (contentSize.width >= (contentOffset.x + viewportSize.width)); } } static BOOL contentSizeOverflowsViewportAtTail(CGSize contentSize, CGPoint contentOffset, CGSize viewportSize, CKDataSourceLayoutAxis layoutAxis) { switch (layoutAxis) { case CKDataSourceLayoutAxisVertical: return contentSize.height >= (contentOffset.y + viewportSize.height); case CKDataSourceLayoutAxisHorizontal: return contentSize.width >= (contentOffset.x + viewportSize.width); } } static BOOL indexPathsAreContiguousAtTail(NSArray<NSIndexPath *> *indexPaths, NSArray<NSArray<CKDataSourceItem *> *> *sections) { __block BOOL isContiguousAtTail = YES; [sections enumerateObjectsUsingBlock:^(NSArray<CKDataSourceItem *> *section, NSUInteger idx, BOOL *stop) { NSArray<NSIndexPath *> *const filteredBySection = [indexPaths filteredArrayUsingPredicate: [NSPredicate predicateWithBlock:^BOOL(NSIndexPath *indexPath, NSDictionary *bindings) { return [indexPath section] == idx; }]]; if (!indexPathsAreContiguousAtTailForSection(filteredBySection, section, idx)) { isContiguousAtTail = NO; *stop = YES; } }]; return isContiguousAtTail; } static BOOL indexPathsAreContiguousAtTailForSection(NSArray<NSIndexPath *> *indexPaths, NSArray<CKDataSourceItem *> *section, NSUInteger sectionIndex) { NSUInteger expectedItemIndex = section.count; for (NSIndexPath *indexPath in indexPaths) { if ([indexPath section] != sectionIndex || [indexPath item] != expectedItemIndex) { return NO; } expectedItemIndex++; } return YES; } static CGSize addSizeToSize(CGSize existingSize, CGSize additionalSize) { existingSize.width += additionalSize.width; existingSize.height += additionalSize.height; return existingSize; } - (CKDataSourceQOS)qos { return _qos; } @end