[ASDataController] Simplify data controller (#2923)

* Start removing ASChangeSetDataController

* Continue removing ASChangeSetDataController

* Remove unnecessary change

* ASDataController is no longer an abstract class, remove its assertion

* Get back beginUpdates and endUpdatesAnimated:completion in ASCollectionNode
This commit is contained in:
Huy Nguyen
2017-01-24 17:41:19 -08:00
committed by Adlai Holler
parent 70c48ba906
commit 38f1efd448
16 changed files with 257 additions and 345 deletions

View File

@@ -321,9 +321,6 @@
A2763D7A1CBDD57D00A9ADBD /* ASPINRemoteImageDownloader.h in Headers */ = {isa = PBXBuildFile; fileRef = A2763D771CBDD57D00A9ADBD /* ASPINRemoteImageDownloader.h */; };
A37320101C571B740011FC94 /* ASTextNode+Beta.h in Headers */ = {isa = PBXBuildFile; fileRef = A373200E1C571B050011FC94 /* ASTextNode+Beta.h */; settings = {ATTRIBUTES = (Public, ); }; };
AC026B581BD3F61800BBC17E /* ASAbsoluteLayoutSpecSnapshotTests.m in Sources */ = {isa = PBXBuildFile; fileRef = AC026B571BD3F61800BBC17E /* ASAbsoluteLayoutSpecSnapshotTests.m */; };
AC026B6A1BD57D6F00BBC17E /* ASChangeSetDataController.h in Headers */ = {isa = PBXBuildFile; fileRef = AC026B671BD57D6F00BBC17E /* ASChangeSetDataController.h */; settings = {ATTRIBUTES = (Public, ); }; };
AC026B6B1BD57D6F00BBC17E /* ASChangeSetDataController.mm in Sources */ = {isa = PBXBuildFile; fileRef = AC026B681BD57D6F00BBC17E /* ASChangeSetDataController.mm */; };
AC026B6C1BD57D6F00BBC17E /* ASChangeSetDataController.mm in Sources */ = {isa = PBXBuildFile; fileRef = AC026B681BD57D6F00BBC17E /* ASChangeSetDataController.mm */; };
AC026B701BD57DBF00BBC17E /* _ASHierarchyChangeSet.h in Headers */ = {isa = PBXBuildFile; fileRef = AC026B6D1BD57DBF00BBC17E /* _ASHierarchyChangeSet.h */; };
AC026B711BD57DBF00BBC17E /* _ASHierarchyChangeSet.mm in Sources */ = {isa = PBXBuildFile; fileRef = AC026B6E1BD57DBF00BBC17E /* _ASHierarchyChangeSet.mm */; };
AC026B721BD57DBF00BBC17E /* _ASHierarchyChangeSet.mm in Sources */ = {isa = PBXBuildFile; fileRef = AC026B6E1BD57DBF00BBC17E /* _ASHierarchyChangeSet.mm */; };
@@ -578,7 +575,6 @@
F7CE6C4E1D2CDB3E00BE4C15 /* ASThread.h in CopyFiles */ = {isa = PBXBuildFile; fileRef = 058D0A12195D050800B7D73C /* ASThread.h */; };
F7CE6C4F1D2CDB3E00BE4C15 /* CoreGraphics+ASConvenience.h in CopyFiles */ = {isa = PBXBuildFile; fileRef = 205F0E1F1B376416007741D0 /* CoreGraphics+ASConvenience.h */; };
F7CE6C501D2CDB3E00BE4C15 /* ASDataController.h in CopyFiles */ = {isa = PBXBuildFile; fileRef = 464052191A3F83C40061C0BA /* ASDataController.h */; };
F7CE6C511D2CDB3E00BE4C15 /* ASChangeSetDataController.h in CopyFiles */ = {isa = PBXBuildFile; fileRef = AC026B671BD57D6F00BBC17E /* ASChangeSetDataController.h */; };
F7CE6C521D2CDB3E00BE4C15 /* ASIndexedNodeContext.h in CopyFiles */ = {isa = PBXBuildFile; fileRef = E5711A2A1C840C81009619D4 /* ASIndexedNodeContext.h */; };
F7CE6C531D2CDB3E00BE4C15 /* NSMutableAttributedString+TextKitAdditions.h in CopyFiles */ = {isa = PBXBuildFile; fileRef = 058D09F5195D050800B7D73C /* NSMutableAttributedString+TextKitAdditions.h */; };
F7CE6C541D2CDB3E00BE4C15 /* _ASAsyncTransaction.h in CopyFiles */ = {isa = PBXBuildFile; fileRef = 058D09F8195D050800B7D73C /* _ASAsyncTransaction.h */; };
@@ -757,7 +753,6 @@
F7CE6C4E1D2CDB3E00BE4C15 /* ASThread.h in CopyFiles */,
F7CE6C4F1D2CDB3E00BE4C15 /* CoreGraphics+ASConvenience.h in CopyFiles */,
F7CE6C501D2CDB3E00BE4C15 /* ASDataController.h in CopyFiles */,
F7CE6C511D2CDB3E00BE4C15 /* ASChangeSetDataController.h in CopyFiles */,
F7CE6C521D2CDB3E00BE4C15 /* ASIndexedNodeContext.h in CopyFiles */,
F7CE6C531D2CDB3E00BE4C15 /* NSMutableAttributedString+TextKitAdditions.h in CopyFiles */,
F7CE6C541D2CDB3E00BE4C15 /* _ASAsyncTransaction.h in CopyFiles */,
@@ -1088,8 +1083,6 @@
A32FEDD31C501B6A004F642A /* ASTextKitFontSizeAdjuster.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; name = ASTextKitFontSizeAdjuster.h; path = TextKit/ASTextKitFontSizeAdjuster.h; sourceTree = "<group>"; };
A373200E1C571B050011FC94 /* ASTextNode+Beta.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = "ASTextNode+Beta.h"; sourceTree = "<group>"; };
AC026B571BD3F61800BBC17E /* ASAbsoluteLayoutSpecSnapshotTests.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = ASAbsoluteLayoutSpecSnapshotTests.m; sourceTree = "<group>"; };
AC026B671BD57D6F00BBC17E /* ASChangeSetDataController.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = ASChangeSetDataController.h; sourceTree = "<group>"; };
AC026B681BD57D6F00BBC17E /* ASChangeSetDataController.mm */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.cpp.objcpp; path = ASChangeSetDataController.mm; sourceTree = "<group>"; };
AC026B6D1BD57DBF00BBC17E /* _ASHierarchyChangeSet.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = _ASHierarchyChangeSet.h; sourceTree = "<group>"; };
AC026B6E1BD57DBF00BBC17E /* _ASHierarchyChangeSet.mm */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.cpp.objcpp; path = _ASHierarchyChangeSet.mm; sourceTree = "<group>"; };
AC21EC0F1B3D0BF600C8B19A /* ASStackLayoutDefines.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; name = ASStackLayoutDefines.h; path = AsyncDisplayKit/Layout/ASStackLayoutDefines.h; sourceTree = "<group>"; };
@@ -1695,8 +1688,6 @@
251B8EF31BBB3D690087C538 /* ASCollectionDataController.mm */,
464052191A3F83C40061C0BA /* ASDataController.h */,
4640521A1A3F83C40061C0BA /* ASDataController.mm */,
AC026B671BD57D6F00BBC17E /* ASChangeSetDataController.h */,
AC026B681BD57D6F00BBC17E /* ASChangeSetDataController.mm */,
E5711A2A1C840C81009619D4 /* ASIndexedNodeContext.h */,
E5711A2D1C840C96009619D4 /* ASIndexedNodeContext.mm */,
AC6145401D8AFAE8003D62A2 /* ASSection.h */,
@@ -1834,7 +1825,6 @@
69E0E8A71D356C9400627613 /* ASEqualityHelpers.h in Headers */,
698C8B621CAB49FC0052DC3F /* ASLayoutElementExtensibility.h in Headers */,
698548641CA9E025008A345F /* ASEnvironment.h in Headers */,
AC026B6A1BD57D6F00BBC17E /* ASChangeSetDataController.h in Headers */,
69F10C871C84C35D0026140C /* ASRangeControllerUpdateRangeProtocol+Beta.h in Headers */,
B350623C1B010EFD0018CF92 /* _ASAsyncTransaction.h in Headers */,
9C70F20D1CDBE9CB007D6C76 /* ASDefaultPlayButton.h in Headers */,
@@ -2312,7 +2302,6 @@
81EE38501C8E94F000456208 /* ASRunLoopQueue.mm in Sources */,
9C70F2041CDA4EFA007D6C76 /* ASTraitCollection.m in Sources */,
ACF6ED321B17843500DA7C62 /* ASAbsoluteLayoutSpec.mm in Sources */,
AC026B6B1BD57D6F00BBC17E /* ASChangeSetDataController.mm in Sources */,
690C35611E055C5D00069B91 /* ASDimensionInternal.mm in Sources */,
68355B311CB5799E001D4E68 /* ASImageNode+AnimatedImage.mm in Sources */,
68C215591DE10D330019C4BC /* ASCollectionViewLayoutInspector.m in Sources */,
@@ -2503,7 +2492,6 @@
83A7D95B1D44547700BF333E /* ASWeakMap.m in Sources */,
DE84918E1C8FFF9F003D89E9 /* ASRunLoopQueue.mm in Sources */,
68FC85E51CE29B7E00EDD713 /* ASTabBarController.m in Sources */,
AC026B6C1BD57D6F00BBC17E /* ASChangeSetDataController.mm in Sources */,
34EFC7741B701D0A00AD841F /* ASAbsoluteLayoutSpec.mm in Sources */,
690C35621E055C5D00069B91 /* ASDimensionInternal.mm in Sources */,
68C2155A1DE10D330019C4BC /* ASCollectionViewLayoutInspector.m in Sources */,

