diff --git a/AsyncDisplayKit.xcodeproj/project.pbxproj b/AsyncDisplayKit.xcodeproj/project.pbxproj index fb4230ab72..12e50d0543 100644 --- a/AsyncDisplayKit.xcodeproj/project.pbxproj +++ b/AsyncDisplayKit.xcodeproj/project.pbxproj @@ -237,6 +237,14 @@ 9CDC18CC1B910E12004965E2 /* ASLayoutablePrivate.h in Headers */ = {isa = PBXBuildFile; fileRef = 9CDC18CB1B910E12004965E2 /* ASLayoutablePrivate.h */; settings = {ATTRIBUTES = (Public, ); }; }; 9CDC18CD1B910E12004965E2 /* ASLayoutablePrivate.h in Headers */ = {isa = PBXBuildFile; fileRef = 9CDC18CB1B910E12004965E2 /* ASLayoutablePrivate.h */; settings = {ATTRIBUTES = (Public, ); }; }; 9F06E5CD1B4CAF4200F015D8 /* ASCollectionViewTests.m in Sources */ = {isa = PBXBuildFile; fileRef = 9F06E5CC1B4CAF4200F015D8 /* ASCollectionViewTests.m */; }; + AC026B691BD57D6F00BBC17E /* ASChangeSetDataController.h in Headers */ = {isa = PBXBuildFile; fileRef = AC026B671BD57D6F00BBC17E /* ASChangeSetDataController.h */; settings = {ATTRIBUTES = (Public, ); }; }; + AC026B6A1BD57D6F00BBC17E /* ASChangeSetDataController.h in Headers */ = {isa = PBXBuildFile; fileRef = AC026B671BD57D6F00BBC17E /* ASChangeSetDataController.h */; settings = {ATTRIBUTES = (Public, ); }; }; + AC026B6B1BD57D6F00BBC17E /* ASChangeSetDataController.m in Sources */ = {isa = PBXBuildFile; fileRef = AC026B681BD57D6F00BBC17E /* ASChangeSetDataController.m */; }; + AC026B6C1BD57D6F00BBC17E /* ASChangeSetDataController.m in Sources */ = {isa = PBXBuildFile; fileRef = AC026B681BD57D6F00BBC17E /* ASChangeSetDataController.m */; }; + AC026B6F1BD57DBF00BBC17E /* _ASHierarchyChangeSet.h in Headers */ = {isa = PBXBuildFile; fileRef = AC026B6D1BD57DBF00BBC17E /* _ASHierarchyChangeSet.h */; }; + AC026B701BD57DBF00BBC17E /* _ASHierarchyChangeSet.h in Headers */ = {isa = PBXBuildFile; fileRef = AC026B6D1BD57DBF00BBC17E /* _ASHierarchyChangeSet.h */; }; + AC026B711BD57DBF00BBC17E /* _ASHierarchyChangeSet.m in Sources */ = {isa = PBXBuildFile; fileRef = AC026B6E1BD57DBF00BBC17E /* _ASHierarchyChangeSet.m */; }; + AC026B721BD57DBF00BBC17E /* _ASHierarchyChangeSet.m in Sources */ = {isa = PBXBuildFile; fileRef = AC026B6E1BD57DBF00BBC17E /* _ASHierarchyChangeSet.m */; }; AC026B581BD3F61800BBC17E /* ASStaticLayoutSpecSnapshotTests.m in Sources */ = {isa = PBXBuildFile; fileRef = AC026B571BD3F61800BBC17E /* ASStaticLayoutSpecSnapshotTests.m */; }; AC21EC101B3D0BF600C8B19A /* ASStackLayoutDefines.h in Headers */ = {isa = PBXBuildFile; fileRef = AC21EC0F1B3D0BF600C8B19A /* ASStackLayoutDefines.h */; settings = {ATTRIBUTES = (Public, ); }; }; AC3C4A511A1139C100143C57 /* ASCollectionView.h in Headers */ = {isa = PBXBuildFile; fileRef = AC3C4A4F1A1139C100143C57 /* ASCollectionView.h */; settings = {ATTRIBUTES = (Public, ); }; }; @@ -246,6 +254,8 @@ AC47D9451B3BB41900AAEE9D /* ASRelativeSize.h in Headers */ = {isa = PBXBuildFile; fileRef = AC47D9431B3BB41900AAEE9D /* ASRelativeSize.h */; settings = {ATTRIBUTES = (Public, ); }; }; AC47D9461B3BB41900AAEE9D /* ASRelativeSize.mm in Sources */ = {isa = PBXBuildFile; fileRef = AC47D9441B3BB41900AAEE9D /* ASRelativeSize.mm */; }; AC6456091B0A335000CF11B8 /* ASCellNode.m in Sources */ = {isa = PBXBuildFile; fileRef = AC6456071B0A335000CF11B8 /* ASCellNode.m */; }; + AC7A2C171BDE11DF0093FE1A /* ASTableViewInternal.h in Headers */ = {isa = PBXBuildFile; fileRef = AC7A2C161BDE11DF0093FE1A /* ASTableViewInternal.h */; }; + AC7A2C181BDE11DF0093FE1A /* ASTableViewInternal.h in Headers */ = {isa = PBXBuildFile; fileRef = AC7A2C161BDE11DF0093FE1A /* ASTableViewInternal.h */; }; ACC945A91BA9E7A0005E1FB8 /* ASViewController.h in Headers */ = {isa = PBXBuildFile; fileRef = ACC945A81BA9E7A0005E1FB8 /* ASViewController.h */; settings = {ATTRIBUTES = (Public, ); }; }; ACC945AB1BA9E7C1005E1FB8 /* ASViewController.m in Sources */ = {isa = PBXBuildFile; fileRef = ACC945AA1BA9E7C1005E1FB8 /* ASViewController.m */; }; ACF6ED1A1B17843500DA7C62 /* ASBackgroundLayoutSpec.h in Headers */ = {isa = PBXBuildFile; fileRef = ACF6ED011B17843500DA7C62 /* ASBackgroundLayoutSpec.h */; settings = {ATTRIBUTES = (Public, ); }; }; @@ -597,6 +607,10 @@ 9C8221941BA237B80037F19A /* ASStackBaselinePositionedLayout.mm */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.cpp.objcpp; path = ASStackBaselinePositionedLayout.mm; sourceTree = ""; }; 9CDC18CB1B910E12004965E2 /* ASLayoutablePrivate.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; name = ASLayoutablePrivate.h; path = AsyncDisplayKit/Layout/ASLayoutablePrivate.h; sourceTree = ""; }; 9F06E5CC1B4CAF4200F015D8 /* ASCollectionViewTests.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = ASCollectionViewTests.m; sourceTree = ""; }; + AC026B671BD57D6F00BBC17E /* ASChangeSetDataController.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = ASChangeSetDataController.h; sourceTree = ""; }; + AC026B681BD57D6F00BBC17E /* ASChangeSetDataController.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = ASChangeSetDataController.m; sourceTree = ""; }; + AC026B6D1BD57DBF00BBC17E /* _ASHierarchyChangeSet.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = _ASHierarchyChangeSet.h; sourceTree = ""; }; + AC026B6E1BD57DBF00BBC17E /* _ASHierarchyChangeSet.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = _ASHierarchyChangeSet.m; sourceTree = ""; }; AC026B571BD3F61800BBC17E /* ASStaticLayoutSpecSnapshotTests.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = ASStaticLayoutSpecSnapshotTests.m; sourceTree = ""; }; AC21EC0F1B3D0BF600C8B19A /* ASStackLayoutDefines.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; name = ASStackLayoutDefines.h; path = AsyncDisplayKit/Layout/ASStackLayoutDefines.h; sourceTree = ""; }; AC3C4A4F1A1139C100143C57 /* ASCollectionView.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = ASCollectionView.h; sourceTree = ""; }; @@ -605,6 +619,7 @@ AC47D9431B3BB41900AAEE9D /* ASRelativeSize.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; name = ASRelativeSize.h; path = AsyncDisplayKit/Layout/ASRelativeSize.h; sourceTree = ""; }; AC47D9441B3BB41900AAEE9D /* ASRelativeSize.mm */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.cpp.objcpp; name = ASRelativeSize.mm; path = AsyncDisplayKit/Layout/ASRelativeSize.mm; sourceTree = ""; }; AC6456071B0A335000CF11B8 /* ASCellNode.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = ASCellNode.m; sourceTree = ""; }; + AC7A2C161BDE11DF0093FE1A /* ASTableViewInternal.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = ASTableViewInternal.h; sourceTree = ""; }; ACC945A81BA9E7A0005E1FB8 /* ASViewController.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = ASViewController.h; sourceTree = ""; }; ACC945AA1BA9E7C1005E1FB8 /* ASViewController.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = ASViewController.m; sourceTree = ""; }; ACF6ED011B17843500DA7C62 /* ASBackgroundLayoutSpec.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; name = ASBackgroundLayoutSpec.h; path = AsyncDisplayKit/Layout/ASBackgroundLayoutSpec.h; sourceTree = ""; }; @@ -791,6 +806,7 @@ D785F6611A74327E00291744 /* ASScrollNode.m */, 055F1A3219ABD3E3004DAFF1 /* ASTableView.h */, 055F1A3319ABD3E3004DAFF1 /* ASTableView.mm */, + AC7A2C161BDE11DF0093FE1A /* ASTableViewInternal.h */, 0574D5E119C110610097DC25 /* ASTableViewProtocols.h */, 058D09DF195D050800B7D73C /* ASTextNode.h */, 058D09E0195D050800B7D73C /* ASTextNode.mm */, @@ -890,6 +906,8 @@ 205F0E1C1B373A2C007741D0 /* ASCollectionViewLayoutController.mm */, 464052191A3F83C40061C0BA /* ASDataController.h */, 4640521A1A3F83C40061C0BA /* ASDataController.mm */, + AC026B671BD57D6F00BBC17E /* ASChangeSetDataController.h */, + AC026B681BD57D6F00BBC17E /* ASChangeSetDataController.m */, 05A6D05819D0EB64002DD95E /* ASDealloc2MainObject.h */, 05A6D05919D0EB64002DD95E /* ASDealloc2MainObject.m */, 4640521B1A3F83C40061C0BA /* ASFlowLayoutController.h */, @@ -954,6 +972,8 @@ 058D0A01195D050800B7D73C /* Private */ = { isa = PBXGroup; children = ( + AC026B6D1BD57DBF00BBC17E /* _ASHierarchyChangeSet.h */, + AC026B6E1BD57DBF00BBC17E /* _ASHierarchyChangeSet.m */, 9C65A7291BA8EA4D0084DA91 /* ASLayoutOptionsPrivate.h */, 9C8221931BA237B80037F19A /* ASStackBaselinePositionedLayout.h */, 9C8221941BA237B80037F19A /* ASStackBaselinePositionedLayout.mm */, @@ -1075,6 +1095,7 @@ isa = PBXHeadersBuildPhase; buildActionMask = 2147483647; files = ( + AC026B691BD57D6F00BBC17E /* ASChangeSetDataController.h in Headers */, 058D0A71195D05F800B7D73C /* _AS-objc-internal.h in Headers */, 058D0A68195D05EC00B7D73C /* _ASAsyncTransaction.h in Headers */, 058D0A6A195D05EC00B7D73C /* _ASAsyncTransactionContainer+Private.h in Headers */, @@ -1111,6 +1132,7 @@ 058D0A4C195D05CB00B7D73C /* ASDisplayNode+Subclasses.h in Headers */, 058D0A4A195D05CB00B7D73C /* ASDisplayNode.h in Headers */, 058D0A84195D060300B7D73C /* ASDisplayNodeExtraIvars.h in Headers */, + AC7A2C171BDE11DF0093FE1A /* ASTableViewInternal.h in Headers */, 058D0A4D195D05CB00B7D73C /* ASDisplayNodeExtras.h in Headers */, 058D0A7B195D05F900B7D73C /* ASDisplayNodeInternal.h in Headers */, 0587F9BD1A7309ED00AFF0BA /* ASEditableTextNode.h in Headers */, @@ -1133,6 +1155,7 @@ 292C599F1A956527007E5DD6 /* ASLayoutRangeType.h in Headers */, ACF6ED261B17843500DA7C62 /* ASLayoutSpec.h in Headers */, ACF6ED4D1B17847A00DA7C62 /* ASLayoutSpecUtilities.h in Headers */, + AC026B6F1BD57DBF00BBC17E /* _ASHierarchyChangeSet.h in Headers */, 0516FA3D1A15563400B4EBED /* ASLog.h in Headers */, 0442850D1BAA64EC00D16268 /* ASMultidimensionalArrayUtils.h in Headers */, 0516FA401A1563D200B4EBED /* ASMultiplexImageNode.h in Headers */, @@ -1182,6 +1205,7 @@ isa = PBXHeadersBuildPhase; buildActionMask = 2147483647; files = ( + AC026B6A1BD57D6F00BBC17E /* ASChangeSetDataController.h in Headers */, B35062481B010EFD0018CF92 /* _AS-objc-internal.h in Headers */, B350623C1B010EFD0018CF92 /* _ASAsyncTransaction.h in Headers */, B350623E1B010EFD0018CF92 /* _ASAsyncTransactionContainer+Private.h in Headers */, @@ -1202,6 +1226,7 @@ B35062461B010EFD0018CF92 /* ASBasicImageDownloaderInternal.h in Headers */, B35062151B010EFD0018CF92 /* ASBatchContext.h in Headers */, 044285081BAA63FE00D16268 /* ASBatchFetching.h in Headers */, + AC026B701BD57DBF00BBC17E /* _ASHierarchyChangeSet.h in Headers */, B35061F31B010EFD0018CF92 /* ASCellNode.h in Headers */, 34EFC7631B701CBF00AD841F /* ASCenterLayoutSpec.h in Headers */, 18C2ED7F1B9B7DE800F627B3 /* ASCollectionNode.h in Headers */, @@ -1223,6 +1248,7 @@ B350625B1B010F070018CF92 /* ASEqualityHelpers.h in Headers */, B350621B1B010EFD0018CF92 /* ASFlowLayoutController.h in Headers */, B350621D1B010EFD0018CF92 /* ASHighlightOverlayLayer.h in Headers */, + AC7A2C181BDE11DF0093FE1A /* ASTableViewInternal.h in Headers */, B35062531B010EFD0018CF92 /* ASImageNode+CGExtras.h in Headers */, B35062021B010EFD0018CF92 /* ASImageNode.h in Headers */, B350621F1B010EFD0018CF92 /* ASImageProtocols.h in Headers */, @@ -1478,6 +1504,7 @@ 058D0A23195D050800B7D73C /* _ASAsyncTransactionContainer.m in Sources */, 058D0A24195D050800B7D73C /* _ASAsyncTransactionGroup.m in Sources */, 058D0A26195D050800B7D73C /* _ASCoreAnimationExtras.mm in Sources */, + AC026B711BD57DBF00BBC17E /* _ASHierarchyChangeSet.m in Sources */, 058D0A18195D050800B7D73C /* _ASDisplayLayer.mm in Sources */, 058D0A19195D050800B7D73C /* _ASDisplayView.mm in Sources */, 9C55866A1BD549CB00B50E3A /* ASAsciiArtBoxCreator.m in Sources */, @@ -1533,6 +1560,7 @@ ACF6ED501B17847A00DA7C62 /* ASStackPositionedLayout.mm in Sources */, ACF6ED521B17847A00DA7C62 /* ASStackUnpositionedLayout.mm in Sources */, ACF6ED321B17843500DA7C62 /* ASStaticLayoutSpec.mm in Sources */, + AC026B6B1BD57D6F00BBC17E /* ASChangeSetDataController.m in Sources */, 055F1A3519ABD3E3004DAFF1 /* ASTableView.mm in Sources */, 058D0A17195D050800B7D73C /* ASTextNode.mm in Sources */, 058D0A1C195D050800B7D73C /* ASTextNodeCoreTextAdditions.m in Sources */, @@ -1592,6 +1620,7 @@ 9B92C8851BC2EB6E00EE46B2 /* ASCollectionDataController.mm in Sources */, B350623D1B010EFD0018CF92 /* _ASAsyncTransaction.m in Sources */, B35062401B010EFD0018CF92 /* _ASAsyncTransactionContainer.m in Sources */, + AC026B721BD57DBF00BBC17E /* _ASHierarchyChangeSet.m in Sources */, B35062421B010EFD0018CF92 /* _ASAsyncTransactionGroup.m in Sources */, B350624A1B010EFD0018CF92 /* _ASCoreAnimationExtras.mm in Sources */, 2767E9421BB19BD600EA9B77 /* ASViewController.m in Sources */, @@ -1647,6 +1676,7 @@ 34EFC7721B701D0300AD841F /* ASStackLayoutSpec.mm in Sources */, 34EFC7761B701D2A00AD841F /* ASStackPositionedLayout.mm in Sources */, 34EFC7781B701D3100AD841F /* ASStackUnpositionedLayout.mm in Sources */, + AC026B6C1BD57D6F00BBC17E /* ASChangeSetDataController.m in Sources */, 34EFC7741B701D0A00AD841F /* ASStaticLayoutSpec.mm in Sources */, B350620B1B010EFD0018CF92 /* ASTableView.mm in Sources */, B350620E1B010EFD0018CF92 /* ASTextNode.mm in Sources */, diff --git a/AsyncDisplayKit/ASTableView.mm b/AsyncDisplayKit/ASTableView.mm index 56a5e41b82..6729242e9c 100644 --- a/AsyncDisplayKit/ASTableView.mm +++ b/AsyncDisplayKit/ASTableView.mm @@ -7,9 +7,10 @@ */ #import "ASTableView.h" +#import "ASTableViewInternal.h" #import "ASAssert.h" -#import "ASDataController.h" +#import "ASChangeSetDataController.h" #import "ASCollectionViewLayoutController.h" #import "ASLayoutController.h" #import "ASRangeController.h" @@ -155,7 +156,6 @@ static BOOL _isInterceptedSelector(SEL sel) _ASTableViewProxy *_proxyDataSource; _ASTableViewProxy *_proxyDelegate; - ASDataController *_dataController; ASFlowLayoutController *_layoutController; ASRangeController *_rangeController; @@ -174,6 +174,7 @@ static BOOL _isInterceptedSelector(SEL sel) } @property (atomic, assign) BOOL asyncDataSourceLocked; +@property (nonatomic, retain, readwrite) ASDataController *dataController; @end @@ -199,24 +200,29 @@ void ASPerformBlockWithoutAnimation(BOOL withoutAnimation, void (^block)()) { } } ++ (Class)dataControllerClass +{ + return [ASChangeSetDataController class]; +} + #pragma mark - #pragma mark Lifecycle -- (void)configureWithAsyncDataFetching:(BOOL)asyncDataFetchingEnabled +- (void)configureWithDataControllerClass:(Class)dataControllerClass asyncDataFetching:(BOOL)asyncDataFetching { _layoutController = [[ASFlowLayoutController alloc] initWithScrollOption:ASFlowLayoutDirectionVertical]; _rangeController = [[ASRangeController alloc] init]; _rangeController.layoutController = _layoutController; _rangeController.delegate = self; - - _dataController = [[ASDataController alloc] initWithAsyncDataFetching:asyncDataFetchingEnabled]; + + _dataController = [[dataControllerClass alloc] initWithAsyncDataFetching:asyncDataFetching]; _dataController.dataSource = self; _dataController.delegate = _rangeController; _layoutController.dataSource = _dataController; - _asyncDataFetchingEnabled = asyncDataFetchingEnabled; + _asyncDataFetchingEnabled = asyncDataFetching; _asyncDataSourceLocked = NO; _leadingScreensForBatching = 1.0; @@ -236,6 +242,11 @@ void ASPerformBlockWithoutAnimation(BOOL withoutAnimation, void (^block)()) { } - (instancetype)initWithFrame:(CGRect)frame style:(UITableViewStyle)style asyncDataFetching:(BOOL)asyncDataFetchingEnabled +{ + return [self initWithFrame:frame style:style dataControllerClass:[self.class dataControllerClass] asyncDataFetching:asyncDataFetchingEnabled]; +} + +- (instancetype)initWithFrame:(CGRect)frame style:(UITableViewStyle)style dataControllerClass:(Class)dataControllerClass asyncDataFetching:(BOOL)asyncDataFetchingEnabled { if (!(self = [super initWithFrame:frame style:style])) return nil; @@ -244,8 +255,8 @@ void ASPerformBlockWithoutAnimation(BOOL withoutAnimation, void (^block)()) { // https://github.com/facebook/AsyncDisplayKit/issues/385 asyncDataFetchingEnabled = NO; - [self configureWithAsyncDataFetching:asyncDataFetchingEnabled]; - + [self configureWithDataControllerClass:dataControllerClass asyncDataFetching:asyncDataFetchingEnabled]; + return self; } @@ -254,7 +265,7 @@ void ASPerformBlockWithoutAnimation(BOOL withoutAnimation, void (^block)()) { if (!(self = [super initWithCoder:aDecoder])) return nil; - [self configureWithAsyncDataFetching:NO]; + [self configureWithDataControllerClass:[self.class dataControllerClass] asyncDataFetching:NO]; return self; } @@ -417,7 +428,7 @@ void ASPerformBlockWithoutAnimation(BOOL withoutAnimation, void (^block)()) { } } - // To ensure _maxWidthForNodesConstrainedSize is up-to-date for every usage, this call to super must be done last + // To ensure _nodesConstrainedWidth is up-to-date for every usage, this call to super must be done last [super layoutSubviews]; } @@ -895,7 +906,7 @@ void ASPerformBlockWithoutAnimation(BOOL withoutAnimation, void (^block)()) { // Normally the content view width equals to the constrained size width (which equals to the table view width). // If there is a mismatch between these values, for example after the table view entered or left editing mode, // content view width is preferred and used to re-measure the cell node. - if (!_ignoreNodesConstrainedWidthChange && contentViewWidth != constrainedSize.max.width) { + if (contentViewWidth != constrainedSize.max.width) { constrainedSize.min.width = contentViewWidth; constrainedSize.max.width = contentViewWidth; diff --git a/AsyncDisplayKit/ASTableViewInternal.h b/AsyncDisplayKit/ASTableViewInternal.h new file mode 100644 index 0000000000..af940837e2 --- /dev/null +++ b/AsyncDisplayKit/ASTableViewInternal.h @@ -0,0 +1,31 @@ +// +// ASTableViewInternal.h +// AsyncDisplayKit +// +// Created by Huy Nguyen on 26/10/15. +// Copyright (c) 2015 Facebook. All rights reserved. +// + +#import "ASTableView.h" + +@class ASDataController; + +@interface ASTableView (Internal) + +@property (nonatomic, retain, readonly) ASDataController *dataController; + +/** + * Initializer. + * + * @param frame A rectangle specifying the initial location and size of the table view in its superview’€™s coordinates. + * The frame of the table view changes as table cells are added and deleted. + * + * @param style A constant that specifies the style of the table view. See UITableViewStyle for descriptions of valid constants. + * + * @param dataControllerClass A controller class injected to and used to create a data controller for the table view. + * + * @param asyncDataFetchingEnabled This option is reserved for future use, and currently a no-op. + */ +- (instancetype)initWithFrame:(CGRect)frame style:(UITableViewStyle)style dataControllerClass:(Class)dataControllerClass asyncDataFetching:(BOOL)asyncDataFetchingEnabled; + +@end diff --git a/AsyncDisplayKit/Details/ASChangeSetDataController.h b/AsyncDisplayKit/Details/ASChangeSetDataController.h new file mode 100644 index 0000000000..cc385a148f --- /dev/null +++ b/AsyncDisplayKit/Details/ASChangeSetDataController.h @@ -0,0 +1,23 @@ +// +// ASChangeSetDataController.h +// AsyncDisplayKit +// +// Created by Huy Nguyen on 19/10/15. +// Copyright (c) 2015 Facebook. All rights reserved. +// + +#import + +/** + * @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 diff --git a/AsyncDisplayKit/Details/ASChangeSetDataController.m b/AsyncDisplayKit/Details/ASChangeSetDataController.m new file mode 100644 index 0000000000..3e2c2ce118 --- /dev/null +++ b/AsyncDisplayKit/Details/ASChangeSetDataController.m @@ -0,0 +1,179 @@ +// +// ASChangeSetDataController.m +// AsyncDisplayKit +// +// Created by Huy Nguyen on 19/10/15. +// Copyright (c) 2015 Facebook. All rights reserved. +// + +#import "ASChangeSetDataController.h" +#import "ASInternalHelpers.h" +#import "_ASHierarchyChangeSet.h" +#import "ASAssert.h" + +@interface ASChangeSetDataController () + +@property (nonatomic, assign) NSUInteger batchUpdateCounter; +@property (nonatomic, strong) _ASHierarchyChangeSet *changeSet; + +@end + +@implementation ASChangeSetDataController + +- (instancetype)initWithAsyncDataFetching:(BOOL)asyncDataFetchingEnabled +{ + if (!(self = [super initWithAsyncDataFetching:asyncDataFetchingEnabled])) { + return nil; + } + + _batchUpdateCounter = 0; + + return self; +} + +#pragma mark - Batching (External API) + +- (void)beginUpdates +{ + ASDisplayNodeAssertMainThread(); + if (_batchUpdateCounter == 0) { + _changeSet = [_ASHierarchyChangeSet new]; + } + _batchUpdateCounter++; +} + +- (void)endUpdatesAnimated:(BOOL)animated completion:(void (^)(BOOL))completion +{ + ASDisplayNodeAssertMainThread(); + _batchUpdateCounter--; + + if (_batchUpdateCounter == 0) { + [_changeSet markCompleted]; + + [super beginUpdates]; + + for (_ASHierarchySectionChange *change in [_changeSet sectionChangesOfType:_ASHierarchyChangeTypeReload]) { + [super reloadSections:change.indexSet withAnimationOptions:change.animationOptions]; + } + + for (_ASHierarchyItemChange *change in [_changeSet itemChangesOfType:_ASHierarchyChangeTypeReload]) { + [super reloadRowsAtIndexPaths:change.indexPaths withAnimationOptions:change.animationOptions]; + } + + 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]; + } + + [super endUpdatesAnimated:animated completion:completion]; + + _changeSet = nil; + } +} + +- (BOOL)batchUpdating +{ + BOOL batchUpdating = (_batchUpdateCounter != 0); + // _changeSet must be available during batch update + ASDisplayNodeAssertTrue(batchUpdating == (_changeSet != nil)); + return batchUpdating; +} + +#pragma mark - Section Editing (External API) + +- (void)insertSections:(NSIndexSet *)sections withAnimationOptions:(ASDataControllerAnimationOptions)animationOptions +{ + ASDisplayNodeAssertMainThread(); + if ([self batchUpdating]) { + [_changeSet insertSections:sections animationOptions:animationOptions]; + } else { + [super insertSections:sections withAnimationOptions:animationOptions]; + } +} + +- (void)deleteSections:(NSIndexSet *)sections withAnimationOptions:(ASDataControllerAnimationOptions)animationOptions +{ + ASDisplayNodeAssertMainThread(); + if ([self batchUpdating]) { + [_changeSet deleteSections:sections animationOptions:animationOptions]; + } else { + [super deleteSections:sections withAnimationOptions:animationOptions]; + } +} + +- (void)reloadSections:(NSIndexSet *)sections withAnimationOptions:(ASDataControllerAnimationOptions)animationOptions +{ + ASDisplayNodeAssertMainThread(); + if ([self batchUpdating]) { + [_changeSet reloadSections:sections animationOptions:animationOptions]; + } else { + [super reloadSections:sections withAnimationOptions:animationOptions]; + } +} + +- (void)moveSection:(NSInteger)section toSection:(NSInteger)newSection withAnimationOptions:(ASDataControllerAnimationOptions)animationOptions +{ + ASDisplayNodeAssertMainThread(); + if ([self batchUpdating]) { + [_changeSet deleteSections:[NSIndexSet indexSetWithIndex:section] animationOptions:animationOptions]; + [_changeSet insertSections:[NSIndexSet indexSetWithIndex:newSection] animationOptions:animationOptions]; + } else { + [super moveSection:section toSection:newSection withAnimationOptions:animationOptions]; + } +} + +#pragma mark - Row Editing (External API) + +- (void)insertRowsAtIndexPaths:(NSArray *)indexPaths withAnimationOptions:(ASDataControllerAnimationOptions)animationOptions +{ + ASDisplayNodeAssertMainThread(); + if ([self batchUpdating]) { + [_changeSet insertItems:indexPaths animationOptions:animationOptions]; + } else { + [super insertRowsAtIndexPaths:indexPaths withAnimationOptions:animationOptions]; + } +} + +- (void)deleteRowsAtIndexPaths:(NSArray *)indexPaths withAnimationOptions:(ASDataControllerAnimationOptions)animationOptions +{ + ASDisplayNodeAssertMainThread(); + if ([self batchUpdating]) { + [_changeSet deleteItems:indexPaths animationOptions:animationOptions]; + } else { + [super deleteRowsAtIndexPaths:indexPaths withAnimationOptions:animationOptions]; + } +} + +- (void)reloadRowsAtIndexPaths:(NSArray *)indexPaths withAnimationOptions:(ASDataControllerAnimationOptions)animationOptions +{ + ASDisplayNodeAssertMainThread(); + if ([self batchUpdating]) { + [_changeSet reloadItems:indexPaths animationOptions:animationOptions]; + } else { + [super reloadRowsAtIndexPaths:indexPaths withAnimationOptions:animationOptions]; + } +} + +- (void)moveRowAtIndexPath:(NSIndexPath *)indexPath toIndexPath:(NSIndexPath *)newIndexPath withAnimationOptions:(ASDataControllerAnimationOptions)animationOptions +{ + ASDisplayNodeAssertMainThread(); + if ([self batchUpdating]) { + [_changeSet deleteItems:@[indexPath] animationOptions:animationOptions]; + [_changeSet insertItems:@[newIndexPath] animationOptions:animationOptions]; + } else { + [super moveRowAtIndexPath:indexPath toIndexPath:newIndexPath withAnimationOptions:animationOptions]; + } +} + +@end diff --git a/AsyncDisplayKit/Details/ASCollectionDataController.h b/AsyncDisplayKit/Details/ASCollectionDataController.h index 54ddfbb810..7c66ff41fb 100644 --- a/AsyncDisplayKit/Details/ASCollectionDataController.h +++ b/AsyncDisplayKit/Details/ASCollectionDataController.h @@ -8,7 +8,7 @@ #import -#import +#import #import @class ASDisplayNode; @@ -32,7 +32,7 @@ @end -@interface ASCollectionDataController : ASDataController +@interface ASCollectionDataController : ASChangeSetDataController - (ASCellNode *)supplementaryNodeOfKind:(NSString *)kind atIndexPath:(NSIndexPath *)indexPath; diff --git a/AsyncDisplayKit/Details/ASDataController.h b/AsyncDisplayKit/Details/ASDataController.h index 1dde3647b5..7847892408 100644 --- a/AsyncDisplayKit/Details/ASDataController.h +++ b/AsyncDisplayKit/Details/ASDataController.h @@ -97,7 +97,7 @@ typedef NSUInteger ASDataControllerAnimationOptions; * * All operations are asynchronous and thread safe. You can call it from background thread (it is recommendated) and the data * will be updated asynchronously. The dataSource must be updated to reflect the changes before these methods has been called. - * For each data updatin, the corresponding methods in delegate will be called. + * For each data updating, the corresponding methods in delegate will be called. */ @protocol ASFlowLayoutControllerDataSource; @interface ASDataController : ASDealloc2MainObject diff --git a/AsyncDisplayKit/Private/ASInternalHelpers.h b/AsyncDisplayKit/Private/ASInternalHelpers.h index 6a9d74e97e..00bed4b1d6 100644 --- a/AsyncDisplayKit/Private/ASInternalHelpers.h +++ b/AsyncDisplayKit/Private/ASInternalHelpers.h @@ -26,3 +26,7 @@ CGFloat ASCeilPixelValue(CGFloat f); CGFloat ASRoundPixelValue(CGFloat f); ASDISPLAYNODE_EXTERN_C_END + +@interface NSIndexPath (ASInverseComparison) +- (NSComparisonResult)asdk_inverseCompare:(NSIndexPath *)otherIndexPath; +@end diff --git a/AsyncDisplayKit/Private/ASInternalHelpers.mm b/AsyncDisplayKit/Private/ASInternalHelpers.mm index f57450d39e..d2337a44b8 100644 --- a/AsyncDisplayKit/Private/ASInternalHelpers.mm +++ b/AsyncDisplayKit/Private/ASInternalHelpers.mm @@ -70,3 +70,12 @@ CGFloat ASRoundPixelValue(CGFloat f) { return roundf(f * ASScreenScale()) / ASScreenScale(); } + +@implementation NSIndexPath (ASInverseComparison) + +- (NSComparisonResult)asdk_inverseCompare:(NSIndexPath *)otherIndexPath +{ + return [otherIndexPath compare:self]; +} + +@end diff --git a/AsyncDisplayKit/Private/_ASHierarchyChangeSet.h b/AsyncDisplayKit/Private/_ASHierarchyChangeSet.h new file mode 100644 index 0000000000..fa67235957 --- /dev/null +++ b/AsyncDisplayKit/Private/_ASHierarchyChangeSet.h @@ -0,0 +1,72 @@ +// +// _ASHierarchyChangeSet.h +// AsyncDisplayKit +// +// Created by Adlai Holler on 9/29/15. +// Copyright © 2015 Facebook. All rights reserved. +// + +#import +#import "ASDataController.h" + +typedef NS_ENUM(NSInteger, _ASHierarchyChangeType) { + _ASHierarchyChangeTypeReload, + _ASHierarchyChangeTypeDelete, + _ASHierarchyChangeTypeInsert +}; + +@interface _ASHierarchySectionChange : NSObject + +// FIXME: Generalize this to `changeMetadata` dict? +@property (nonatomic, readonly) ASDataControllerAnimationOptions animationOptions; + +@property (nonatomic, strong, readonly) NSIndexSet *indexSet; +@property (nonatomic, readonly) _ASHierarchyChangeType changeType; +@end + +@interface _ASHierarchyItemChange : NSObject +@property (nonatomic, readonly) ASDataControllerAnimationOptions animationOptions; + +/// Index paths are sorted descending for changeType .Delete, ascending otherwise +@property (nonatomic, strong, readonly) NSArray *indexPaths; +@property (nonatomic, readonly) _ASHierarchyChangeType changeType; +@end + +@interface _ASHierarchyChangeSet : NSObject + +@property (nonatomic, strong, readonly) NSIndexSet *deletedSections; +@property (nonatomic, strong, readonly) NSIndexSet *insertedSections; +@property (nonatomic, strong, readonly) NSIndexSet *reloadedSections; +@property (nonatomic, strong, readonly) NSArray *insertedItems; +@property (nonatomic, strong, readonly) NSArray *deletedItems; +@property (nonatomic, strong, readonly) NSArray *reloadedItems; + +@property (nonatomic, readonly) BOOL completed; + +/// Call this once the change set has been constructed to prevent future modifications to the changeset. Calling this more than once is a programmer error. +- (void)markCompleted; + +/** + @abstract Return sorted changes of the given type, grouped by animation options. + + Items deleted from deleted sections are not reported. + Items inserted into inserted sections are not reported. + Items reloaded in reloaded sections are not reported. + + The safe order for processing change groups is: + - Reloaded sections & reloaded items + - Deleted items, descending order + - Deleted sections, descending order + - Inserted sections, ascending order + - Inserted items, ascending order + */ +- (NSArray /*<_ASHierarchySectionChange *>*/ *)sectionChangesOfType:(_ASHierarchyChangeType)changeType; +- (NSArray /*<_ASHierarchyItemChange *>*/ *)itemChangesOfType:(_ASHierarchyChangeType)changeType; + +- (void)deleteSections:(NSIndexSet *)sections animationOptions:(ASDataControllerAnimationOptions)options; +- (void)insertSections:(NSIndexSet *)sections animationOptions:(ASDataControllerAnimationOptions)options; +- (void)reloadSections:(NSIndexSet *)sections animationOptions:(ASDataControllerAnimationOptions)options; +- (void)insertItems:(NSArray *)indexPaths animationOptions:(ASDataControllerAnimationOptions)options; +- (void)deleteItems:(NSArray *)indexPaths animationOptions:(ASDataControllerAnimationOptions)options; +- (void)reloadItems:(NSArray *)indexPaths animationOptions:(ASDataControllerAnimationOptions)options; +@end diff --git a/AsyncDisplayKit/Private/_ASHierarchyChangeSet.m b/AsyncDisplayKit/Private/_ASHierarchyChangeSet.m new file mode 100644 index 0000000000..d7c6ecab0b --- /dev/null +++ b/AsyncDisplayKit/Private/_ASHierarchyChangeSet.m @@ -0,0 +1,337 @@ +// +// _ASHierarchyChangeSet.m +// AsyncDisplayKit +// +// Created by Adlai Holler on 9/29/15. +// Copyright © 2015 Facebook. All rights reserved. +// + +#import "_ASHierarchyChangeSet.h" +#import "ASInternalHelpers.h" + +@interface _ASHierarchySectionChange () +- (instancetype)initWithChangeType:(_ASHierarchyChangeType)changeType indexSet:(NSIndexSet *)indexSet animationOptions:(ASDataControllerAnimationOptions)animationOptions; + +/** + On return `changes` is sorted according to the change type with changes coalesced by animationOptions + Assumes: `changes` is [_ASHierarchySectionChange] all with the same changeType + */ ++ (void)sortAndCoalesceChanges:(NSMutableArray *)changes; +@end + +@interface _ASHierarchyItemChange () +- (instancetype)initWithChangeType:(_ASHierarchyChangeType)changeType indexPaths:(NSArray *)indexPaths animationOptions:(ASDataControllerAnimationOptions)animationOptions presorted:(BOOL)presorted; + +/** + On return `changes` is sorted according to the change type with changes coalesced by animationOptions + Assumes: `changes` is [_ASHierarchyItemChange] all with the same changeType + */ ++ (void)sortAndCoalesceChanges:(NSMutableArray *)changes ignoringChangesInSections:(NSIndexSet *)sections; +@end + +@interface _ASHierarchyChangeSet () + +@property (nonatomic, strong, readonly) NSMutableArray *insertItemChanges; +@property (nonatomic, strong, readonly) NSMutableArray *deleteItemChanges; +@property (nonatomic, strong, readonly) NSMutableArray *reloadItemChanges; +@property (nonatomic, strong, readonly) NSMutableArray *insertSectionChanges; +@property (nonatomic, strong, readonly) NSMutableArray *deleteSectionChanges; +@property (nonatomic, strong, readonly) NSMutableArray *reloadSectionChanges; + +@end + +@implementation _ASHierarchyChangeSet { + NSMutableIndexSet *_deletedSections; + NSMutableIndexSet *_insertedSections; + NSMutableIndexSet *_reloadedSections; + NSMutableArray *_insertedItems; + NSMutableArray *_deletedItems; + NSMutableArray *_reloadedItems; +} + +- (instancetype)init +{ + self = [super init]; + if (self) { + _deletedSections = [NSMutableIndexSet new]; + _insertedSections = [NSMutableIndexSet new]; + _reloadedSections = [NSMutableIndexSet new]; + + _deletedItems = [NSMutableArray new]; + _insertedItems = [NSMutableArray new]; + _reloadedItems = [NSMutableArray new]; + + _insertItemChanges = [NSMutableArray new]; + _deleteItemChanges = [NSMutableArray new]; + _reloadItemChanges = [NSMutableArray new]; + _insertSectionChanges = [NSMutableArray new]; + _deleteSectionChanges = [NSMutableArray new]; + _reloadSectionChanges = [NSMutableArray new]; + } + return self; +} + +#pragma mark External API + +- (void)markCompleted +{ + NSAssert(!_completed, @"Attempt to mark already-completed changeset as completed."); + _completed = YES; + [self _sortAndCoalesceChangeArrays]; +} + +- (NSArray *)sectionChangesOfType:(_ASHierarchyChangeType)changeType +{ + [self _ensureCompleted]; + switch (changeType) { + case _ASHierarchyChangeTypeInsert: + return _insertSectionChanges; + case _ASHierarchyChangeTypeReload: + return _reloadSectionChanges; + case _ASHierarchyChangeTypeDelete: + return _deleteSectionChanges; + default: + NSAssert(NO, @"Request for section changes with invalid type: %lu", changeType); + } +} + +- (NSArray *)itemChangesOfType:(_ASHierarchyChangeType)changeType +{ + [self _ensureCompleted]; + switch (changeType) { + case _ASHierarchyChangeTypeInsert: + return _insertItemChanges; + case _ASHierarchyChangeTypeReload: + return _reloadItemChanges; + case _ASHierarchyChangeTypeDelete: + return _deleteItemChanges; + default: + NSAssert(NO, @"Request for item changes with invalid type: %lu", changeType); + } +} + +- (void)deleteItems:(NSArray *)indexPaths animationOptions:(ASDataControllerAnimationOptions)options +{ + [self _ensureNotCompleted]; + _ASHierarchyItemChange *change = [[_ASHierarchyItemChange alloc] initWithChangeType:_ASHierarchyChangeTypeDelete indexPaths:indexPaths animationOptions:options presorted:NO]; + [_deleteItemChanges addObject:change]; +} + +- (void)deleteSections:(NSIndexSet *)sections animationOptions:(ASDataControllerAnimationOptions)options +{ + [self _ensureNotCompleted]; + _ASHierarchySectionChange *change = [[_ASHierarchySectionChange alloc] initWithChangeType:_ASHierarchyChangeTypeDelete indexSet:sections animationOptions:options]; + [_deleteSectionChanges addObject:change]; +} + +- (void)insertItems:(NSArray *)indexPaths animationOptions:(ASDataControllerAnimationOptions)options +{ + [self _ensureNotCompleted]; + _ASHierarchyItemChange *change = [[_ASHierarchyItemChange alloc] initWithChangeType:_ASHierarchyChangeTypeInsert indexPaths:indexPaths animationOptions:options presorted:NO]; + [_insertItemChanges addObject:change]; +} + +- (void)insertSections:(NSIndexSet *)sections animationOptions:(ASDataControllerAnimationOptions)options +{ + [self _ensureNotCompleted]; + _ASHierarchySectionChange *change = [[_ASHierarchySectionChange alloc] initWithChangeType:_ASHierarchyChangeTypeInsert indexSet:sections animationOptions:options]; + [_insertSectionChanges addObject:change]; +} + +- (void)reloadItems:(NSArray *)indexPaths animationOptions:(ASDataControllerAnimationOptions)options +{ + [self _ensureNotCompleted]; + _ASHierarchyItemChange *change = [[_ASHierarchyItemChange alloc] initWithChangeType:_ASHierarchyChangeTypeReload indexPaths:indexPaths animationOptions:options presorted:NO]; + [_reloadItemChanges addObject:change]; +} + +- (void)reloadSections:(NSIndexSet *)sections animationOptions:(ASDataControllerAnimationOptions)options +{ + [self _ensureNotCompleted]; + _ASHierarchySectionChange *change = [[_ASHierarchySectionChange alloc] initWithChangeType:_ASHierarchyChangeTypeReload indexSet:sections animationOptions:options]; + [_reloadSectionChanges addObject:change]; +} + +#pragma mark Private + +- (BOOL)_ensureNotCompleted +{ + NSAssert(!_completed, @"Attempt to modify completed changeset %@", self); + return !_completed; +} + +- (BOOL)_ensureCompleted +{ + NSAssert(_completed, @"Attempt to process incomplete changeset %@", self); + return _completed; +} + +- (void)_sortAndCoalesceChangeArrays +{ + @autoreleasepool { + [_ASHierarchySectionChange sortAndCoalesceChanges:_deleteSectionChanges]; + [_ASHierarchySectionChange sortAndCoalesceChanges:_insertSectionChanges]; + [_ASHierarchySectionChange sortAndCoalesceChanges:_reloadSectionChanges]; + [_ASHierarchyItemChange sortAndCoalesceChanges:_deleteItemChanges ignoringChangesInSections:_deletedSections]; + [_ASHierarchyItemChange sortAndCoalesceChanges:_reloadItemChanges ignoringChangesInSections:_reloadedSections]; + [_ASHierarchyItemChange sortAndCoalesceChanges:_insertItemChanges ignoringChangesInSections:_insertedSections]; + } +} + +@end + +@implementation _ASHierarchySectionChange + +- (instancetype)initWithChangeType:(_ASHierarchyChangeType)changeType indexSet:(NSIndexSet *)indexSet animationOptions:(ASDataControllerAnimationOptions)animationOptions +{ + self = [super init]; + if (self) { + _changeType = changeType; + _indexSet = indexSet; + _animationOptions = animationOptions; + } + return self; +} + ++ (void)sortAndCoalesceChanges:(NSMutableArray *)changes +{ + if (changes.count < 1) { + return; + } + + _ASHierarchyChangeType type = [changes.firstObject changeType]; + + // Lookup table [Int: AnimationOptions] + NSMutableDictionary *animationOptions = [NSMutableDictionary new]; + + // All changed indexes, sorted + NSMutableIndexSet *allIndexes = [NSMutableIndexSet new]; + + for (_ASHierarchySectionChange *change in changes) { + [change.indexSet enumerateIndexesUsingBlock:^(NSUInteger idx, __unused BOOL *stop) { + animationOptions[@(idx)] = @(change.animationOptions); + }]; + [allIndexes addIndexes:change.indexSet]; + } + + // Create new changes by grouping sorted changes by animation option + NSMutableArray *result = [NSMutableArray new]; + + __block ASDataControllerAnimationOptions currentOptions = 0; + __block NSMutableIndexSet *currentIndexes = nil; + NSUInteger lastIndex = allIndexes.lastIndex; + + NSEnumerationOptions options = type == _ASHierarchyChangeTypeDelete ? NSEnumerationReverse : kNilOptions; + [allIndexes enumerateIndexesWithOptions:options usingBlock:^(NSUInteger idx, __unused BOOL * stop) { + ASDataControllerAnimationOptions options = [animationOptions[@(idx)] integerValue]; + BOOL endingCurrentGroup = NO; + + if (currentIndexes == nil) { + // Starting a new group + currentIndexes = [NSMutableIndexSet indexSetWithIndex:idx]; + currentOptions = options; + } else if (options == currentOptions) { + // Continuing the current group + [currentIndexes addIndex:idx]; + } else { + endingCurrentGroup = YES; + } + + BOOL endingLastGroup = (currentIndexes != nil && lastIndex == idx); + + if (endingCurrentGroup || endingLastGroup) { + _ASHierarchySectionChange *change = [[_ASHierarchySectionChange alloc] initWithChangeType:type indexSet:currentIndexes animationOptions:currentOptions]; + [result addObject:change]; + currentOptions = 0; + currentIndexes = nil; + } + }]; + + [changes setArray:result]; +} + +@end + +@implementation _ASHierarchyItemChange + +- (instancetype)initWithChangeType:(_ASHierarchyChangeType)changeType indexPaths:(NSArray *)indexPaths animationOptions:(ASDataControllerAnimationOptions)animationOptions presorted:(BOOL)presorted +{ + self = [super init]; + if (self) { + _changeType = changeType; + if (presorted) { + _indexPaths = indexPaths; + } else { + SEL sorting = changeType == _ASHierarchyChangeTypeDelete ? @selector(asdk_inverseCompare:) : @selector(compare:); + _indexPaths = [indexPaths sortedArrayUsingSelector:sorting]; + } + _animationOptions = animationOptions; + } + return self; +} + ++ (void)sortAndCoalesceChanges:(NSMutableArray *)changes ignoringChangesInSections:(NSIndexSet *)sections +{ + if (changes.count < 1) { + return; + } + + _ASHierarchyChangeType type = [changes.firstObject changeType]; + + // Lookup table [NSIndexPath: AnimationOptions] + NSMutableDictionary *animationOptions = [NSMutableDictionary new]; + + // All changed index paths, sorted + NSMutableArray *allIndexPaths = [NSMutableArray new]; + + NSPredicate *indexPathInValidSection = [NSPredicate predicateWithBlock:^BOOL(NSIndexPath *indexPath, __unused NSDictionary *_) { + return ![sections containsIndex:indexPath.section]; + }]; + for (_ASHierarchyItemChange *change in changes) { + for (NSIndexPath *indexPath in change.indexPaths) { + if ([indexPathInValidSection evaluateWithObject:indexPath]) { + animationOptions[indexPath] = @(change.animationOptions); + [allIndexPaths addObject:indexPath]; + } + } + } + + SEL sorting = type == _ASHierarchyChangeTypeDelete ? @selector(asdk_inverseCompare:) : @selector(compare:); + [allIndexPaths sortUsingSelector:sorting]; + + // Create new changes by grouping sorted changes by animation option + NSMutableArray *result = [NSMutableArray new]; + + ASDataControllerAnimationOptions currentOptions = 0; + NSMutableArray *currentIndexPaths = nil; + NSIndexPath *lastIndexPath = allIndexPaths.lastObject; + + for (NSIndexPath *indexPath in allIndexPaths) { + ASDataControllerAnimationOptions options = [animationOptions[indexPath] integerValue]; + BOOL endingCurrentGroup = NO; + + if (currentIndexPaths == nil) { + // Starting a new group + currentIndexPaths = [NSMutableArray arrayWithObject:indexPath]; + currentOptions = options; + } else if (options == currentOptions) { + // Continuing the current group + [currentIndexPaths addObject:indexPath]; + } else { + endingCurrentGroup = YES; + } + + BOOL endingLastGroup = (currentIndexPaths != nil && (NSOrderedSame == [lastIndexPath compare:indexPath])); + + if (endingCurrentGroup || endingLastGroup) { + _ASHierarchyItemChange *change = [[_ASHierarchyItemChange alloc] initWithChangeType:type indexPaths:currentIndexPaths animationOptions:currentOptions presorted:YES]; + [result addObject:change]; + currentOptions = 0; + currentIndexPaths = nil; + } + } + + [changes setArray:result]; +} + +@end diff --git a/AsyncDisplayKitTests/ASTableViewTests.m b/AsyncDisplayKitTests/ASTableViewTests.m index fa3d826d12..b4e137bcd7 100644 --- a/AsyncDisplayKitTests/ASTableViewTests.m +++ b/AsyncDisplayKitTests/ASTableViewTests.m @@ -9,18 +9,44 @@ #import #import "ASTableView.h" +#import "ASTableViewInternal.h" #import "ASDisplayNode+Subclasses.h" +#import "ASChangeSetDataController.h" #define NumberOfSections 10 #define NumberOfRowsPerSection 20 #define NumberOfReloadIterations 50 +@interface ASTestDataController : ASChangeSetDataController +@property (atomic) int numberOfAllNodesRelayouts; +@end + +@implementation ASTestDataController + +- (void)relayoutAllNodes +{ + _numberOfAllNodesRelayouts++; + [super relayoutAllNodes]; +} + +@end + @interface ASTestTableView : ASTableView @property (atomic, copy) void (^willDeallocBlock)(ASTableView *tableView); @end @implementation ASTestTableView +- (instancetype)initWithFrame:(CGRect)frame style:(UITableViewStyle)style asyncDataFetching:(BOOL)asyncDataFetchingEnabled +{ + return [super initWithFrame:frame style:style dataControllerClass:[ASTestDataController class] asyncDataFetching:asyncDataFetchingEnabled]; +} + +- (ASTestDataController *)testDataController +{ + return (ASTestDataController *)self.dataController; +} + - (void)dealloc { if (_willDeallocBlock) { @@ -219,7 +245,7 @@ } } -- (void)testRelayoutAllRowsWithNonZeroSizeInitially +- (void)testRelayoutAllNodesWithNonZeroSizeInitially { // Initial width of the table view is non-zero and all nodes are measured with this size. // Any subsequence size change must trigger a relayout. @@ -233,12 +259,14 @@ tableView.asyncDelegate = dataSource; tableView.asyncDataSource = dataSource; + + [tableView layoutIfNeeded]; - [self triggerFirstLayoutMeasurementForTableView:tableView]; - [self triggerSizeChangeAndAssertRelayoutAllRowsForTableView:tableView newSize:tableViewFinalSize]; + XCTAssertEqual(tableView.testDataController.numberOfAllNodesRelayouts, 0); + [self triggerSizeChangeAndAssertRelayoutAllNodesForTableView:tableView newSize:tableViewFinalSize]; } -- (void)testRelayoutAllRowsWithZeroSizeInitially +- (void)testRelayoutAllNodesWithZeroSizeInitially { // Initial width of the table view is 0. The first size change is part of the initial config. // Any subsequence size change after that must trigger a relayout. @@ -256,10 +284,10 @@ [superview addSubview:tableView]; // Width and height are swapped so that a later size change will simulate a rotation tableView.frame = CGRectMake(0, 0, tableViewFinalSize.height, tableViewFinalSize.width); - // Trigger layout measurement on all nodes [tableView layoutIfNeeded]; - [self triggerSizeChangeAndAssertRelayoutAllRowsForTableView:tableView newSize:tableViewFinalSize]; + XCTAssertEqual(tableView.testDataController.numberOfAllNodesRelayouts, 0); + [self triggerSizeChangeAndAssertRelayoutAllNodesForTableView:tableView newSize:tableViewFinalSize]; } - (void)testRelayoutVisibleRowsWhenEditingModeIsChanged @@ -407,7 +435,7 @@ }]; } -- (void)triggerSizeChangeAndAssertRelayoutAllRowsForTableView:(ASTableView *)tableView newSize:(CGSize)newSize +- (void)triggerSizeChangeAndAssertRelayoutAllNodesForTableView:(ASTestTableView *)tableView newSize:(CGSize)newSize { XCTestExpectation *nodesMeasuredUsingNewConstrainedSizeExpectation = [self expectationWithDescription:@"nodesMeasuredUsingNewConstrainedSize"]; @@ -419,11 +447,13 @@ [tableView layoutIfNeeded]; [tableView endUpdatesAnimated:NO completion:^(BOOL completed) { + XCTAssertEqual(tableView.testDataController.numberOfAllNodesRelayouts, 1); + for (int section = 0; section < NumberOfSections; section++) { for (int row = 0; row < NumberOfRowsPerSection; row++) { NSIndexPath *indexPath = [NSIndexPath indexPathForRow:row inSection:section]; ASTestTextCellNode *node = (ASTestTextCellNode *)[tableView nodeForRowAtIndexPath:indexPath]; - XCTAssertEqual(node.numberOfLayoutsOnMainThread, 1); + XCTAssertLessThanOrEqual(node.numberOfLayoutsOnMainThread, 1); XCTAssertEqual(node.constrainedSizeForCalculatedLayout.max.width, newSize.width); } }