Make it possible to map between sections even if they're empty (#660)

* Make section mapping work even for empty sections

* Unlock more cases and update changelog

* Fix complexity documentation
This commit is contained in:
Adlai Holler 2017-11-03 16:24:48 -07:00 committed by GitHub
parent ff608c92bf
commit 4fc753a458
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
9 changed files with 95 additions and 83 deletions

View File

@ -8,6 +8,7 @@
- [Layout] Fixes a deadlock in layout. [#638](https://github.com/TextureGroup/Texture/pull/638) [Garrett Moon](https://github.com/garrettmoon)
- Updated to be backwards compatible with Xcode 8. [Adlai Holler](https://github.com/Adlai-Holler)
- [API CHANGES] `ASPerformMainThreadDeallocation` and `ASPerformBackgroundDeallocation` functions take `id *` instead of `id` and they're now more reliable. Also, in Swift, `ASDeallocQueue.sharedDeallocationQueue() -> ASDeallocQueue.sharedDeallocationQueue`. [Adlai Holler](https://github.com/Adlai-Holler) [#651](https://github.com/TextureGroup/Texture/pull/651)
- [Collection/Table] Added direct support for mapping section indexes between data spaces. [Adlai Holler](https://github.com/Adlai-Holler) [#651](https://github.com/TextureGroup/Texture/pull/660)
## 2.6
- [Xcode 9] Updated to require Xcode 9 (to fix warnings) [Garrett Moon](https://github.com/garrettmoon)

View File

@ -64,8 +64,6 @@
return __val; \
}
#define ASIndexPathForSection(section) [NSIndexPath indexPathForItem:0 inSection:section]
#define ASFlowLayoutDefault(layout, property, default) \
({ \
UICollectionViewFlowLayout *flowLayout = ASDynamicCast(layout, UICollectionViewFlowLayout); \
@ -680,18 +678,12 @@ static NSString * const kReuseIdentifier = @"_ASCollectionReuseIdentifier";
return nil;
}
// If this is a section index path, we don't currently have a method
// to do a mapping.
if (indexPath.item == NSNotFound) {
return indexPath;
} else {
NSIndexPath *viewIndexPath = [_dataController.visibleMap convertIndexPath:indexPath fromMap:_dataController.pendingMap];
if (viewIndexPath == nil && wait) {
[self waitUntilAllUpdatesAreCommitted];
return [self convertIndexPathFromCollectionNode:indexPath waitingIfNeeded:NO];
}
return viewIndexPath;
}
}
/**
@ -725,13 +717,7 @@ static NSString * const kReuseIdentifier = @"_ASCollectionReuseIdentifier";
return nil;
}
// If this is a section index path, we don't currently have a method
// to do a mapping.
if (indexPath.item == NSNotFound) {
return indexPath;
} else {
return [_dataController.pendingMap convertIndexPath:indexPath fromMap:_dataController.visibleMap];
}
}
- (NSArray<NSIndexPath *> *)convertIndexPathsToCollectionNode:(NSArray<NSIndexPath *> *)indexPaths
@ -1050,7 +1036,7 @@ static NSString * const kReuseIdentifier = @"_ASCollectionReuseIdentifier";
ASDisplayNodeAssertMainThread();
ASElementMap *map = _dataController.visibleMap;
ASCollectionElement *e = [map supplementaryElementOfKind:UICollectionElementKindSectionHeader
atIndexPath:ASIndexPathForSection(section)];
atIndexPath:[NSIndexPath indexPathForItem:0 inSection:section]];
return e ? [self sizeForElement:e] : ASFlowLayoutDefault(l, headerReferenceSize, CGSizeZero);
}
@ -1060,29 +1046,28 @@ static NSString * const kReuseIdentifier = @"_ASCollectionReuseIdentifier";
ASDisplayNodeAssertMainThread();
ASElementMap *map = _dataController.visibleMap;
ASCollectionElement *e = [map supplementaryElementOfKind:UICollectionElementKindSectionFooter
atIndexPath:ASIndexPathForSection(section)];
atIndexPath:[NSIndexPath indexPathForItem:0 inSection:section]];
return e ? [self sizeForElement:e] : ASFlowLayoutDefault(l, footerReferenceSize, CGSizeZero);
}
// For the methods that call delegateIndexPathForSection:withSelector:, translate the section from
// visibleMap to pendingMap. If the section no longer exists, or the delegate doesn't implement
// the selector, we will return a nil indexPath (and then use the ASFlowLayoutDefault).
- (NSIndexPath *)delegateIndexPathForSection:(NSInteger)section withSelector:(SEL)selector
// the selector, we will return NSNotFound (and then use the ASFlowLayoutDefault).
- (NSInteger)delegateIndexForSection:(NSInteger)section withSelector:(SEL)selector
{
if ([_asyncDelegate respondsToSelector:selector]) {
return [_dataController.pendingMap convertIndexPath:ASIndexPathForSection(section)
fromMap:_dataController.visibleMap];
return [_dataController.pendingMap convertSection:section fromMap:_dataController.visibleMap];
} else {
return nil;
return NSNotFound;
}
}
- (UIEdgeInsets)collectionView:(UICollectionView *)cv layout:(UICollectionViewLayout *)l
insetForSectionAtIndex:(NSInteger)section
{
NSIndexPath *indexPath = [self delegateIndexPathForSection:section withSelector:_cmd];
if (indexPath) {
return [(id)_asyncDelegate collectionView:cv layout:l insetForSectionAtIndex:indexPath.section];
section = [self delegateIndexForSection:section withSelector:_cmd];
if (section != NSNotFound) {
return [(id)_asyncDelegate collectionView:cv layout:l insetForSectionAtIndex:section];
}
return ASFlowLayoutDefault(l, sectionInset, UIEdgeInsetsZero);
}
@ -1090,10 +1075,10 @@ static NSString * const kReuseIdentifier = @"_ASCollectionReuseIdentifier";
- (CGFloat)collectionView:(UICollectionView *)cv layout:(UICollectionViewLayout *)l
minimumInteritemSpacingForSectionAtIndex:(NSInteger)section
{
NSIndexPath *indexPath = [self delegateIndexPathForSection:section withSelector:_cmd];
if (indexPath) {
section = [self delegateIndexForSection:section withSelector:_cmd];
if (section != NSNotFound) {
return [(id)_asyncDelegate collectionView:cv layout:l
minimumInteritemSpacingForSectionAtIndex:indexPath.section];
minimumInteritemSpacingForSectionAtIndex:section];
}
return ASFlowLayoutDefault(l, minimumInteritemSpacing, 10.0); // Default is documented as 10.0
}
@ -1101,10 +1086,10 @@ static NSString * const kReuseIdentifier = @"_ASCollectionReuseIdentifier";
- (CGFloat)collectionView:(UICollectionView *)cv layout:(UICollectionViewLayout *)l
minimumLineSpacingForSectionAtIndex:(NSInteger)section
{
NSIndexPath *indexPath = [self delegateIndexPathForSection:section withSelector:_cmd];
if (indexPath) {
section = [self delegateIndexForSection:section withSelector:_cmd];
if (section != NSNotFound) {
return [(id)_asyncDelegate collectionView:cv layout:l
minimumLineSpacingForSectionAtIndex:indexPath.section];
minimumLineSpacingForSectionAtIndex:section];
}
return ASFlowLayoutDefault(l, minimumLineSpacing, 10.0); // Default is documented as 10.0
}

View File

@ -588,18 +588,12 @@ static NSString * const kCellReuseIdentifier = @"_ASTableViewCell";
- (NSIndexPath *)convertIndexPathFromTableNode:(NSIndexPath *)indexPath waitingIfNeeded:(BOOL)wait
{
// If this is a section index path, we don't currently have a method
// to do a mapping.
if (indexPath == nil || indexPath.row == NSNotFound) {
return indexPath;
} else {
NSIndexPath *viewIndexPath = [_dataController.visibleMap convertIndexPath:indexPath fromMap:_dataController.pendingMap];
if (viewIndexPath == nil && wait) {
[self waitUntilAllUpdatesAreCommitted];
return [self convertIndexPathFromTableNode:indexPath waitingIfNeeded:NO];
}
return viewIndexPath;
}
}
- (NSIndexPath *)convertIndexPathToTableNode:(NSIndexPath *)indexPath
@ -608,13 +602,7 @@ static NSString * const kCellReuseIdentifier = @"_ASTableViewCell";
return nil;
}
// If this is a section index path, we don't currently have a method
// to do a mapping.
if (indexPath.row == NSNotFound) {
return indexPath;
} else {
return [_dataController.pendingMap convertIndexPath:indexPath fromMap:_dataController.visibleMap];
}
}
- (NSArray<NSIndexPath *> *)convertIndexPathsToTableNode:(NSArray<NSIndexPath *> *)indexPaths

View File

@ -540,7 +540,7 @@ typedef dispatch_block_t ASDataControllerCompletionBlock;
ASMutableElementMap *mutableMap = [previousMap mutableCopy];
// Step 1.1: Update the mutable copies to match the data source's state
[self _updateSectionContextsInMap:mutableMap changeSet:changeSet];
[self _updateSectionsInMap:mutableMap changeSet:changeSet];
ASPrimitiveTraitCollection existingTraitCollection = [self.node primitiveTraitCollection];
[self _updateElementsInMap:mutableMap changeSet:changeSet traitCollection:existingTraitCollection shouldFetchSizeRanges:(! canDelegate) previousMap:previousMap];
@ -599,43 +599,38 @@ typedef dispatch_block_t ASDataControllerCompletionBlock;
/**
* Update sections based on the given change set.
*/
- (void)_updateSectionContextsInMap:(ASMutableElementMap *)map changeSet:(_ASHierarchyChangeSet *)changeSet
- (void)_updateSectionsInMap:(ASMutableElementMap *)map changeSet:(_ASHierarchyChangeSet *)changeSet
{
ASDisplayNodeAssertMainThread();
if (!_dataSourceFlags.contextForSection) {
return;
}
if (changeSet.includesReloadData) {
[map removeAllSectionContexts];
[map removeAllSections];
NSUInteger sectionCount = [self itemCountsFromDataSource].size();
NSIndexSet *sectionIndexes = [NSIndexSet indexSetWithIndexesInRange:NSMakeRange(0, sectionCount)];
[self _insertSectionContextsIntoMap:map indexes:sectionIndexes];
[self _insertSectionsIntoMap:map indexes:sectionIndexes];
// Return immediately because reloadData can't be used in conjuntion with other updates.
return;
}
for (_ASHierarchySectionChange *change in [changeSet sectionChangesOfType:_ASHierarchyChangeTypeDelete]) {
[map removeSectionContextsAtIndexes:change.indexSet];
[map removeSectionsAtIndexes:change.indexSet];
}
for (_ASHierarchySectionChange *change in [changeSet sectionChangesOfType:_ASHierarchyChangeTypeInsert]) {
[self _insertSectionContextsIntoMap:map indexes:change.indexSet];
[self _insertSectionsIntoMap:map indexes:change.indexSet];
}
}
- (void)_insertSectionContextsIntoMap:(ASMutableElementMap *)map indexes:(NSIndexSet *)sectionIndexes
- (void)_insertSectionsIntoMap:(ASMutableElementMap *)map indexes:(NSIndexSet *)sectionIndexes
{
ASDisplayNodeAssertMainThread();
if (!_dataSourceFlags.contextForSection) {
return;
}
[sectionIndexes enumerateIndexesUsingBlock:^(NSUInteger idx, BOOL * _Nonnull stop) {
id<ASSectionContext> context = [_dataSource dataController:self contextForSection:idx];
id<ASSectionContext> context;
if (_dataSourceFlags.contextForSection) {
context = [_dataSource dataController:self contextForSection:idx];
}
ASSection *section = [[ASSection alloc] initWithSectionID:_nextSectionID context:context];
[map insertSection:section atIndex:idx];
_nextSectionID++;

View File

@ -64,10 +64,20 @@ AS_SUBCLASSING_RESTRICTED
@property (copy, readonly) NSArray<ASCollectionElement *> *itemElements;
/**
* Returns the index path that corresponds to the same element in @c map at the given @c indexPath. O(1)
* Returns the index path that corresponds to the same element in @c map at the given @c indexPath.
* O(1) for items, fast O(N) for sections.
*
* Note you can pass "section index paths" of length 1 and get a corresponding section index path.
*/
- (nullable NSIndexPath *)convertIndexPath:(NSIndexPath *)indexPath fromMap:(ASElementMap *)map;
/**
* Returns the section index into the receiver that corresponds to the same element in @c map at @c sectionIndex. Fast O(N).
*
* Returns @c NSNotFound if the section does not exist in the receiver.
*/
- (NSInteger)convertSection:(NSInteger)sectionIndex fromMap:(ASElementMap *)map;
/**
* Returns the index path for the given element. O(1)
*/

View File

@ -47,6 +47,8 @@
- (instancetype)initWithSections:(NSArray<ASSection *> *)sections items:(ASCollectionElementTwoDimensionalArray *)items supplementaryElements:(ASSupplementaryElementDictionary *)supplementaryElements
{
NSCParameterAssert(items.count == sections.count);
if (self = [super init]) {
_sections = [sections copy];
_sectionsOfItems = [[NSArray alloc] initWithArray:items copyItems:YES];
@ -157,8 +159,25 @@
- (NSIndexPath *)convertIndexPath:(NSIndexPath *)indexPath fromMap:(ASElementMap *)map
{
id element = [map elementForItemAtIndexPath:indexPath];
if (indexPath.item == NSNotFound) {
// Section index path
NSInteger result = [self convertSection:indexPath.section fromMap:map];
return (result != NSNotFound ? [NSIndexPath indexPathWithIndex:result] : nil);
} else {
// Item index path
ASCollectionElement *element = [map elementForItemAtIndexPath:indexPath];
return [self indexPathForElement:element];
}
}
- (NSInteger)convertSection:(NSInteger)sectionIndex fromMap:(ASElementMap *)map
{
if (![map sectionIndexIsValid:sectionIndex assert:YES]) {
return NSNotFound;
}
ASSection *section = map.sections[sectionIndex];
return [_sections indexOfObjectIdenticalTo:section];
}
#pragma mark - NSCopying

View File

@ -37,10 +37,10 @@ AS_SUBCLASSING_RESTRICTED
- (void)insertSection:(ASSection *)section atIndex:(NSInteger)index;
- (void)removeAllSectionContexts;
- (void)removeAllSections;
/// Only modifies the array of ASSection * objects
- (void)removeSectionContextsAtIndexes:(NSIndexSet *)indexes;
- (void)removeSectionsAtIndexes:(NSIndexSet *)indexes;
- (void)removeAllElements;

View File

@ -48,7 +48,7 @@ typedef NSMutableDictionary<NSString *, NSMutableDictionary<NSIndexPath *, ASCol
return [[ASElementMap alloc] initWithSections:_sections items:_sectionsOfItems supplementaryElements:_supplementaryElements];
}
- (void)removeAllSectionContexts
- (void)removeAllSections
{
[_sections removeAllObjects];
}
@ -63,7 +63,7 @@ typedef NSMutableDictionary<NSString *, NSMutableDictionary<NSIndexPath *, ASCol
ASDeleteElementsInTwoDimensionalArrayAtIndexPaths(_sectionsOfItems, indexPaths);
}
- (void)removeSectionContextsAtIndexes:(NSIndexSet *)indexes
- (void)removeSectionsAtIndexes:(NSIndexSet *)indexes
{
[_sections removeObjectsAtIndexes:indexes];
}

View File

@ -16,15 +16,29 @@
//
#import <Foundation/Foundation.h>
#import <AsyncDisplayKit/ASBaseDefines.h>
@protocol ASSectionContext;
NS_ASSUME_NONNULL_BEGIN
/**
* An object representing the metadata for a section of elements in a collection.
*
* Its sectionID is namespaced to the data controller that created the section.
*
* These are useful for tracking the movement & lifetime of sections, independent of
* their contents.
*/
AS_SUBCLASSING_RESTRICTED
@interface ASSection : NSObject
@property (nonatomic, assign, readonly) NSInteger sectionID;
@property (nonatomic, strong, nullable, readonly) id<ASSectionContext> context;
@property (assign, readonly) NSInteger sectionID;
@property (strong, nullable, readonly) id<ASSectionContext> context;
- (nullable instancetype)init __unavailable;
- (nullable instancetype)initWithSectionID:(NSInteger)sectionID context:(nullable id<ASSectionContext>)context NS_DESIGNATED_INITIALIZER;
- (instancetype)init NS_UNAVAILABLE;
- (instancetype)initWithSectionID:(NSInteger)sectionID context:(nullable id<ASSectionContext>)context NS_DESIGNATED_INITIALIZER;
@end
NS_ASSUME_NONNULL_END