View File

@@ -529,7 +529,7 @@
- (void)beginUpdates
{
[self.dataController beginUpdates];
[self.view beginUpdates];
}
- (void)endUpdatesAnimated:(BOOL)animated
@@ -539,7 +539,7 @@
- (void)endUpdatesAnimated:(BOOL)animated completion:(void (^)(BOOL))completion
{
[self.dataController endUpdatesAnimated:animated completion:completion];
[self.view endUpdatesAnimated:animated completion:completion];
}
- (void)insertSections:(NSIndexSet *)sections

View File

@@ -26,6 +26,7 @@
#import "ASCollectionViewLayoutFacilitatorProtocol.h"
#import "ASSectionContext.h"
#import "ASCollectionView+Undeprecated.h"
#import "_ASHierarchyChangeSet.h"
/**
* A macro to get self.collectionNode and assign it to a local variable, or return
@@ -196,7 +197,17 @@ static NSString * const kReuseIdentifier = @"_ASCollectionReuseIdentifier";
* (0 sections) we always check at least once after each update (initial reload is the first update.)
*/
BOOL _hasEverCheckedForBatchFetchingDueToUpdate;
/**
* The change set that we're currently building, if any.
*/
_ASHierarchyChangeSet *_changeSet;
/**
* Counter used to keep track of nested batch updates.
*/
NSInteger _batchUpdateCount;
struct {
unsigned int scrollViewDidScroll:1;
unsigned int scrollViewWillBeginDragging:1;
@@ -352,6 +363,8 @@ static NSString * const kReuseIdentifier = @"_ASCollectionReuseIdentifier";
- (void)dealloc
{
ASDisplayNodeAssertMainThread();
ASDisplayNodeCAssert(_batchUpdateCount == 0, @"ASCollectionView deallocated in the middle of a batch update.");
// Sometimes the UIKit classes can call back to their delegate even during deallocation, due to animation completion blocks etc.
_isDeallocating = YES;
[self setAsyncDelegate:nil];
@@ -402,6 +415,12 @@ static NSString * const kReuseIdentifier = @"_ASCollectionReuseIdentifier";
- (void)waitUntilAllUpdatesAreCommitted
{
ASDisplayNodeAssertMainThread();
if (_batchUpdateCount > 0) {
// This assertion will be enabled soon.
// ASDisplayNodeFailAssert(@"Should not call %@ during batch update", NSStringFromSelector(_cmd));
return;
}
[_dataController waitUntilAllUpdatesAreCommitted];
}
@@ -761,15 +780,43 @@ static NSString * const kReuseIdentifier = @"_ASCollectionReuseIdentifier";
return _dataController;
}
- (void)beginUpdates
{
ASDisplayNodeAssertMainThread();
// _changeSet must be available during batch update
ASDisplayNodeAssertTrue((_batchUpdateCount > 0) == (_changeSet != nil));
if (_batchUpdateCount == 0) {
_changeSet = [[_ASHierarchyChangeSet alloc] initWithOldData:[_dataController itemCountsFromDataSource]];
}
_batchUpdateCount++;
}
- (void)endUpdatesAnimated:(BOOL)animated completion:(nullable void (^)(BOOL))completion
{
ASDisplayNodeAssertMainThread();
ASDisplayNodeAssertNotNil(_changeSet, @"_changeSet must be available when batch update ends");
_batchUpdateCount--;
// Prevent calling endUpdatesAnimated:completion: in an unbalanced way
NSAssert(_batchUpdateCount >= 0, @"endUpdatesAnimated:completion: called without having a balanced beginUpdates call");
[_changeSet addCompletionHandler:completion];
if (_batchUpdateCount == 0) {
[_dataController updateWithChangeSet:_changeSet animated:animated];
_changeSet = nil;
}
}
- (void)performBatchAnimated:(BOOL)animated updates:(void (^)())updates completion:(void (^)(BOOL))completion
{
ASDisplayNodeAssertMainThread();
[_dataController beginUpdates];
[self beginUpdates];
if (updates) {
updates();
}
[_dataController endUpdatesAnimated:animated completion:completion];
[self endUpdatesAnimated:animated completion:completion];
}
- (void)performBatchUpdates:(void (^)())updates completion:(void (^)(BOOL))completion
@@ -789,27 +836,35 @@ static NSString * const kReuseIdentifier = @"_ASCollectionReuseIdentifier";
{
ASDisplayNodeAssertMainThread();
if (sections.count == 0) { return; }
[_dataController insertSections:sections withAnimationOptions:kASCollectionViewAnimationNone];
[self performBatchUpdates:^{
[_changeSet insertSections:sections animationOptions:kASCollectionViewAnimationNone];
} completion:nil];
}
- (void)deleteSections:(NSIndexSet *)sections
{
ASDisplayNodeAssertMainThread();
if (sections.count == 0) { return; }
[_dataController deleteSections:sections withAnimationOptions:kASCollectionViewAnimationNone];
[self performBatchUpdates:^{
[_changeSet deleteSections:sections animationOptions:kASCollectionViewAnimationNone];
} completion:nil];
}
- (void)reloadSections:(NSIndexSet *)sections
{
ASDisplayNodeAssertMainThread();
if (sections.count == 0) { return; }
[_dataController reloadSections:sections withAnimationOptions:kASCollectionViewAnimationNone];
[self performBatchUpdates:^{
[_changeSet reloadSections:sections animationOptions:kASCollectionViewAnimationNone];
} completion:nil];
}
- (void)moveSection:(NSInteger)section toSection:(NSInteger)newSection
{
ASDisplayNodeAssertMainThread();
[_dataController moveSection:section toSection:newSection withAnimationOptions:kASCollectionViewAnimationNone];
[self performBatchUpdates:^{
[_changeSet moveSection:section toSection:newSection animationOptions:kASCollectionViewAnimationNone];
} completion:nil];
}
- (id<ASSectionContext>)contextForSection:(NSInteger)section
@@ -822,27 +877,35 @@ static NSString * const kReuseIdentifier = @"_ASCollectionReuseIdentifier";
{
ASDisplayNodeAssertMainThread();
if (indexPaths.count == 0) { return; }
[_dataController insertRowsAtIndexPaths:indexPaths withAnimationOptions:kASCollectionViewAnimationNone];
[self performBatchUpdates:^{
[_changeSet insertItems:indexPaths animationOptions:kASCollectionViewAnimationNone];
} completion:nil];
}
- (void)deleteItemsAtIndexPaths:(NSArray *)indexPaths
{
ASDisplayNodeAssertMainThread();
if (indexPaths.count == 0) { return; }
[_dataController deleteRowsAtIndexPaths:indexPaths withAnimationOptions:kASCollectionViewAnimationNone];
[self performBatchUpdates:^{
[_changeSet deleteItems:indexPaths animationOptions:kASCollectionViewAnimationNone];
} completion:nil];
}
- (void)reloadItemsAtIndexPaths:(NSArray *)indexPaths
{
ASDisplayNodeAssertMainThread();
if (indexPaths.count == 0) { return; }
[_dataController reloadRowsAtIndexPaths:indexPaths withAnimationOptions:kASCollectionViewAnimationNone];
[self performBatchUpdates:^{
[_changeSet reloadItems:indexPaths animationOptions:kASCollectionViewAnimationNone];
} completion:nil];
}
- (void)moveItemAtIndexPath:(NSIndexPath *)indexPath toIndexPath:(NSIndexPath *)newIndexPath
{
ASDisplayNodeAssertMainThread();
[_dataController moveRowAtIndexPath:indexPath toIndexPath:newIndexPath withAnimationOptions:kASCollectionViewAnimationNone];
[self performBatchUpdates:^{
[_changeSet moveItemAtIndexPath:indexPath toIndexPath:newIndexPath animationOptions:kASCollectionViewAnimationNone];
} completion:nil];
}
#pragma mark -

View File

@@ -10,18 +10,18 @@
#import "ASTableViewInternal.h"
#import "_ASCoreAnimationExtras.h"
#import "_ASDisplayLayer.h"
#import "_ASHierarchyChangeSet.h"
#import "ASAssert.h"
#import "ASAvailability.h"
#import "ASBatchFetching.h"
#import "ASCellNode+Internal.h"
#import "ASChangeSetDataController.h"
#import "ASDelegateProxy.h"
#import "ASDisplayNodeExtras.h"
#import "ASDisplayNode+FrameworkPrivate.h"
#import "ASInternalHelpers.h"
#import "ASLayout.h"
#import "_ASDisplayLayer.h"
#import "_ASCoreAnimationExtras.h"
#import "ASTableNode.h"
#import "ASEqualityHelpers.h"
#import "ASTableView+Undeprecated.h"
@@ -154,6 +154,16 @@ static NSString * const kCellReuseIdentifier = @"_ASTableViewCell";
// This is useful because we need to measure our row nodes against (width - indexView.width).
__weak UIView *_sectionIndexView;
/**
* The change set that we're currently building, if any.
*/
_ASHierarchyChangeSet *_changeSet;
/**
* Counter used to keep track of nested batch updates.
*/
NSInteger _batchUpdateCount;
struct {
unsigned int scrollViewDidScroll:1;
unsigned int scrollViewWillBeginDragging:1;
@@ -230,7 +240,7 @@ static NSString * const kCellReuseIdentifier = @"_ASTableViewCell";
+ (Class)dataControllerClass
{
return [ASChangeSetDataController class];
return [ASDataController class];
}
#pragma mark -
@@ -303,6 +313,8 @@ static NSString * const kCellReuseIdentifier = @"_ASTableViewCell";
- (void)dealloc
{
ASDisplayNodeAssertMainThread();
ASDisplayNodeCAssert(_batchUpdateCount == 0, @"ASTableView deallocated in the middle of a batch update.");
// Sometimes the UIKit classes can call back to their delegate even during deallocation.
_isDeallocating = YES;
[self setAsyncDelegate:nil];
@@ -605,7 +617,7 @@ static NSString * const kCellReuseIdentifier = @"_ASTableViewCell";
NSIndexPath *indexPath = [_dataController completedIndexPathForNode:cellNode];
indexPath = [self validateIndexPath:indexPath];
if (indexPath == nil && wait) {
[_dataController waitUntilAllUpdatesAreCommitted];
[self waitUntilAllUpdatesAreCommitted];
indexPath = [_dataController completedIndexPathForNode:cellNode];
indexPath = [self validateIndexPath:indexPath];
}
@@ -631,7 +643,13 @@ static NSString * const kCellReuseIdentifier = @"_ASTableViewCell";
- (void)beginUpdates
{
ASDisplayNodeAssertMainThread();
[_dataController beginUpdates];
// _changeSet must be available during batch update
ASDisplayNodeAssertTrue((_batchUpdateCount > 0) == (_changeSet != nil));
if (_batchUpdateCount == 0) {
_changeSet = [[_ASHierarchyChangeSet alloc] initWithOldData:[_dataController itemCountsFromDataSource]];
}
_batchUpdateCount++;
}
- (void)endUpdates
@@ -643,12 +661,29 @@ static NSString * const kCellReuseIdentifier = @"_ASTableViewCell";
- (void)endUpdatesAnimated:(BOOL)animated completion:(void (^)(BOOL completed))completion;
{
ASDisplayNodeAssertMainThread();
[_dataController endUpdatesAnimated:animated completion:completion];
ASDisplayNodeAssertNotNil(_changeSet, @"_changeSet must be available when batch update ends");
_batchUpdateCount--;
// Prevent calling endUpdatesAnimated:completion: in an unbalanced way
NSAssert(_batchUpdateCount >= 0, @"endUpdatesAnimated:completion: called without having a balanced beginUpdates call");
[_changeSet addCompletionHandler:completion];
if (_batchUpdateCount == 0) {
[_dataController updateWithChangeSet:_changeSet animated:animated];
_changeSet = nil;
}
}
- (void)waitUntilAllUpdatesAreCommitted
{
ASDisplayNodeAssertMainThread();
if (_batchUpdateCount > 0) {
// This assertion will be enabled soon.
// ASDisplayNodeFailAssert(@"Should not call %@ during batch update", NSStringFromSelector(_cmd));
return;
}
[_dataController waitUntilAllUpdatesAreCommitted];
}
@@ -681,54 +716,70 @@ static NSString * const kCellReuseIdentifier = @"_ASTableViewCell";
{
ASDisplayNodeAssertMainThread();
if (sections.count == 0) { return; }
[_dataController insertSections:sections withAnimationOptions:animation];
[self beginUpdates];
[_changeSet insertSections:sections animationOptions:animation];
[self endUpdates];
}
- (void)deleteSections:(NSIndexSet *)sections withRowAnimation:(UITableViewRowAnimation)animation
{
ASDisplayNodeAssertMainThread();
if (sections.count == 0) { return; }
[_dataController deleteSections:sections withAnimationOptions:animation];
[self beginUpdates];
[_changeSet deleteSections:sections animationOptions:animation];
[self endUpdates];
}
- (void)reloadSections:(NSIndexSet *)sections withRowAnimation:(UITableViewRowAnimation)animation
{
ASDisplayNodeAssertMainThread();
if (sections.count == 0) { return; }
[_dataController reloadSections:sections withAnimationOptions:animation];
[self beginUpdates];
[_changeSet reloadSections:sections animationOptions:animation];
[self endUpdates];
}
- (void)moveSection:(NSInteger)section toSection:(NSInteger)newSection
{
ASDisplayNodeAssertMainThread();
[_dataController moveSection:section toSection:newSection withAnimationOptions:UITableViewRowAnimationNone];
[self beginUpdates];
[_changeSet moveSection:section toSection:newSection animationOptions:UITableViewRowAnimationNone];
[self endUpdates];
}
- (void)insertRowsAtIndexPaths:(NSArray *)indexPaths withRowAnimation:(UITableViewRowAnimation)animation
{
ASDisplayNodeAssertMainThread();
if (indexPaths.count == 0) { return; }
[_dataController insertRowsAtIndexPaths:indexPaths withAnimationOptions:animation];
[self beginUpdates];
[_changeSet insertItems:indexPaths animationOptions:animation];
[self endUpdates];
}
- (void)deleteRowsAtIndexPaths:(NSArray *)indexPaths withRowAnimation:(UITableViewRowAnimation)animation
{
ASDisplayNodeAssertMainThread();
if (indexPaths.count == 0) { return; }
[_dataController deleteRowsAtIndexPaths:indexPaths withAnimationOptions:animation];
[self beginUpdates];
[_changeSet deleteItems:indexPaths animationOptions:animation];
[self endUpdates];
}
- (void)reloadRowsAtIndexPaths:(NSArray *)indexPaths withRowAnimation:(UITableViewRowAnimation)animation
{
ASDisplayNodeAssertMainThread();
if (indexPaths.count == 0) { return; }
[_dataController reloadRowsAtIndexPaths:indexPaths withAnimationOptions:animation];
[self beginUpdates];
[_changeSet reloadItems:indexPaths animationOptions:animation];
[self endUpdates];
}
- (void)moveRowAtIndexPath:(NSIndexPath *)indexPath toIndexPath:(NSIndexPath *)newIndexPath
{
ASDisplayNodeAssertMainThread();
[_dataController moveRowAtIndexPath:indexPath toIndexPath:newIndexPath withAnimationOptions:UITableViewRowAnimationNone];
[self beginUpdates];
[_changeSet moveItemAtIndexPath:indexPath toIndexPath:newIndexPath animationOptions:UITableViewRowAnimationNone];
[self endUpdates];
}
#pragma mark -

View File

@@ -44,7 +44,7 @@
#import <AsyncDisplayKit/ASTabBarController.h>
#import <AsyncDisplayKit/ASRangeControllerUpdateRangeProtocol+Beta.h>
#import <AsyncDisplayKit/ASChangeSetDataController.h>
#import <AsyncDisplayKit/ASDataController.h>
#import <AsyncDisplayKit/ASLayout.h>
#import <AsyncDisplayKit/ASDimension.h>

View File

@@ -1,27 +0,0 @@
//
// ASChangeSetDataController.h
// AsyncDisplayKit
//
// Created by Huy Nguyen on 19/10/15.
//
// 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 <AsyncDisplayKit/ASDataController.h>
/**
* @abstract Subclass of ASDataController that simulates ordering of operations in batch updates defined in UITableView and UICollectionView.
*
* @discussion The ordering is achieved by using _ASHierarchyChangeSet to enqueue and sort operations.
* More information about the ordering and the index paths used for operations can be found here:
* https://developer.apple.com/library/ios/documentation/UserExperience/Conceptual/TableView_iPhone/ManageInsertDeleteRow/ManageInsertDeleteRow.html#//apple_ref/doc/uid/TP40007451-CH10-SW17
*
* @see ASDataController
* @see _ASHierarchyChangeSet
*/
@interface ASChangeSetDataController : ASDataController
@end

View File

@@ -1,208 +0,0 @@
//
// ASChangeSetDataController.m
// AsyncDisplayKit
//
// Created by Huy Nguyen on 19/10/15.
//
// 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 "ASChangeSetDataController.h"
#import "_ASHierarchyChangeSet.h"
#import "ASAssert.h"
#import "ASDataController+Subclasses.h"
@implementation ASChangeSetDataController {
NSInteger _changeSetBatchUpdateCounter;
_ASHierarchyChangeSet *_changeSet;
}
- (void)dealloc
{
ASDisplayNodeCAssert(_changeSetBatchUpdateCounter == 0, @"ASChangeSetDataController deallocated in the middle of a batch update.");
}
#pragma mark - Batching (External API)
- (void)beginUpdates
{
ASDisplayNodeAssertMainThread();
if (_changeSetBatchUpdateCounter <= 0) {
_changeSetBatchUpdateCounter = 0;
_changeSet = [[_ASHierarchyChangeSet alloc] initWithOldData:[self itemCountsFromDataSource]];
}
_changeSetBatchUpdateCounter++;
}
- (void)endUpdatesAnimated:(BOOL)animated completion:(void (^)(BOOL))completion
{
ASDisplayNodeAssertMainThread();
_changeSetBatchUpdateCounter--;
// Prevent calling endUpdatesAnimated:completion: in an unbalanced way
NSAssert(_changeSetBatchUpdateCounter >= 0, @"endUpdatesAnimated:completion: called without having a balanced beginUpdates call");
[_changeSet addCompletionHandler:completion];
if (_changeSetBatchUpdateCounter == 0) {
void (^batchCompletion)(BOOL) = _changeSet.completionHandler;
/**
* 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 (!self.initialReloadDataHasBeenCalled) {
if (batchCompletion != nil) {
batchCompletion(YES);
}
_changeSet = nil;
return;
}
[self invalidateDataSourceItemCounts];
// 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;
}
}
ASDataControllerLogEvent(self, @"triggeredUpdate: %@", _changeSet);
[super beginUpdates];
for (_ASHierarchyItemChange *change in [_changeSet itemChangesOfType:_ASHierarchyChangeTypeDelete]) {
[super deleteRowsAtIndexPaths:change.indexPaths withAnimationOptions:change.animationOptions];
}
for (_ASHierarchySectionChange *change in [_changeSet sectionChangesOfType:_ASHierarchyChangeTypeDelete]) {
[super deleteSections:change.indexSet withAnimationOptions:change.animationOptions];
}
for (_ASHierarchySectionChange *change in [_changeSet sectionChangesOfType:_ASHierarchyChangeTypeInsert]) {
[super insertSections:change.indexSet withAnimationOptions:change.animationOptions];
}
for (_ASHierarchyItemChange *change in [_changeSet itemChangesOfType:_ASHierarchyChangeTypeInsert]) {
[super insertRowsAtIndexPaths:change.indexPaths withAnimationOptions:change.animationOptions];
}
#if ASEVENTLOG_ENABLE
NSString *changeSetDescription = ASObjectDescriptionMakeTiny(_changeSet);
batchCompletion = ^(BOOL finished) {
if (batchCompletion != nil) {
batchCompletion(finished);
}
ASDataControllerLogEvent(self, @"finishedUpdate: %@", changeSetDescription);
};
#endif
[super endUpdatesAnimated:animated completion:batchCompletion];
_changeSet = nil;
}
}
- (BOOL)batchUpdating
{
BOOL batchUpdating = (_changeSetBatchUpdateCounter != 0);
// _changeSet must be available during batch update
ASDisplayNodeAssertTrue(batchUpdating == (_changeSet != nil));
return batchUpdating;
}
- (void)waitUntilAllUpdatesAreCommitted
{
ASDisplayNodeAssertMainThread();
if (self.batchUpdating) {
// This assertion will be enabled soon.
// ASDisplayNodeFailAssert(@"Should not call %@ during batch update", NSStringFromSelector(_cmd));
return;
}
[super waitUntilAllUpdatesAreCommitted];
}
#pragma mark - Section Editing (External API)
- (void)insertSections:(NSIndexSet *)sections withAnimationOptions:(ASDataControllerAnimationOptions)animationOptions
{
ASDisplayNodeAssertMainThread();
[self beginUpdates];
[_changeSet insertSections:sections animationOptions:animationOptions];
[self endUpdates];
}
- (void)deleteSections:(NSIndexSet *)sections withAnimationOptions:(ASDataControllerAnimationOptions)animationOptions
{
ASDisplayNodeAssertMainThread();
[self beginUpdates];
[_changeSet deleteSections:sections animationOptions:animationOptions];
[self endUpdates];
}
- (void)reloadSections:(NSIndexSet *)sections withAnimationOptions:(ASDataControllerAnimationOptions)animationOptions
{
ASDisplayNodeAssertMainThread();
[self beginUpdates];
[_changeSet reloadSections:sections animationOptions:animationOptions];
[self endUpdates];
}
- (void)moveSection:(NSInteger)section toSection:(NSInteger)newSection withAnimationOptions:(ASDataControllerAnimationOptions)animationOptions
{
ASDisplayNodeAssertMainThread();
[self beginUpdates];
[_changeSet deleteSections:[NSIndexSet indexSetWithIndex:section] animationOptions:animationOptions];
[_changeSet insertSections:[NSIndexSet indexSetWithIndex:newSection] animationOptions:animationOptions];
[self endUpdates];
}
#pragma mark - Row Editing (External API)
- (void)insertRowsAtIndexPaths:(NSArray *)indexPaths withAnimationOptions:(ASDataControllerAnimationOptions)animationOptions
{
ASDisplayNodeAssertMainThread();
[self beginUpdates];
[_changeSet insertItems:indexPaths animationOptions:animationOptions];
[self endUpdates];
}
- (void)deleteRowsAtIndexPaths:(NSArray *)indexPaths withAnimationOptions:(ASDataControllerAnimationOptions)animationOptions
{
ASDisplayNodeAssertMainThread();
[self beginUpdates];
[_changeSet deleteItems:indexPaths animationOptions:animationOptions];
[self endUpdates];
}
- (void)reloadRowsAtIndexPaths:(NSArray *)indexPaths withAnimationOptions:(ASDataControllerAnimationOptions)animationOptions
{
ASDisplayNodeAssertMainThread();
[self beginUpdates];
[_changeSet reloadItems:indexPaths animationOptions:animationOptions];
[self endUpdates];
}
- (void)moveRowAtIndexPath:(NSIndexPath *)indexPath toIndexPath:(NSIndexPath *)newIndexPath withAnimationOptions:(ASDataControllerAnimationOptions)animationOptions
{
ASDisplayNodeAssertMainThread();
[self beginUpdates];
[_changeSet deleteItems:@[indexPath] animationOptions:animationOptions];
[_changeSet insertItems:@[newIndexPath] animationOptions:animationOptions];
[self endUpdates];
}
@end

View File

@@ -10,7 +10,7 @@
#import <UIKit/UIKit.h>
#import <AsyncDisplayKit/ASChangeSetDataController.h>
#import <AsyncDisplayKit/ASDataController.h>
#import <AsyncDisplayKit/ASDimension.h>
@class ASDisplayNode;
@@ -41,7 +41,7 @@ NS_ASSUME_NONNULL_BEGIN
@end
@interface ASCollectionDataController : ASChangeSetDataController
@interface ASCollectionDataController : ASDataController
- (instancetype)initWithDataSource:(id<ASCollectionDataControllerSource>)dataSource eventLog:(nullable ASEventLog *)eventLog NS_DESIGNATED_INITIALIZER;

View File

@@ -48,6 +48,10 @@ NS_ASSUME_NONNULL_BEGIN
*/
- (nullable NSArray<NSIndexPath *> *)convertIndexPathsToCollectionNode:(nullable NSArray<NSIndexPath *> *)indexPaths;
- (void)beginUpdates;
- (void)endUpdatesAnimated:(BOOL)animated completion:(nullable void (^)(BOOL))completion;
@end
NS_ASSUME_NONNULL_END

View File

@@ -14,6 +14,9 @@
#import <AsyncDisplayKit/ASDimension.h>
#import <AsyncDisplayKit/ASFlowLayoutController.h>
#import <AsyncDisplayKit/ASEventLog.h>
#ifdef __cplusplus
#import <vector>
#endif
NS_ASSUME_NONNULL_BEGIN
@@ -25,6 +28,7 @@ NS_ASSUME_NONNULL_BEGIN
@class ASCellNode;
@class ASDataController;
@class _ASHierarchyChangeSet;
@protocol ASEnvironment;
typedef NSUInteger ASDataControllerAnimationOptions;
@@ -137,6 +141,16 @@ extern NSString * const ASCollectionInvalidUpdateException;
*/
@property (nonatomic, weak) id<ASDataControllerEnvironmentDelegate> environmentDelegate;
#ifdef __cplusplus
/**
* Returns the most recently gathered item counts from the data source. If the counts
* have been invalidated, this synchronously queries the data source and saves the result.
*
* This must be called on the main thread.
*/
- (std::vector<NSInteger>)itemCountsFromDataSource;
#endif
/**
* Returns YES if reloadData has been called at least once. Before this point it is
* important to ignore/suppress some operations. For example, inserting a section
@@ -155,25 +169,7 @@ extern NSString * const ASCollectionInvalidUpdateException;
/** @name Data Updating */
- (void)beginUpdates;
- (void)endUpdates;
- (void)endUpdatesAnimated:(BOOL)animated completion:(void (^ _Nullable)(BOOL))completion;
- (void)insertSections:(NSIndexSet *)sections withAnimationOptions:(ASDataControllerAnimationOptions)animationOptions;
- (void)deleteSections:(NSIndexSet *)sections withAnimationOptions:(ASDataControllerAnimationOptions)animationOptions;
- (void)reloadSections:(NSIndexSet *)sections withAnimationOptions:(ASDataControllerAnimationOptions)animationOptions;
- (void)moveSection:(NSInteger)section toSection:(NSInteger)newSection withAnimationOptions:(ASDataControllerAnimationOptions)animationOptions;
- (void)insertRowsAtIndexPaths:(NSArray<NSIndexPath *> *)indexPaths withAnimationOptions:(ASDataControllerAnimationOptions)animationOptions;
- (void)deleteRowsAtIndexPaths:(NSArray<NSIndexPath *> *)indexPaths withAnimationOptions:(ASDataControllerAnimationOptions)animationOptions;
- (void)reloadRowsAtIndexPaths:(NSArray<NSIndexPath *> *)indexPaths withAnimationOptions:(ASDataControllerAnimationOptions)animationOptions;
- (void)updateWithChangeSet:(_ASHierarchyChangeSet *)changeSet animated:(BOOL)animated;
/**
* Re-measures all loaded nodes in the backing store.
@@ -183,8 +179,6 @@ extern NSString * const ASCollectionInvalidUpdateException;
*/
- (void)relayoutAllNodes;
- (void)moveRowAtIndexPath:(NSIndexPath *)indexPath toIndexPath:(NSIndexPath *)newIndexPath withAnimationOptions:(ASDataControllerAnimationOptions)animationOptions;
- (void)reloadDataWithAnimationOptions:(ASDataControllerAnimationOptions)animationOptions completion:(void (^ _Nullable)())completion;
- (void)reloadDataImmediatelyWithAnimationOptions:(ASDataControllerAnimationOptions)animationOptions;

View File

@@ -10,6 +10,7 @@
#import "ASDataController.h"
#import "_ASHierarchyChangeSet.h"
#import "ASAssert.h"
#import "ASCellNode.h"
#import "ASEnvironmentInternal.h"
@@ -77,7 +78,6 @@ NSString * const ASCollectionInvalidUpdateException = @"ASCollectionInvalidUpdat
if (!(self = [super init])) {
return nil;
}
ASDisplayNodeAssert(![self isMemberOfClass:[ASDataController class]], @"ASDataController is an abstract class and should not be instantiated. Instantiate a subclass instead.");
_dataSource = dataSource;
@@ -580,11 +580,6 @@ NSString * const ASCollectionInvalidUpdateException = @"ASCollectionInvalidUpdat
});
}
- (void)endUpdates
{
[self endUpdatesAnimated:YES completion:nil];
}
- (void)endUpdatesAnimated:(BOOL)animated completion:(void (^)(BOOL))completion
{
LOG(@"endUpdatesWithCompletion - beginning");
@@ -603,6 +598,74 @@ NSString * const ASCollectionInvalidUpdateException = @"ASCollectionInvalidUpdat
});
}
- (void)updateWithChangeSet:(_ASHierarchyChangeSet *)changeSet animated:(BOOL)animated
{
ASDisplayNodeAssertMainThread();
void (^batchCompletion)(BOOL) = changeSet.completionHandler;
/**
* 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 (!self.initialReloadDataHasBeenCalled) {
if (batchCompletion != nil) {
batchCompletion(YES);
}
return;
}
[self invalidateDataSourceItemCounts];
// 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;
}
}
ASDataControllerLogEvent(self, @"triggeredUpdate: %@", changeSet);
[self beginUpdates];
for (_ASHierarchyItemChange *change in [changeSet itemChangesOfType:_ASHierarchyChangeTypeDelete]) {
[self deleteRowsAtIndexPaths:change.indexPaths withAnimationOptions:change.animationOptions];
}
for (_ASHierarchySectionChange *change in [changeSet sectionChangesOfType:_ASHierarchyChangeTypeDelete]) {
[self deleteSections:change.indexSet withAnimationOptions:change.animationOptions];
}
for (_ASHierarchySectionChange *change in [changeSet sectionChangesOfType:_ASHierarchyChangeTypeInsert]) {
[self insertSections:change.indexSet withAnimationOptions:change.animationOptions];
}
for (_ASHierarchyItemChange *change in [changeSet itemChangesOfType:_ASHierarchyChangeTypeInsert]) {
[self insertRowsAtIndexPaths:change.indexPaths withAnimationOptions:change.animationOptions];
}
#if ASEVENTLOG_ENABLE
NSString *changeSetDescription = ASObjectDescriptionMakeTiny(changeSet);
batchCompletion = ^(BOOL finished) {
if (batchCompletion != nil) {
batchCompletion(finished);
}
ASDataControllerLogEvent(self, @"finishedUpdate: %@", changeSetDescription);
};
#endif
[self endUpdatesAnimated:animated completion:batchCompletion];
}
#pragma mark - Section Editing (External API)
- (void)insertSections:(NSIndexSet *)sections withAnimationOptions:(ASDataControllerAnimationOptions)animationOptions
@@ -665,17 +728,6 @@ NSString * const ASCollectionInvalidUpdateException = @"ASCollectionInvalidUpdat
});
}
- (void)reloadSections:(NSIndexSet *)sections withAnimationOptions:(ASDataControllerAnimationOptions)animationOptions
{
ASDisplayNodeAssert(NO, @"ASDataController does not support %@. Call this on ASChangeSetDataController the reload will be broken into delete & insert.", NSStringFromSelector(_cmd));
}
- (void)moveSection:(NSInteger)section toSection:(NSInteger)newSection withAnimationOptions:(ASDataControllerAnimationOptions)animationOptions
{
ASDisplayNodeAssert(NO, @"ASDataController does not support %@. Call this on ASChangeSetDataController and the move will be processed along with the current batch of updates.", NSStringFromSelector(_cmd));
}
#pragma mark - Backing store manipulation optional hooks (Subclass API)
- (void)prepareForReloadDataWithSectionCount:(NSInteger)newSectionCount
@@ -794,11 +846,6 @@ NSString * const ASCollectionInvalidUpdateException = @"ASCollectionInvalidUpdat
});
}
- (void)reloadRowsAtIndexPaths:(NSArray *)indexPaths withAnimationOptions:(ASDataControllerAnimationOptions)animationOptions
{
ASDisplayNodeAssert(NO, @"ASDataController does not support %@. Call this on ASChangeSetDataController and the reload will be broken into delete & insert.", NSStringFromSelector(_cmd));
}
- (void)relayoutAllNodes
{
ASDisplayNodeAssertMainThread();
@@ -843,11 +890,6 @@ NSString * const ASCollectionInvalidUpdateException = @"ASCollectionInvalidUpdat
}
}
- (void)moveRowAtIndexPath:(NSIndexPath *)indexPath toIndexPath:(NSIndexPath *)newIndexPath withAnimationOptions:(ASDataControllerAnimationOptions)animationOptions
{
ASDisplayNodeAssert(NO, @"ASDataController does not support %@. Call this on ASChangeSetDataController and the move will be processed along with the current batch of updates.", NSStringFromSelector(_cmd));
}
#pragma mark - Data Querying (Subclass API)
- (NSArray<NSIndexPath *> *)indexPathsForEditingNodesOfKind:(NSString *)kind

View File

@@ -34,21 +34,6 @@ typedef void (^ASDataControllerCompletionBlock)(NSArray<ASCellNode *> *nodes, NS
*/
- (NSMutableArray *)completedNodesOfKind:(NSString *)kind;
/**
* Ensure that next time `itemCountsFromDataSource` is called, new values are retrieved.
*
* This must be called on the main thread.
*/
- (void)invalidateDataSourceItemCounts;
/**
* Returns the most recently gathered item counts from the data source. If the counts
* have been invalidated, this synchronously queries the data source and saves the result.
*
* This must be called on the main thread.
*/
- (std::vector<NSInteger>)itemCountsFromDataSource;
#pragma mark - Node sizing
/**

View File

@@ -145,6 +145,9 @@ NSString *NSStringFromASHierarchyChangeType(_ASHierarchyChangeType changeType);
- (void)insertItems:(NSArray<NSIndexPath *> *)indexPaths animationOptions:(ASDataControllerAnimationOptions)options;
- (void)deleteItems:(NSArray<NSIndexPath *> *)indexPaths animationOptions:(ASDataControllerAnimationOptions)options;
- (void)reloadItems:(NSArray<NSIndexPath *> *)indexPaths animationOptions:(ASDataControllerAnimationOptions)options;
- (void)moveSection:(NSInteger)section toSection:(NSInteger)newSection animationOptions:(ASDataControllerAnimationOptions)options;
- (void)moveItemAtIndexPath:(NSIndexPath *)indexPath toIndexPath:(NSIndexPath *)newIndexPath animationOptions:(ASDataControllerAnimationOptions)options;
@end
NS_ASSUME_NONNULL_END

View File

@@ -287,6 +287,24 @@ NSString *NSStringFromASHierarchyChangeType(_ASHierarchyChangeType changeType)
[_reloadSectionChanges addObject:change];
}
- (void)moveItemAtIndexPath:(NSIndexPath *)indexPath toIndexPath:(NSIndexPath *)newIndexPath animationOptions:(ASDataControllerAnimationOptions)options
{
/**
* TODO: Proper move implementation.
*/
[self deleteItems:@[ indexPath ] animationOptions:options];
[self insertItems:@[ newIndexPath ] animationOptions:options];
}
- (void)moveSection:(NSInteger)section toSection:(NSInteger)newSection animationOptions:(ASDataControllerAnimationOptions)options
{
/**
* TODO: Proper move implementation.
*/
[self deleteSections:[NSIndexSet indexSetWithIndex:section] animationOptions:options];
[self insertSections:[NSIndexSet indexSetWithIndex:newSection] animationOptions:options];
}
#pragma mark Private
- (BOOL)_ensureNotCompleted

View File

@@ -696,7 +696,7 @@
XCTAssertTrue(toSection < originalSection);
ASTestSectionContext *movedSectionContext = (ASTestSectionContext *)[cn contextForSection:toSection];
XCTAssertNotNil(movedSectionContext);
// ASCollectionView currently uses ASChangeSetDataController which splits a move operation to a pair of delete and insert ones.
// ASCollectionView currently splits a move operation to a pair of delete and insert ones.
// So this movedSectionContext is newly loaded and thus is second generation.
XCTAssertEqual(movedSectionContext.sectionGeneration, 2);
XCTAssertEqual(movedSectionContext.sectionIndex, toSection);

View File

@@ -13,7 +13,6 @@
#import "ASTableView.h"
#import "ASTableViewInternal.h"
#import "ASDisplayNode+Subclasses.h"
#import "ASChangeSetDataController.h"
#import "ASCellNode.h"
#import "ASTableNode.h"
#import "ASTableView+Undeprecated.h"
@@ -24,7 +23,7 @@
#define NumberOfSections 10
#define NumberOfReloadIterations 50
@interface ASTestDataController : ASChangeSetDataController
@interface ASTestDataController : ASDataController
@property (nonatomic) int numberOfAllNodesRelayouts;
@end