// // ASDataController.mm // Texture // // Copyright (c) Facebook, Inc. and its affiliates. All rights reserved. // Changes after 4/13/2017 are: Copyright (c) Pinterest, Inc. All rights reserved. // Licensed under Apache 2.0: http://www.apache.org/licenses/LICENSE-2.0 // #ifndef MINIMAL_ASDK #import #include #import #import #import #import #import #import #import #import #import #import #import #import #import #import #import #import #import #import #import #import "Private/ASInternalHelpers.h" #import #import #import //#define LOG(...) NSLog(__VA_ARGS__) #define LOG(...) #define ASSERT_ON_EDITING_QUEUE ASDisplayNodeAssertNotNil(dispatch_get_specific(&kASDataControllerEditingQueueKey), @"%@ must be called on the editing transaction queue.", NSStringFromSelector(_cmd)) const static char * kASDataControllerEditingQueueKey = "kASDataControllerEditingQueueKey"; const static char * kASDataControllerEditingQueueContext = "kASDataControllerEditingQueueContext"; NSString * const ASDataControllerRowNodeKind = @"_ASDataControllerRowNodeKind"; NSString * const ASCollectionInvalidUpdateException = @"ASCollectionInvalidUpdateException"; typedef dispatch_block_t ASDataControllerCompletionBlock; typedef void (^ASDataControllerSynchronizationBlock)(); @interface ASDataController () { id _layoutDelegate; NSInteger _nextSectionID; BOOL _itemCountsFromDataSourceAreValid; // Main thread only. std::vector _itemCountsFromDataSource; // Main thread only. ASMainSerialQueue *_mainSerialQueue; dispatch_queue_t _editingTransactionQueue; // Serial background queue. Dispatches concurrent layout and manages _editingNodes. dispatch_group_t _editingTransactionGroup; // Group of all edit transaction blocks. Useful for waiting. std::atomic _editingTransactionGroupCount; BOOL _initialReloadDataHasBeenCalled; BOOL _synchronized; NSMutableSet *_onDidFinishSynchronizingBlocks; struct { unsigned int supplementaryNodeKindsInSections:1; unsigned int supplementaryNodesOfKindInSection:1; unsigned int supplementaryNodeBlockOfKindAtIndexPath:1; unsigned int constrainedSizeForNodeAtIndexPath:1; unsigned int constrainedSizeForSupplementaryNodeOfKindAtIndexPath:1; unsigned int contextForSection:1; } _dataSourceFlags; } @property (copy) ASElementMap *pendingMap; @property (copy) ASElementMap *visibleMap; @end @implementation ASDataController #pragma mark - Lifecycle - (instancetype)initWithDataSource:(id)dataSource node:(nullable id)node eventLog:(ASEventLog *)eventLog { if (!(self = [super init])) { return nil; } _node = node; _dataSource = dataSource; _dataSourceFlags.supplementaryNodeKindsInSections = [_dataSource respondsToSelector:@selector(dataController:supplementaryNodeKindsInSections:)]; _dataSourceFlags.supplementaryNodesOfKindInSection = [_dataSource respondsToSelector:@selector(dataController:supplementaryNodesOfKind:inSection:)]; _dataSourceFlags.supplementaryNodeBlockOfKindAtIndexPath = [_dataSource respondsToSelector:@selector(dataController:supplementaryNodeBlockOfKind:atIndexPath:shouldAsyncLayout:)]; _dataSourceFlags.constrainedSizeForNodeAtIndexPath = [_dataSource respondsToSelector:@selector(dataController:constrainedSizeForNodeAtIndexPath:)]; _dataSourceFlags.constrainedSizeForSupplementaryNodeOfKindAtIndexPath = [_dataSource respondsToSelector:@selector(dataController:constrainedSizeForSupplementaryNodeOfKind:atIndexPath:)]; _dataSourceFlags.contextForSection = [_dataSource respondsToSelector:@selector(dataController:contextForSection:)]; #if ASEVENTLOG_ENABLE _eventLog = eventLog; #endif self.visibleMap = self.pendingMap = [[ASElementMap alloc] init]; _nextSectionID = 0; _mainSerialQueue = [[ASMainSerialQueue alloc] init]; _synchronized = YES; _onDidFinishSynchronizingBlocks = [[NSMutableSet alloc] init]; const char *queueName = [[NSString stringWithFormat:@"org.AsyncDisplayKit.ASDataController.editingTransactionQueue:%p", self] cStringUsingEncoding:NSASCIIStringEncoding]; _editingTransactionQueue = dispatch_queue_create(queueName, DISPATCH_QUEUE_SERIAL); dispatch_queue_set_specific(_editingTransactionQueue, &kASDataControllerEditingQueueKey, &kASDataControllerEditingQueueContext, NULL); _editingTransactionGroup = dispatch_group_create(); return self; } - (id)layoutDelegate { ASDisplayNodeAssertMainThread(); return _layoutDelegate; } - (void)setLayoutDelegate:(id)layoutDelegate { ASDisplayNodeAssertMainThread(); if (layoutDelegate != _layoutDelegate) { _layoutDelegate = layoutDelegate; } } #pragma mark - Cell Layout - (void)_allocateNodesFromElements:(NSArray *)elements { ASSERT_ON_EDITING_QUEUE; NSUInteger nodeCount = elements.count; __weak id weakDataSource = _dataSource; if (nodeCount == 0 || weakDataSource == nil) { return; } ASSignpostStart(ASSignpostDataControllerBatch); { as_activity_create_for_scope("Data controller batch"); dispatch_queue_t queue = dispatch_get_global_queue(QOS_CLASS_USER_INITIATED, 0); NSUInteger threadCount = 0; if ([_dataSource dataControllerShouldSerializeNodeCreation:self]) { threadCount = 1; } ASDispatchApply(nodeCount, queue, threadCount, ^(size_t i) { __strong id strongDataSource = weakDataSource; if (strongDataSource == nil) { return; } unowned ASCollectionElement *element = elements[i]; NSMutableDictionary *dict = [[NSThread currentThread] threadDictionary]; dict[ASThreadDictMaxConstraintSizeKey] = [NSValue valueWithCGSize:element.constrainedSize.max]; unowned ASCellNode *node = element.node; [dict removeObjectForKey:ASThreadDictMaxConstraintSizeKey]; // Layout the node if the size range is valid. ASSizeRange sizeRange = element.constrainedSize; if (ASSizeRangeHasSignificantArea(sizeRange)) { [self _layoutNode:node withConstrainedSize:sizeRange]; } }); } ASSignpostEndCustom(ASSignpostDataControllerBatch, self, 0, (weakDataSource != nil ? ASSignpostColorDefault : ASSignpostColorRed)); } /** * Measure and layout the given node with the constrained size range. */ - (void)_layoutNode:(ASCellNode *)node withConstrainedSize:(ASSizeRange)constrainedSize { if (![_dataSource dataController:self shouldEagerlyLayoutNode:node]) { return; } ASDisplayNodeAssert(ASSizeRangeHasSignificantArea(constrainedSize), @"Attempt to layout cell node with invalid size range %@", NSStringFromASSizeRange(constrainedSize)); CGRect frame = CGRectZero; frame.size = [node layoutThatFits:constrainedSize].size; node.frame = frame; } #pragma mark - Data Source Access (Calling _dataSource) - (NSArray *)_allIndexPathsForItemsOfKind:(NSString *)kind inSections:(NSIndexSet *)sections { ASDisplayNodeAssertMainThread(); if (sections.count == 0 || _dataSource == nil) { return @[]; } const auto indexPaths = [[NSMutableArray alloc] init]; if ([kind isEqualToString:ASDataControllerRowNodeKind]) { std::vector counts = [self itemCountsFromDataSource]; [sections enumerateRangesUsingBlock:^(NSRange range, BOOL * _Nonnull stop) { for (NSUInteger sectionIndex = range.location; sectionIndex < NSMaxRange(range); sectionIndex++) { NSUInteger itemCount = counts[sectionIndex]; for (NSUInteger i = 0; i < itemCount; i++) { [indexPaths addObject:[NSIndexPath indexPathForItem:i inSection:sectionIndex]]; } } }]; } else if (_dataSourceFlags.supplementaryNodesOfKindInSection) { id dataSource = _dataSource; [sections enumerateRangesUsingBlock:^(NSRange range, BOOL * _Nonnull stop) { for (NSUInteger sectionIndex = range.location; sectionIndex < NSMaxRange(range); sectionIndex++) { NSUInteger itemCount = [dataSource dataController:self supplementaryNodesOfKind:kind inSection:sectionIndex]; for (NSUInteger i = 0; i < itemCount; i++) { [indexPaths addObject:[NSIndexPath indexPathForItem:i inSection:sectionIndex]]; } } }]; } return indexPaths; } /** * Agressively repopulates supplementary nodes of all kinds for sections that contains some given index paths. * * @param map The element map into which to apply the change. * @param indexPaths The index paths belongs to sections whose supplementary nodes need to be repopulated. * @param changeSet The changeset that triggered this repopulation. * @param traitCollection The trait collection needed to initialize elements * @param indexPathsAreNew YES if index paths are "after the update," NO otherwise. * @param shouldFetchSizeRanges Whether constrained sizes should be fetched from data source */ - (void)_repopulateSupplementaryNodesIntoMap:(ASMutableElementMap *)map forSectionsContainingIndexPaths:(NSArray *)indexPaths changeSet:(_ASHierarchyChangeSet *)changeSet traitCollection:(ASPrimitiveTraitCollection)traitCollection indexPathsAreNew:(BOOL)indexPathsAreNew shouldFetchSizeRanges:(BOOL)shouldFetchSizeRanges previousMap:(ASElementMap *)previousMap { ASDisplayNodeAssertMainThread(); if (indexPaths.count == 0) { return; } // Remove all old supplementaries from these sections NSIndexSet *oldSections = [NSIndexSet as_sectionsFromIndexPaths:indexPaths]; // Add in new ones with the new kinds. NSIndexSet *newSections; if (indexPathsAreNew) { newSections = oldSections; } else { newSections = [oldSections as_indexesByMapping:^NSUInteger(NSUInteger oldSection) { return [changeSet newSectionForOldSection:oldSection]; }]; } for (NSString *kind in [self supplementaryKindsInSections:newSections]) { [self _insertElementsIntoMap:map kind:kind forSections:newSections traitCollection:traitCollection shouldFetchSizeRanges:shouldFetchSizeRanges changeSet:changeSet previousMap:previousMap]; } } /** * Update supplementary nodes of all kinds for sections. * * @param map The element map into which to apply the change. * @param traitCollection The trait collection needed to initialize elements * @param shouldFetchSizeRanges Whether constrained sizes should be fetched from data source */ - (void)_updateSupplementaryNodesIntoMap:(ASMutableElementMap *)map traitCollection:(ASPrimitiveTraitCollection)traitCollection shouldFetchSizeRanges:(BOOL)shouldFetchSizeRanges previousMap:(ASElementMap *)previousMap { ASDisplayNodeAssertMainThread(); if (self.layoutDelegate != nil) { // TODO: https://github.com/TextureGroup/Texture/issues/948 return; } NSUInteger sectionCount = [self itemCountsFromDataSource].size(); if (sectionCount > 0) { NSIndexSet *sectionIndexes = [NSIndexSet indexSetWithIndexesInRange:NSMakeRange(0, sectionCount)]; ASSizeRange newSizeRange = ASSizeRangeZero; for (NSString *kind in [self supplementaryKindsInSections:sectionIndexes]) { NSArray *indexPaths = [self _allIndexPathsForItemsOfKind:kind inSections:sectionIndexes]; NSMutableArray *indexPathsToDeleteForKind = [[NSMutableArray alloc] init]; NSMutableArray *indexPathsToInsertForKind = [[NSMutableArray alloc] init]; // If supplementary node does exist and size is now zero, remove it. // If supplementary node doesn't exist and size is now non-zero, insert one. for (NSIndexPath *indexPath in indexPaths) { ASCollectionElement *previousElement = [previousMap supplementaryElementOfKind:kind atIndexPath:indexPath]; newSizeRange = [self constrainedSizeForNodeOfKind:kind atIndexPath:indexPath]; BOOL sizeRangeIsZero = ASSizeRangeEqualToSizeRange(ASSizeRangeZero, newSizeRange); if (previousElement != nil && sizeRangeIsZero) { [indexPathsToDeleteForKind addObject:indexPath]; } else if (previousElement == nil && !sizeRangeIsZero) { [indexPathsToInsertForKind addObject:indexPath]; } } [map removeSupplementaryElementsAtIndexPaths:indexPathsToDeleteForKind kind:kind]; [self _insertElementsIntoMap:map kind:kind atIndexPaths:indexPathsToInsertForKind traitCollection:traitCollection shouldFetchSizeRanges:shouldFetchSizeRanges changeSet:nil previousMap:previousMap]; } } } /** * Inserts new elements of a certain kind for some sections * * @param kind The kind of the elements, e.g ASDataControllerRowNodeKind * @param sections The sections that should be populated by new elements * @param traitCollection The trait collection needed to initialize elements * @param shouldFetchSizeRanges Whether constrained sizes should be fetched from data source */ - (void)_insertElementsIntoMap:(ASMutableElementMap *)map kind:(NSString *)kind forSections:(NSIndexSet *)sections traitCollection:(ASPrimitiveTraitCollection)traitCollection shouldFetchSizeRanges:(BOOL)shouldFetchSizeRanges changeSet:(_ASHierarchyChangeSet *)changeSet previousMap:(ASElementMap *)previousMap { ASDisplayNodeAssertMainThread(); if (sections.count == 0 || _dataSource == nil) { return; } NSArray *indexPaths = [self _allIndexPathsForItemsOfKind:kind inSections:sections]; [self _insertElementsIntoMap:map kind:kind atIndexPaths:indexPaths traitCollection:traitCollection shouldFetchSizeRanges:shouldFetchSizeRanges changeSet:changeSet previousMap:previousMap]; } /** * Inserts new elements of a certain kind at some index paths * * @param map The map to insert the elements into. * @param kind The kind of the elements, e.g ASDataControllerRowNodeKind * @param indexPaths The index paths at which new elements should be populated * @param traitCollection The trait collection needed to initialize elements * @param shouldFetchSizeRanges Whether constrained sizes should be fetched from data source */ - (void)_insertElementsIntoMap:(ASMutableElementMap *)map kind:(NSString *)kind atIndexPaths:(NSArray *)indexPaths traitCollection:(ASPrimitiveTraitCollection)traitCollection shouldFetchSizeRanges:(BOOL)shouldFetchSizeRanges changeSet:(_ASHierarchyChangeSet *)changeSet previousMap:(ASElementMap *)previousMap { ASDisplayNodeAssertMainThread(); if (indexPaths.count == 0 || _dataSource == nil) { return; } BOOL isRowKind = [kind isEqualToString:ASDataControllerRowNodeKind]; if (!isRowKind && !_dataSourceFlags.supplementaryNodeBlockOfKindAtIndexPath) { // Populating supplementary elements but data source doesn't support. return; } LOG(@"Populating elements of kind: %@, for index paths: %@", kind, indexPaths); id dataSource = self.dataSource; id node = self.node; BOOL shouldAsyncLayout = YES; for (NSIndexPath *indexPath in indexPaths) { ASCellNodeBlock nodeBlock; id nodeModel; if (isRowKind) { nodeModel = [dataSource dataController:self nodeModelForItemAtIndexPath:indexPath]; // Get the prior element and attempt to update the existing cell node. if (nodeModel != nil && !changeSet.includesReloadData) { NSIndexPath *oldIndexPath = [changeSet oldIndexPathForNewIndexPath:indexPath]; if (oldIndexPath != nil) { ASCollectionElement *oldElement = [previousMap elementForItemAtIndexPath:oldIndexPath]; ASCellNode *oldNode = oldElement.node; if ([oldNode canUpdateToNodeModel:nodeModel]) { // Just wrap the node in a block. The collection element will -setNodeModel: nodeBlock = ^{ return oldNode; }; } } } if (nodeBlock == nil) { nodeBlock = [dataSource dataController:self nodeBlockAtIndexPath:indexPath shouldAsyncLayout:&shouldAsyncLayout]; } } else { nodeBlock = [dataSource dataController:self supplementaryNodeBlockOfKind:kind atIndexPath:indexPath shouldAsyncLayout:&shouldAsyncLayout]; } ASSizeRange constrainedSize = ASSizeRangeUnconstrained; if (shouldFetchSizeRanges) { constrainedSize = [self constrainedSizeForNodeOfKind:kind atIndexPath:indexPath]; } ASCollectionElement *element = [[ASCollectionElement alloc] initWithNodeModel:nodeModel nodeBlock:nodeBlock supplementaryElementKind:isRowKind ? nil : kind constrainedSize:constrainedSize owningNode:node traitCollection:traitCollection]; [map insertElement:element atIndexPath:indexPath]; changeSet.countForAsyncLayout += (shouldAsyncLayout ? 1 : 0); } } - (void)invalidateDataSourceItemCounts { ASDisplayNodeAssertMainThread(); _itemCountsFromDataSourceAreValid = NO; } - (std::vector)itemCountsFromDataSource { ASDisplayNodeAssertMainThread(); if (NO == _itemCountsFromDataSourceAreValid) { id source = self.dataSource; NSInteger sectionCount = [source numberOfSectionsInDataController:self]; std::vector newCounts; newCounts.reserve(sectionCount); for (NSInteger i = 0; i < sectionCount; i++) { newCounts.push_back([source dataController:self rowsInSection:i]); } _itemCountsFromDataSource = newCounts; _itemCountsFromDataSourceAreValid = YES; } return _itemCountsFromDataSource; } - (NSArray *)supplementaryKindsInSections:(NSIndexSet *)sections { if (_dataSourceFlags.supplementaryNodeKindsInSections) { return [_dataSource dataController:self supplementaryNodeKindsInSections:sections]; } return @[]; } /** * Returns constrained size for the node of the given kind and at the given index path. * NOTE: index path must be in the data-source index space. */ - (ASSizeRange)constrainedSizeForNodeOfKind:(NSString *)kind atIndexPath:(NSIndexPath *)indexPath { ASDisplayNodeAssertMainThread(); id dataSource = _dataSource; if (dataSource == nil || indexPath == nil) { return ASSizeRangeZero; } if ([kind isEqualToString:ASDataControllerRowNodeKind]) { ASDisplayNodeAssert(_dataSourceFlags.constrainedSizeForNodeAtIndexPath, @"-dataController:constrainedSizeForNodeAtIndexPath: must also be implemented"); return [dataSource dataController:self constrainedSizeForNodeAtIndexPath:indexPath]; } if (_dataSourceFlags.constrainedSizeForSupplementaryNodeOfKindAtIndexPath){ return [dataSource dataController:self constrainedSizeForSupplementaryNodeOfKind:kind atIndexPath:indexPath]; } ASDisplayNodeAssert(NO, @"Unknown constrained size for node of kind %@ by data source %@", kind, dataSource); return ASSizeRangeZero; } #pragma mark - Batching (External API) - (void)waitUntilAllUpdatesAreProcessed { // Schedule block in main serial queue to wait until all operations are finished that are // where scheduled while waiting for the _editingTransactionQueue to finish [self _scheduleBlockOnMainSerialQueue:^{ }]; } - (BOOL)isProcessingUpdates { ASDisplayNodeAssertMainThread(); return _mainSerialQueue.numberOfScheduledBlocks > 0 || _editingTransactionGroupCount > 0; } - (void)onDidFinishProcessingUpdates:(void (^)())completion { ASDisplayNodeAssertMainThread(); if (!completion) { return; } if ([self isProcessingUpdates] == NO) { ASPerformBlockOnMainThread(completion); } else { dispatch_async(_editingTransactionQueue, ^{ // Retry the block. If we're done processing updates, it'll run immediately, otherwise // wait again for updates to quiesce completely. // Don't use _mainSerialQueue so that we don't affect -isProcessingUpdates. dispatch_async(dispatch_get_main_queue(), ^{ [self onDidFinishProcessingUpdates:completion]; }); }); } } - (BOOL)isSynchronized { return _synchronized; } - (void)onDidFinishSynchronizing:(void (^)())completion { ASDisplayNodeAssertMainThread(); if (!completion) { return; } if ([self isSynchronized]) { ASPerformBlockOnMainThread(completion); } else { // Hang on to the completion block so that it gets called the next time view is synchronized to data. [_onDidFinishSynchronizingBlocks addObject:[completion copy]]; } } - (void)updateWithChangeSet:(_ASHierarchyChangeSet *)changeSet { ASDisplayNodeAssertMainThread(); _synchronized = NO; [changeSet addCompletionHandler:^(BOOL finished) { _synchronized = YES; [self onDidFinishProcessingUpdates:^{ if (_synchronized) { for (ASDataControllerSynchronizationBlock block in _onDidFinishSynchronizingBlocks) { block(); } [_onDidFinishSynchronizingBlocks removeAllObjects]; } }]; }]; if (changeSet.includesReloadData) { if (_initialReloadDataHasBeenCalled) { as_log_debug(ASCollectionLog(), "reloadData %@", ASViewToDisplayNode(ASDynamicCast(self.dataSource, UIView))); } else { as_log_debug(ASCollectionLog(), "Initial reloadData %@", ASViewToDisplayNode(ASDynamicCast(self.dataSource, UIView))); _initialReloadDataHasBeenCalled = YES; } } else { as_log_debug(ASCollectionLog(), "performBatchUpdates %@ %@", ASViewToDisplayNode(ASDynamicCast(self.dataSource, UIView)), changeSet); } NSTimeInterval transactionQueueFlushDuration = 0.0f; { AS::ScopeTimer t(transactionQueueFlushDuration); dispatch_group_wait(_editingTransactionGroup, DISPATCH_TIME_FOREVER); } // If the initial reloadData has not been called, just bail because we don't have our old data source counts. // See ASUICollectionViewTests.testThatIssuingAnUpdateBeforeInitialReloadIsUnacceptable // for the issue that UICollectionView has that we're choosing to workaround. if (!_initialReloadDataHasBeenCalled) { as_log_debug(ASCollectionLog(), "%@ Skipped update because load hasn't happened.", ASObjectDescriptionMakeTiny(_dataSource)); [changeSet executeCompletionHandlerWithFinished:YES]; return; } [self invalidateDataSourceItemCounts]; // Log events #if ASEVENTLOG_ENABLE ASDataControllerLogEvent(self, @"updateWithChangeSet waited on previous update for %fms. changeSet: %@", transactionQueueFlushDuration * 1000.0f, changeSet); NSTimeInterval changeSetStartTime = CACurrentMediaTime(); NSString *changeSetDescription = ASObjectDescriptionMakeTiny(changeSet); [changeSet addCompletionHandler:^(BOOL finished) { ASDataControllerLogEvent(self, @"finishedUpdate in %fms: %@", (CACurrentMediaTime() - changeSetStartTime) * 1000.0f, changeSetDescription); }]; #endif // Attempt to mark the update completed. This is when update validation will occur inside the changeset. // If an invalid update exception is thrown, we catch it and inject our "validationErrorSource" object, // which is the table/collection node's data source, into the exception reason to help debugging. @try { [changeSet markCompletedWithNewItemCounts:[self itemCountsFromDataSource]]; } @catch (NSException *e) { id responsibleDataSource = self.validationErrorSource; if (e.name == ASCollectionInvalidUpdateException && responsibleDataSource != nil) { [NSException raise:ASCollectionInvalidUpdateException format:@"%@: %@", [responsibleDataSource class], e.reason]; } else { @throw e; } } BOOL canDelegate = (self.layoutDelegate != nil); ASElementMap *newMap; ASCollectionLayoutContext *layoutContext; { as_activity_scope(as_activity_create("Latch new data for collection update", changeSet.rootActivity, OS_ACTIVITY_FLAG_DEFAULT)); // Step 1: Populate a new map that reflects the data source's state and use it as pendingMap ASElementMap *previousMap = self.pendingMap; if (changeSet.isEmpty) { // If the change set is empty, nothing has changed so we can just reuse the previous map newMap = previousMap; } else { // Mutable copy of current data. ASMutableElementMap *mutableMap = [previousMap mutableCopy]; // Step 1.1: Update the mutable copies to match the data source's state [self _updateSectionsInMap:mutableMap changeSet:changeSet]; ASPrimitiveTraitCollection existingTraitCollection = [self.node primitiveTraitCollection]; [self _updateElementsInMap:mutableMap changeSet:changeSet traitCollection:existingTraitCollection shouldFetchSizeRanges:(! canDelegate) previousMap:previousMap]; // Step 1.2: Clone the new data newMap = [mutableMap copy]; } self.pendingMap = newMap; // Step 2: Ask layout delegate for contexts if (canDelegate) { layoutContext = [self.layoutDelegate layoutContextWithElements:newMap]; } } as_log_debug(ASCollectionLog(), "New content: %@", newMap.smallDescription); Class layoutDelegateClass = [self.layoutDelegate class]; ++_editingTransactionGroupCount; dispatch_group_async(_editingTransactionGroup, _editingTransactionQueue, ^{ __block __unused os_activity_scope_state_s preparationScope = {}; // unused if deployment target < iOS10 as_activity_scope_enter(as_activity_create("Prepare nodes for collection update", AS_ACTIVITY_CURRENT, OS_ACTIVITY_FLAG_DEFAULT), &preparationScope); // Step 3: Call the layout delegate if possible. Otherwise, allocate and layout all elements if (canDelegate) { [layoutDelegateClass calculateLayoutWithContext:layoutContext]; } else { const auto elementsToProcess = [[NSMutableArray alloc] init]; for (ASCollectionElement *element in newMap) { ASCellNode *nodeIfAllocated = element.nodeIfAllocated; if (nodeIfAllocated.shouldUseUIKitCell) { // If the node exists and we know it is a passthrough cell, we know it will never have a .calculatedLayout. continue; } else if (nodeIfAllocated.calculatedLayout == nil) { // If the node hasn't been allocated, or it doesn't have a valid layout, let's process it. [elementsToProcess addObject:element]; } } [self _allocateNodesFromElements:elementsToProcess]; } // Step 4: Inform the delegate on main thread [_mainSerialQueue performBlockOnMainThread:^{ as_activity_scope_leave(&preparationScope); [_delegate dataController:self updateWithChangeSet:changeSet updates:^{ // Step 5: Deploy the new data as "completed" // // Note that since the backing collection view might be busy responding to user events (e.g scrolling), // it will not consume the batch update blocks immediately. // As a result, in a short intermidate time, the view will still be relying on the old data source state. // Thus, we can't just swap the new map immediately before step 4, but until this update block is executed. // (https://github.com/TextureGroup/Texture/issues/378) self.visibleMap = newMap; }]; }]; --_editingTransactionGroupCount; }); // We've now dispatched node allocation and layout to a concurrent background queue. // In some cases, it's advantageous to prevent the main thread from returning, to ensure the next // frame displayed to the user has the view updates in place. Doing this does slightly reduce // total latency, by donating the main thread's priority to the background threads. As such, the // two cases where it makes sense to block: // 1. There is very little work to be performed in the background (UIKit passthrough) // 2. There is a higher priority on display latency than smoothness, e.g. app startup. if ([_dataSource dataController:self shouldSynchronouslyProcessChangeSet:changeSet]) { [self waitUntilAllUpdatesAreProcessed]; } } /** * Update sections based on the given change set. */ - (void)_updateSectionsInMap:(ASMutableElementMap *)map changeSet:(_ASHierarchyChangeSet *)changeSet { ASDisplayNodeAssertMainThread(); if (changeSet.includesReloadData) { [map removeAllSections]; NSUInteger sectionCount = [self itemCountsFromDataSource].size(); NSIndexSet *sectionIndexes = [NSIndexSet indexSetWithIndexesInRange:NSMakeRange(0, sectionCount)]; [self _insertSectionsIntoMap:map indexes:sectionIndexes]; // Return immediately because reloadData can't be used in conjuntion with other updates. return; } for (_ASHierarchySectionChange *change in [changeSet sectionChangesOfType:_ASHierarchyChangeTypeDelete]) { [map removeSectionsAtIndexes:change.indexSet]; } for (_ASHierarchySectionChange *change in [changeSet sectionChangesOfType:_ASHierarchyChangeTypeInsert]) { [self _insertSectionsIntoMap:map indexes:change.indexSet]; } } - (void)_insertSectionsIntoMap:(ASMutableElementMap *)map indexes:(NSIndexSet *)sectionIndexes { ASDisplayNodeAssertMainThread(); [sectionIndexes enumerateIndexesUsingBlock:^(NSUInteger idx, BOOL * _Nonnull stop) { id context; if (_dataSourceFlags.contextForSection) { context = [_dataSource dataController:self contextForSection:idx]; } ASSection *section = [[ASSection alloc] initWithSectionID:_nextSectionID context:context]; [map insertSection:section atIndex:idx]; _nextSectionID++; }]; } /** * Update elements based on the given change set. */ - (void)_updateElementsInMap:(ASMutableElementMap *)map changeSet:(_ASHierarchyChangeSet *)changeSet traitCollection:(ASPrimitiveTraitCollection)traitCollection shouldFetchSizeRanges:(BOOL)shouldFetchSizeRanges previousMap:(ASElementMap *)previousMap { ASDisplayNodeAssertMainThread(); if (changeSet.includesReloadData) { [map removeAllElements]; NSUInteger sectionCount = [self itemCountsFromDataSource].size(); if (sectionCount > 0) { NSIndexSet *sectionIndexes = [NSIndexSet indexSetWithIndexesInRange:NSMakeRange(0, sectionCount)]; [self _insertElementsIntoMap:map sections:sectionIndexes traitCollection:traitCollection shouldFetchSizeRanges:shouldFetchSizeRanges changeSet:changeSet previousMap:previousMap]; } // Return immediately because reloadData can't be used in conjuntion with other updates. return; } // Migrate old supplementary nodes to their new index paths. [map migrateSupplementaryElementsWithSectionMapping:changeSet.sectionMapping]; for (_ASHierarchyItemChange *change in [changeSet itemChangesOfType:_ASHierarchyChangeTypeDelete]) { [map removeItemsAtIndexPaths:change.indexPaths]; // Aggressively repopulate supplementary nodes (#1773 & #1629) [self _repopulateSupplementaryNodesIntoMap:map forSectionsContainingIndexPaths:change.indexPaths changeSet:changeSet traitCollection:traitCollection indexPathsAreNew:NO shouldFetchSizeRanges:shouldFetchSizeRanges previousMap:previousMap]; } for (_ASHierarchySectionChange *change in [changeSet sectionChangesOfType:_ASHierarchyChangeTypeDelete]) { NSIndexSet *sectionIndexes = change.indexSet; [map removeSectionsOfItems:sectionIndexes]; } for (_ASHierarchySectionChange *change in [changeSet sectionChangesOfType:_ASHierarchyChangeTypeInsert]) { [self _insertElementsIntoMap:map sections:change.indexSet traitCollection:traitCollection shouldFetchSizeRanges:shouldFetchSizeRanges changeSet:changeSet previousMap:previousMap]; } for (_ASHierarchyItemChange *change in [changeSet itemChangesOfType:_ASHierarchyChangeTypeInsert]) { [self _insertElementsIntoMap:map kind:ASDataControllerRowNodeKind atIndexPaths:change.indexPaths traitCollection:traitCollection shouldFetchSizeRanges:shouldFetchSizeRanges changeSet:changeSet previousMap:previousMap]; // Aggressively reload supplementary nodes (#1773 & #1629) [self _repopulateSupplementaryNodesIntoMap:map forSectionsContainingIndexPaths:change.indexPaths changeSet:changeSet traitCollection:traitCollection indexPathsAreNew:YES shouldFetchSizeRanges:shouldFetchSizeRanges previousMap:previousMap]; } } - (void)_insertElementsIntoMap:(ASMutableElementMap *)map sections:(NSIndexSet *)sectionIndexes traitCollection:(ASPrimitiveTraitCollection)traitCollection shouldFetchSizeRanges:(BOOL)shouldFetchSizeRanges changeSet:(_ASHierarchyChangeSet *)changeSet previousMap:(ASElementMap *)previousMap { ASDisplayNodeAssertMainThread(); if (sectionIndexes.count == 0 || _dataSource == nil) { return; } // Items [map insertEmptySectionsOfItemsAtIndexes:sectionIndexes]; [self _insertElementsIntoMap:map kind:ASDataControllerRowNodeKind forSections:sectionIndexes traitCollection:traitCollection shouldFetchSizeRanges:shouldFetchSizeRanges changeSet:changeSet previousMap:previousMap]; // Supplementaries for (NSString *kind in [self supplementaryKindsInSections:sectionIndexes]) { // Step 2: Populate new elements for all sections [self _insertElementsIntoMap:map kind:kind forSections:sectionIndexes traitCollection:traitCollection shouldFetchSizeRanges:shouldFetchSizeRanges changeSet:changeSet previousMap:previousMap]; } } #pragma mark - Relayout - (void)relayoutNodes:(id)nodes nodesSizeChanged:(NSMutableArray *)nodesSizesChanged { NSParameterAssert(nodes); NSParameterAssert(nodesSizesChanged); ASDisplayNodeAssertMainThread(); if (!_initialReloadDataHasBeenCalled) { return; } id dataSource = self.dataSource; const auto visibleMap = self.visibleMap; const auto pendingMap = self.pendingMap; for (ASCellNode *node in nodes) { const auto element = node.collectionElement; NSIndexPath *indexPathInPendingMap = [pendingMap indexPathForElement:element]; // Ensure the element is present in both maps or skip it. If it's not in the visible map, // then we can't check the presented size. If it's not in the pending map, we can't get the constrained size. // This will only happen if the element has been deleted, so the specifics of this behavior aren't important. if (indexPathInPendingMap == nil || [visibleMap indexPathForElement:element] == nil) { continue; } NSString *kind = element.supplementaryElementKind ?: ASDataControllerRowNodeKind; ASSizeRange constrainedSize = [self constrainedSizeForNodeOfKind:kind atIndexPath:indexPathInPendingMap]; [self _layoutNode:node withConstrainedSize:constrainedSize]; BOOL matchesSize = [dataSource dataController:self presentedSizeForElement:element matchesSize:node.frame.size]; if (! matchesSize) { [nodesSizesChanged addObject:node]; } } } - (void)relayoutAllNodesWithInvalidationBlock:(nullable void (^)())invalidationBlock { ASDisplayNodeAssertMainThread(); if (!_initialReloadDataHasBeenCalled) { return; } // Can't relayout right away because _visibleMap may not be up-to-date, // i.e there might be some nodes that were measured using the old constrained size but haven't been added to _visibleMap LOG(@"Edit Command - relayoutRows"); [self _scheduleBlockOnMainSerialQueue:^{ // Because -invalidateLayout doesn't trigger any operations by itself, and we answer queries from UICollectionView using layoutThatFits:, // we invalidate the layout before we have updated all of the cells. Any cells that the collection needs the size of immediately will get // -layoutThatFits: with a new constraint, on the main thread, and synchronously calculate them. Meanwhile, relayoutAllNodes will update // the layout of any remaining nodes on background threads (and fast-return for any nodes that the UICV got to first). if (invalidationBlock) { invalidationBlock(); } [self _relayoutAllNodes]; }]; } - (void)_relayoutAllNodes { ASDisplayNodeAssertMainThread(); // Aggressively repopulate all supplemtary elements // Assuming this method is run on the main serial queue, _pending and _visible maps are synced and can be manipulated directly. ASDisplayNodeAssert(_visibleMap == _pendingMap, @"Expected visible and pending maps to be synchronized: %@", self); ASMutableElementMap *newMap = [_pendingMap mutableCopy]; [self _updateSupplementaryNodesIntoMap:newMap traitCollection:[self.node primitiveTraitCollection] shouldFetchSizeRanges:YES previousMap:_pendingMap]; _pendingMap = [newMap copy]; _visibleMap = _pendingMap; for (ASCollectionElement *element in _visibleMap) { // Ignore this element if it is no longer in the latest data. It is still recognized in the UIKit world but will be deleted soon. NSIndexPath *indexPathInPendingMap = [_pendingMap indexPathForElement:element]; if (indexPathInPendingMap == nil) { continue; } NSString *kind = element.supplementaryElementKind ?: ASDataControllerRowNodeKind; ASSizeRange newConstrainedSize = [self constrainedSizeForNodeOfKind:kind atIndexPath:indexPathInPendingMap]; if (ASSizeRangeHasSignificantArea(newConstrainedSize)) { element.constrainedSize = newConstrainedSize; // Node may not be allocated yet (e.g node virtualization or same size optimization) // Call context.nodeIfAllocated here to avoid premature node allocation and layout ASCellNode *node = element.nodeIfAllocated; if (node) { [self _layoutNode:node withConstrainedSize:newConstrainedSize]; } } } } # pragma mark - ASPrimitiveTraitCollection - (void)environmentDidChange { ASPerformBlockOnMainThread(^{ if (!_initialReloadDataHasBeenCalled) { return; } // Can't update the trait collection right away because _visibleMap may not be up-to-date, // i.e there might be some elements that were allocated using the old trait collection but haven't been added to _visibleMap [self _scheduleBlockOnMainSerialQueue:^{ ASPrimitiveTraitCollection newTraitCollection = [self.node primitiveTraitCollection]; for (ASCollectionElement *element in _visibleMap) { element.traitCollection = newTraitCollection; } }]; }); } - (void)clearData { ASDisplayNodeAssertMainThread(); if (_initialReloadDataHasBeenCalled) { [self waitUntilAllUpdatesAreProcessed]; self.visibleMap = self.pendingMap = [[ASElementMap alloc] init]; } } # pragma mark - Helper methods - (void)_scheduleBlockOnMainSerialQueue:(dispatch_block_t)block { ASDisplayNodeAssertMainThread(); dispatch_group_wait(_editingTransactionGroup, DISPATCH_TIME_FOREVER); [_mainSerialQueue performBlockOnMainThread:block]; } @end #endif