diff --git a/AsyncDisplayKit.xcodeproj/project.pbxproj b/AsyncDisplayKit.xcodeproj/project.pbxproj index 545f9d05e7..c7da8e107d 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, ); }; }; @@ -597,6 +605,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 = ""; }; @@ -890,6 +902,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 +968,8 @@ 058D0A01195D050800B7D73C /* Private */ = { isa = PBXGroup; children = ( + AC026B6D1BD57DBF00BBC17E /* _ASHierarchyChangeSet.h */, + AC026B6E1BD57DBF00BBC17E /* _ASHierarchyChangeSet.m */, 9C65A7291BA8EA4D0084DA91 /* ASLayoutOptionsPrivate.h */, 9C8221931BA237B80037F19A /* ASStackBaselinePositionedLayout.h */, 9C8221941BA237B80037F19A /* ASStackBaselinePositionedLayout.mm */, @@ -1075,6 +1091,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 */, @@ -1133,6 +1150,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 +1200,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 +1221,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 */, @@ -1478,6 +1498,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 +1554,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 */, @@ -1593,6 +1615,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 */, @@ -1648,6 +1671,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 18695a714b..2433553d08 100644 --- a/AsyncDisplayKit/ASTableView.mm +++ b/AsyncDisplayKit/ASTableView.mm @@ -9,7 +9,7 @@ #import "ASTableView.h" #import "ASAssert.h" -#import "ASDataController.h" +#import "ASChangeSetDataController.h" #import "ASCollectionViewLayoutController.h" #import "ASLayoutController.h" #import "ASRangeController.h" @@ -210,7 +210,7 @@ void ASPerformBlockWithoutAnimation(BOOL withoutAnimation, void (^block)()) { _rangeController.layoutController = _layoutController; _rangeController.delegate = self; - _dataController = [[ASDataController alloc] initWithAsyncDataFetching:asyncDataFetchingEnabled]; + _dataController = [[ASChangeSetDataController alloc] initWithAsyncDataFetching:asyncDataFetchingEnabled]; _dataController.dataSource = self; _dataController.delegate = _rangeController; diff --git a/AsyncDisplayKit/Details/ASChangeSetDataController.h b/AsyncDisplayKit/Details/ASChangeSetDataController.h new file mode 100644 index 0000000000..26bc5780e5 --- /dev/null +++ b/AsyncDisplayKit/Details/ASChangeSetDataController.h @@ -0,0 +1,19 @@ +// +// ASChangeSetDataController.h +// AsyncDisplayKit +// +// Created by Huy Nguyen on 19/10/15. +// Copyright (c) 2015 Facebook. All rights reserved. +// + +#import + +/** + * Subclass of ASDataController that enqueues and sorts edit commands during batch updating (using _ASHierarchyChangeSet). + * + * @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..b685a0408a 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)as_inverseCompare:(NSIndexPath *)otherIndexPath; +@end diff --git a/AsyncDisplayKit/Private/ASInternalHelpers.mm b/AsyncDisplayKit/Private/ASInternalHelpers.mm index f57450d39e..63003963b8 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)as_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..ad2ee2fb01 --- /dev/null +++ b/AsyncDisplayKit/Private/_ASHierarchyChangeSet.m @@ -0,0 +1,336 @@ +// +// _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; + + NSEnumerationOptions options = type == _ASHierarchyChangeTypeDelete ? NSEnumerationReverse : kNilOptions; + [allIndexes enumerateIndexesWithOptions:options usingBlock:^(NSUInteger idx, __unused BOOL * stop) { + + ASDataControllerAnimationOptions options = [animationOptions[@(idx)] integerValue]; + 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 { + // Ending the current group + _ASHierarchySectionChange *change = [[_ASHierarchySectionChange alloc] initWithChangeType:type indexSet:currentIndexes animationOptions:currentOptions]; + [result addObject:change]; + currentOptions = 0; + currentIndexes = nil; + } + }]; + + if (currentIndexes != nil) { + // Ending the last group + _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(as_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(as_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; + + for (NSIndexPath *indexPath in allIndexPaths) { + ASDataControllerAnimationOptions options = [animationOptions[indexPath] integerValue]; + 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 { + // Ending the current group + _ASHierarchyItemChange *change = [[_ASHierarchyItemChange alloc] initWithChangeType:type indexPaths:currentIndexPaths animationOptions:currentOptions presorted:YES]; + [result addObject:change]; + currentOptions = 0; + currentIndexPaths = nil; + } + } + + if (currentIndexPaths != nil) { + // Ending the last group + _ASHierarchyItemChange *change = [[_ASHierarchyItemChange alloc] initWithChangeType:type indexPaths:currentIndexPaths animationOptions:currentOptions presorted:YES]; + [result addObject:change]; + currentOptions = 0; + currentIndexPaths = nil; + } + [changes setArray:result]; +} + +@end