Swiftgram/Source/Details/ASDataController.mm
Huy Nguyen 3ccc2f0f15 Introduce ASCollectionGalleryLayoutDelegate (#76)
* Implement ASCollectionGalleryLayoutDelegate
- It arranges items of the same size into a multi-line stack (say photo gallery or pager). It takes advantage of the fact that its items always have a fixed size to measure as few items as possible while still being able to track their positions at all time. This helps reduce startup/reloadData time, as well as memory footprint.
- It then uses a measure range, which also works as a allocate range, to figure out which items to measure ahead of time. And it guarantees that each item is scheduled to measure only once.
- Lastly, ASCollectionLayoutDelegate has some new methods that allow delegates to hook up and stay ahead of layout attributes requests from the backing view. ASCollectionGalleryLayoutDelegate for example uses these methods to ensure elements that have their layout attributes requested are always ready for consumption, and to measure more elements in the background.

* Handle items that span multiple pages and other improvements in gallery delegate

* Minor fixes

* Fix failing tests

* Fix custom collection example

* Implement missing method in gallery layout delegate

* Fix warnings

* Some improvements
- Collection layout delegates must have a crollable directions property.
- Simplify gallery delegate by not storing unmeasured attributes since calling measure on already measured elements should be cache hits and super fast.
- Abstact some code in gallery delegate to ASCollectionLayoutState+Private and _ASCollectionGalleryLayoutItem.
- Other improvements in gallery delegate

* Fix file licenses

* Move measure range logic to ASCollectionLayout

* Track unmeasured elements

* Remove pending layout in ASCollectionLayout

* Get back pending layout because the timing to latch new data is not ideal

* Add ASCollectionLayoutCache

* Fix file licenses

* Fix xcodeproj

* Add async collection layout to examples/ASCollectionView

* Measure method in ASCollectionLayout to be a class method

* Encourage more immutable states
- Make -calculateLayoutWithContext: to be class methods in ASDataControllerLayoutDelegate and ASCollectionLayoutDelegate.
- Add layout delegate class and layout cache to ASCollectionLayoutContext+Private, to be use by ASCollectionLayout only.
- ASDataController no longer allocates all nodes but lets ASCollectionLayout determine.
- Add scrollableDirections to the layout context since it's often needed by the layout pass. Otherwise users have to wrap it in an info object.
- Update built-in layout delegates and CustomCollectionView example.
- Publish ASHashing. It might be helpful for clients that implement custom collection info objects.

* Remove additionalInfo property in ASCollectionLayoutState

* ASCollectionLayoutState to correctly filter unmeasured elements

* Add ASHashing to umbrella header

* Fix file licenses

* Add ASDispatchAsync and use it in ASCollectionLayout

* Improve code comment in ASCollectionLayoutState
2017-07-14 18:50:26 +00:00

832 lines
34 KiB
Plaintext

//
// ASDataController.mm
// Texture
//
// 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 /ASDK-Licenses directory of this source tree. An additional
// grant of patent rights can be found in the PATENTS file in the same directory.
//
// Modifications to this file made after 4/13/2017 are: Copyright (c) 2017-present,
// Pinterest, Inc. Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
#import <AsyncDisplayKit/ASDataController.h>
#import <AsyncDisplayKit/_ASHierarchyChangeSet.h>
#import <AsyncDisplayKit/_ASScopeTimer.h>
#import <AsyncDisplayKit/ASAssert.h>
#import <AsyncDisplayKit/ASCellNode.h>
#import <AsyncDisplayKit/ASCollectionElement.h>
#import <AsyncDisplayKit/ASCollectionLayoutContext.h>
#import <AsyncDisplayKit/ASDispatch.h>
#import <AsyncDisplayKit/ASDisplayNodeExtras.h>
#import <AsyncDisplayKit/ASElementMap.h>
#import <AsyncDisplayKit/ASLayout.h>
#import <AsyncDisplayKit/ASLog.h>
#import <AsyncDisplayKit/ASSignpost.h>
#import <AsyncDisplayKit/ASMainSerialQueue.h>
#import <AsyncDisplayKit/ASMutableElementMap.h>
#import <AsyncDisplayKit/ASRangeManagingNode.h>
#import <AsyncDisplayKit/ASThread.h>
#import <AsyncDisplayKit/ASTwoDimensionalArrayUtils.h>
#import <AsyncDisplayKit/ASSection.h>
#import <AsyncDisplayKit/ASInternalHelpers.h>
#import <AsyncDisplayKit/ASCellNode+Internal.h>
#import <AsyncDisplayKit/ASDisplayNode+Subclasses.h>
#import <AsyncDisplayKit/NSIndexSet+ASHelpers.h>
//#define LOG(...) NSLog(__VA_ARGS__)
#define LOG(...)
#define RETURN_IF_NO_DATASOURCE(val) if (_dataSource == nil) { return val; }
#define ASSERT_ON_EDITING_QUEUE ASDisplayNodeAssertNotNil(dispatch_get_specific(&kASDataControllerEditingQueueKey), @"%@ must be called on the editing transaction queue.", NSStringFromSelector(_cmd))
const static NSUInteger kASDataControllerSizingCountPerProcessor = 5;
const static char * kASDataControllerEditingQueueKey = "kASDataControllerEditingQueueKey";
const static char * kASDataControllerEditingQueueContext = "kASDataControllerEditingQueueContext";
NSString * const ASDataControllerRowNodeKind = @"_ASDataControllerRowNodeKind";
NSString * const ASCollectionInvalidUpdateException = @"ASCollectionInvalidUpdateException";
typedef dispatch_block_t ASDataControllerCompletionBlock;
@interface ASDataController () {
id<ASDataControllerLayoutDelegate> _layoutDelegate;
NSInteger _nextSectionID;
BOOL _itemCountsFromDataSourceAreValid; // Main thread only.
std::vector<NSInteger> _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.
BOOL _initialReloadDataHasBeenCalled;
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 (atomic, copy, readwrite) ASElementMap *pendingMap;
@property (atomic, copy, readwrite) ASElementMap *visibleMap;
@end
@implementation ASDataController
#pragma mark - Lifecycle
- (instancetype)initWithDataSource:(id<ASDataControllerSource>)dataSource node:(nullable id<ASRangeManagingNode>)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:)];
_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];
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;
}
+ (NSUInteger)parallelProcessorCount
{
static NSUInteger parallelProcessorCount;
static dispatch_once_t onceToken;
dispatch_once(&onceToken, ^{
parallelProcessorCount = [[NSProcessInfo processInfo] activeProcessorCount];
});
return parallelProcessorCount;
}
- (id<ASDataControllerLayoutDelegate>)layoutDelegate
{
ASDisplayNodeAssertMainThread();
return _layoutDelegate;
}
- (void)setLayoutDelegate:(id<ASDataControllerLayoutDelegate>)layoutDelegate
{
ASDisplayNodeAssertMainThread();
if (layoutDelegate != _layoutDelegate) {
_layoutDelegate = layoutDelegate;
}
}
#pragma mark - Cell Layout
- (void)batchAllocateNodesFromElements:(NSArray<ASCollectionElement *> *)elements batchSize:(NSInteger)batchSize batchCompletion:(ASDataControllerCompletionBlock)batchCompletionHandler
{
ASSERT_ON_EDITING_QUEUE;
if (elements.count == 0 || _dataSource == nil) {
batchCompletionHandler();
return;
}
ASSignpostStart(ASSignpostDataControllerBatch);
if (batchSize == 0) {
batchSize = [[ASDataController class] parallelProcessorCount] * kASDataControllerSizingCountPerProcessor;
}
NSUInteger count = elements.count;
// Processing in batches
for (NSUInteger i = 0; i < count; i += batchSize) {
NSRange batchedRange = NSMakeRange(i, MIN(count - i, batchSize));
NSArray<ASCollectionElement *> *batchedElements = [elements subarrayWithRange:batchedRange];
{
as_activity_create_for_scope("Data controller batch");
[self _allocateNodesFromElements:batchedElements];
}
batchCompletionHandler();
}
ASSignpostEndCustom(ASSignpostDataControllerBatch, self, 0, (_dataSource != nil ? ASSignpostColorDefault : ASSignpostColorRed));
}
/**
* Measure and layout the given node with the constrained size range.
*/
- (void)_layoutNode:(ASCellNode *)node withConstrainedSize:(ASSizeRange)constrainedSize
{
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;
}
// TODO Is returned array still needed? Can it be removed?
- (void)_allocateNodesFromElements:(NSArray<ASCollectionElement *> *)elements
{
ASSERT_ON_EDITING_QUEUE;
NSUInteger nodeCount = elements.count;
if (!nodeCount || _dataSource == nil) {
return;
}
dispatch_queue_t queue = dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0);
ASDispatchApply(nodeCount, queue, 0, ^(size_t i) {
RETURN_IF_NO_DATASOURCE();
// Allocate the node.
ASCollectionElement *context = elements[i];
ASCellNode *node = context.node;
if (node == nil) {
ASDisplayNodeAssertNotNil(node, @"Node block created nil node; %@, %@", self, self.dataSource);
node = [[ASCellNode alloc] init]; // Fallback to avoid crash for production apps.
}
// Layout the node if the size range is valid.
ASSizeRange sizeRange = context.constrainedSize;
if (ASSizeRangeHasSignificantArea(sizeRange)) {
[self _layoutNode:node withConstrainedSize:sizeRange];
}
});
}
#pragma mark - Data Source Access (Calling _dataSource)
- (NSArray<NSIndexPath *> *)_allIndexPathsForItemsOfKind:(NSString *)kind inSections:(NSIndexSet *)sections
{
ASDisplayNodeAssertMainThread();
if (sections.count == 0 || _dataSource == nil) {
return @[];
}
NSMutableArray<NSIndexPath *> *indexPaths = [NSMutableArray array];
if ([kind isEqualToString:ASDataControllerRowNodeKind]) {
std::vector<NSInteger> 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<ASDataControllerSource> 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<NSIndexPath *> *)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];
}
}
/**
* 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<NSIndexPath *> *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<NSIndexPath *> *)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<ASDataControllerSource> dataSource = self.dataSource;
id<ASRangeManagingNode> node = self.node;
for (NSIndexPath *indexPath in indexPaths) {
ASCellNodeBlock nodeBlock;
id viewModel;
if (isRowKind) {
viewModel = [dataSource dataController:self viewModelForItemAtIndexPath:indexPath];
// Get the prior element and attempt to update the existing cell node.
if (viewModel != nil && !changeSet.includesReloadData) {
NSIndexPath *oldIndexPath = [changeSet oldIndexPathForNewIndexPath:indexPath];
if (oldIndexPath != nil) {
ASCollectionElement *oldElement = [previousMap elementForItemAtIndexPath:oldIndexPath];
ASCellNode *oldNode = oldElement.node;
if ([oldNode canUpdateToViewModel:viewModel]) {
// Just wrap the node in a block. The collection element will -setViewModel:
nodeBlock = ^{
return oldNode;
};
}
}
}
if (nodeBlock == nil) {
nodeBlock = [dataSource dataController:self nodeBlockAtIndexPath:indexPath];
}
} else {
nodeBlock = [dataSource dataController:self supplementaryNodeBlockOfKind:kind atIndexPath:indexPath];
}
ASSizeRange constrainedSize = ASSizeRangeUnconstrained;
if (shouldFetchSizeRanges) {
constrainedSize = [self constrainedSizeForNodeOfKind:kind atIndexPath:indexPath];
}
ASCollectionElement *element = [[ASCollectionElement alloc] initWithViewModel:viewModel
nodeBlock:nodeBlock
supplementaryElementKind:isRowKind ? nil : kind
constrainedSize:constrainedSize
owningNode:node
traitCollection:traitCollection];
[map insertElement:element atIndexPath:indexPath];
}
}
- (void)invalidateDataSourceItemCounts
{
ASDisplayNodeAssertMainThread();
_itemCountsFromDataSourceAreValid = NO;
}
- (std::vector<NSInteger>)itemCountsFromDataSource
{
ASDisplayNodeAssertMainThread();
if (NO == _itemCountsFromDataSourceAreValid) {
id<ASDataControllerSource> source = self.dataSource;
NSInteger sectionCount = [source numberOfSectionsInDataController:self];
std::vector<NSInteger> 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<NSString *> *)supplementaryKindsInSections:(NSIndexSet *)sections
{
if (_dataSourceFlags.supplementaryNodeKindsInSections) {
return [_dataSource dataController:self supplementaryNodeKindsInSections:sections];
}
return @[];
}
- (ASSizeRange)constrainedSizeForElement:(ASCollectionElement *)element inElementMap:(ASElementMap *)map
{
ASDisplayNodeAssertMainThread();
NSString *kind = element.supplementaryElementKind ?: ASDataControllerRowNodeKind;
NSIndexPath *indexPath = [map indexPathForElement:element];
return [self constrainedSizeForNodeOfKind:kind atIndexPath:indexPath];
}
- (ASSizeRange)constrainedSizeForNodeOfKind:(NSString *)kind atIndexPath:(NSIndexPath *)indexPath
{
ASDisplayNodeAssertMainThread();
id<ASDataControllerSource> dataSource = _dataSource;
if (dataSource == 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)waitUntilAllUpdatesAreCommitted
{
// 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:^{ }];
}
- (void)updateWithChangeSet:(_ASHierarchyChangeSet *)changeSet
{
ASDisplayNodeAssertMainThread();
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;
{
ASDN::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;
id 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 _updateSectionContextsInMap: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<ASDataControllerLayoutDelegate> layoutDelegateClass = [self.layoutDelegate class];
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);
dispatch_block_t completion = ^() {
[_mainSerialQueue performBlockOnMainThread:^{
as_activity_scope_leave(&preparationScope);
// TODO Merge the two delegate methods below
[_delegate dataController:self willUpdateWithChangeSet:changeSet];
// Step 4: Inform the delegate
[_delegate dataController:self didUpdateWithChangeSet: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;
}];
}];
};
// Step 3: Call the layout delegate if possible. Otherwise, allocate and layout all elements
if (canDelegate) {
[layoutDelegateClass calculateLayoutWithContext:layoutContext];
completion();
} else {
NSArray<ASCollectionElement *> *elementsToProcess = ASArrayByFlatMapping(newMap,
ASCollectionElement *element,
(element.nodeIfAllocated.calculatedLayout == nil ? element : nil));
[self batchAllocateNodesFromElements:elementsToProcess batchSize:elementsToProcess.count batchCompletion:completion];
}
});
if (_usesSynchronousDataLoading) {
[self waitUntilAllUpdatesAreCommitted];
}
}
/**
* Update sections based on the given change set.
*/
- (void)_updateSectionContextsInMap:(ASMutableElementMap *)map changeSet:(_ASHierarchyChangeSet *)changeSet
{
ASDisplayNodeAssertMainThread();
if (!_dataSourceFlags.contextForSection) {
return;
}
if (changeSet.includesReloadData) {
[map removeAllSectionContexts];
NSUInteger sectionCount = [self itemCountsFromDataSource].size();
NSIndexSet *sectionIndexes = [NSIndexSet indexSetWithIndexesInRange:NSMakeRange(0, sectionCount)];
[self _insertSectionContextsIntoMap: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 removeSectionContextsAtIndexes:change.indexSet];
}
for (_ASHierarchySectionChange *change in [changeSet sectionChangesOfType:_ASHierarchyChangeTypeInsert]) {
[self _insertSectionContextsIntoMap:map indexes:change.indexSet];
}
}
- (void)_insertSectionContextsIntoMap:(ASMutableElementMap *)map indexes:(NSIndexSet *)sectionIndexes
{
ASDisplayNodeAssertMainThread();
if (!_dataSourceFlags.contextForSection) {
return;
}
[sectionIndexes enumerateIndexesUsingBlock:^(NSUInteger idx, BOOL * _Nonnull stop) {
id<ASSectionContext> 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<NSFastEnumeration>)nodes nodesSizeChanged:(NSMutableArray *)nodesSizesChanged
{
NSParameterAssert(nodesSizesChanged);
ASDisplayNodeAssertMainThread();
if (!_initialReloadDataHasBeenCalled) {
return;
}
id<ASDataControllerSource> dataSource = self.dataSource;
auto visibleMap = self.visibleMap;
auto pendingMap = self.pendingMap;
for (ASCellNode *node in nodes) {
auto element = node.collectionElement;
// 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 ([visibleMap indexPathForElement:element] == nil || [pendingMap indexPathForElement:element] == nil) {
continue;
}
ASSizeRange constrainedSize = [self constrainedSizeForElement:element inElementMap:pendingMap];
[self _layoutNode:node withConstrainedSize:constrainedSize];
BOOL matchesSize = [dataSource dataController:self presentedSizeForElement:element matchesSize:node.frame.size];
if (! matchesSize) {
[nodesSizesChanged addObject:node];
}
}
}
- (void)relayoutAllNodes
{
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:^{
[self _relayoutAllNodes];
}];
}
- (void)_relayoutAllNodes
{
ASDisplayNodeAssertMainThread();
for (ASCollectionElement *element in _visibleMap) {
ASSizeRange constrainedSize = [self constrainedSizeForElement:element inElementMap:_visibleMap];
if (ASSizeRangeHasSignificantArea(constrainedSize)) {
element.constrainedSize = constrainedSize;
// Node may not be allocated yet (e.g node virtualization or same size optimization)
// Call context.nodeIfAllocated here to avoid immature node allocation and layout
ASCellNode *node = element.nodeIfAllocated;
if (node) {
[self _layoutNode:node withConstrainedSize:constrainedSize];
}
}
}
}
# 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;
}
}];
});
}
# pragma mark - Helper methods
- (void)_scheduleBlockOnMainSerialQueue:(dispatch_block_t)block
{
ASDisplayNodeAssertMainThread();
dispatch_group_wait(_editingTransactionGroup, DISPATCH_TIME_FOREVER);
[_mainSerialQueue performBlockOnMainThread:block];
}
@end