From 5115f660c81c3b84dd5908dde216769fbdad25f0 Mon Sep 17 00:00:00 2001 From: appleguy Date: Mon, 17 Jul 2017 04:30:46 -0700 Subject: [PATCH 01/16] [ASCollectionView] Add delegate bridging and index space translation for missing UICollectionViewLayout properties. (#440) * [ASCollectionView] Add delegate bridging and index space translation for missing UICollectionViewLayout properties This is a first attempt at resolving #438. I know there is already one similar method to do indexPath conversions, and that the #define for accessing properties on the UICollectionViewFlowLayout should probably be moved somewhere else. Looking for feedback on the general direction here. In particular, I'm a bit surprised that so much has changed in how these calls occur, as I believe especially constrainedSizeForNodeAtIndexPath: is now called with inconsistent index path spaces (but was not before this PR landed in March: https://github.com/facebookarchive/AsyncDisplayKit/pull/3136/files) Since the impact of mixing the spaces is fairly serious (can cause crashes), I'm also wondering if I am misinterpreting some aspects of the code, or if maybe the crashing impact wasn't noticed yet. * [ASCollectionView] Cleanup and final changes for UIKit passthrough fixes. --- CHANGELOG.md | 1 + Source/ASCollectionNode.mm | 4 +- Source/ASCollectionView.mm | 165 ++++++++++++++++++++----------- Source/Details/ASDelegateProxy.m | 3 + Source/Details/ASElementMap.m | 2 +- 5 files changed, 117 insertions(+), 58 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index b39354c6d4..b3bd346c4e 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,6 +1,7 @@ ## master * Add your own contributions to the next release on the line below this with your name. +- [ASCollectionView] Add delegate bridging and index space translation for missing UICollectionViewLayout properties. [Scott Goodson](https://github.com/appleguy) - [ASTextNode2] Add initial implementation for link handling. [Scott Goodson](https://github.com/appleguy) [#396](https://github.com/TextureGroup/Texture/pull/396) - [ASTextNode2] Provide compile flag to globally enable new implementation of ASTextNode: ASTEXTNODE_EXPERIMENT_GLOBAL_ENABLE. [Scott Goodson](https://github.com/appleguy) [#396](https://github.com/TextureGroup/Texture/pull/410) - Add ASCollectionGalleryLayoutDelegate - an async collection layout that makes same-size collections (e.g photo galleries, pagers, etc) fast and lightweight! [Huy Nguyen](https://github.com/nguyenhuy/) [#76](https://github.com/TextureGroup/Texture/pull/76) diff --git a/Source/ASCollectionNode.mm b/Source/ASCollectionNode.mm index 67aea1fc81..81c9a907a8 100644 --- a/Source/ASCollectionNode.mm +++ b/Source/ASCollectionNode.mm @@ -217,7 +217,9 @@ // Intentionally allocate the view here and trigger a layout pass on it, which in turn will trigger the intial data load. // We can get rid of this call later when ASDataController, ASRangeController and ASCollectionLayout can operate without the view. // TODO (ASCL) If this node supports async layout, kick off the initial data load without allocating the view - [[self view] layoutIfNeeded]; + if (CGRectEqualToRect(self.bounds, CGRectZero) == NO) { + [[self view] layoutIfNeeded]; + } } #if ASRangeControllerLoggingEnabled diff --git a/Source/ASCollectionView.mm b/Source/ASCollectionView.mm index 36cd29c93e..3d518b1a8a 100644 --- a/Source/ASCollectionView.mm +++ b/Source/ASCollectionView.mm @@ -622,15 +622,17 @@ static NSString * const kReuseIdentifier = @"_ASCollectionReuseIdentifier"; return CGSizeZero; } - NSString *supplementaryKind = element.supplementaryElementKind; - NSIndexPath *indexPath = [_dataController.visibleMap indexPathForElement:element]; - ASSizeRange sizeRange; - if (supplementaryKind == nil) { - sizeRange = [self dataController:_dataController constrainedSizeForNodeAtIndexPath:indexPath]; + ASCellNode *node = element.node; + BOOL useUIKitCell = node.shouldUseUIKitCell; + if (useUIKitCell) { + // In this case, we should use the exact value that was stashed earlier by calling sizeForItem:, referenceSizeFor*, etc. + // Although the node would use the preferredSize in layoutThatFits, we can skip this because there's no constrainedSize. + ASDisplayNodeAssert([node.superclass isSubclassOfClass:[ASCellNode class]] == NO, + @"Placeholder cells for UIKit passthrough should be generic ASCellNodes: %@", node); + return node.style.preferredSize; } else { - sizeRange = [self dataController:_dataController constrainedSizeForSupplementaryNodeOfKind:supplementaryKind atIndexPath:indexPath]; + return [node layoutThatFits:element.constrainedSize].size; } - return [element.node layoutThatFits:sizeRange].size; } - (CGSize)calculatedSizeForNodeAtIndexPath:(NSIndexPath *)indexPath @@ -949,63 +951,84 @@ static NSString * const kReuseIdentifier = @"_ASCollectionReuseIdentifier"; return [_dataController.visibleMap numberOfItemsInSection:section]; } -- (CGSize)collectionView:(UICollectionView *)collectionView layout:(UICollectionViewLayout *)collectionViewLayout sizeForItemAtIndexPath:(NSIndexPath *)indexPath +#define ASIndexPathForSection(section) [NSIndexPath indexPathForItem:0 inSection:section] +#define ASFlowLayoutDefault(layout, property, default) \ +({ \ + UICollectionViewFlowLayout *flowLayout = ASDynamicCast(layout, UICollectionViewFlowLayout); \ + flowLayout ? flowLayout.property : default; \ +}) + +- (CGSize)collectionView:(UICollectionView *)collectionView layout:(UICollectionViewLayout *)layout + sizeForItemAtIndexPath:(NSIndexPath *)indexPath { ASDisplayNodeAssertMainThread(); - ASCollectionElement *element = [_dataController.visibleMap elementForItemAtIndexPath:indexPath]; - if (element == nil) { - ASDisplayNodeAssert(NO, @"Unexpected nil element for collectionView:layout:sizeForItemAtIndexPath: %@, %@, %@", self, collectionViewLayout, indexPath); - return CGSizeZero; - } - - ASCellNode *cell = element.node; - if (cell.shouldUseUIKitCell) { - if ([_asyncDelegate respondsToSelector:@selector(collectionView:layout:sizeForItemAtIndexPath:)]) { - CGSize size = [(id)_asyncDelegate collectionView:collectionView layout:collectionViewLayout sizeForItemAtIndexPath:indexPath]; - cell.style.preferredSize = size; - return size; - } - } - - return [self sizeForElement:element]; + ASCollectionElement *e = [_dataController.visibleMap elementForItemAtIndexPath:indexPath]; + return e ? [self sizeForElement:e] : ASFlowLayoutDefault(layout, itemSize, CGSizeZero); } -- (CGSize)collectionView:(UICollectionView *)collectionView layout:(UICollectionViewLayout *)layout referenceSizeForHeaderInSection:(NSInteger)section +- (CGSize)collectionView:(UICollectionView *)cv layout:(UICollectionViewLayout *)l +referenceSizeForHeaderInSection:(NSInteger)section { ASDisplayNodeAssertMainThread(); - NSIndexPath *indexPath = [NSIndexPath indexPathForItem:0 inSection:section]; - ASCollectionElement *element = [_dataController.visibleMap supplementaryElementOfKind:UICollectionElementKindSectionHeader - atIndexPath:indexPath]; - if (element == nil) { - return CGSizeZero; - } - - if (element.node.shouldUseUIKitCell && _asyncDelegateFlags.interop) { - if ([_asyncDelegate respondsToSelector:@selector(collectionView:layout:referenceSizeForHeaderInSection:)]) { - return [(id)_asyncDelegate collectionView:collectionView layout:layout referenceSizeForHeaderInSection:section]; - } - } - - return [self sizeForElement:element]; + ASElementMap *map = _dataController.visibleMap; + ASCollectionElement *e = [map supplementaryElementOfKind:UICollectionElementKindSectionHeader + atIndexPath:ASIndexPathForSection(section)]; + return e ? [self sizeForElement:e] : ASFlowLayoutDefault(l, headerReferenceSize, CGSizeZero); } -- (CGSize)collectionView:(UICollectionView *)collectionView layout:(UICollectionViewLayout *)layout referenceSizeForFooterInSection:(NSInteger)section +- (CGSize)collectionView:(UICollectionView *)cv layout:(UICollectionViewLayout *)l +referenceSizeForFooterInSection:(NSInteger)section { ASDisplayNodeAssertMainThread(); - NSIndexPath *indexPath = [NSIndexPath indexPathForItem:0 inSection:section]; - ASCollectionElement *element = [_dataController.visibleMap supplementaryElementOfKind:UICollectionElementKindSectionFooter - atIndexPath:indexPath]; - if (element == nil) { - return CGSizeZero; - } + ASElementMap *map = _dataController.visibleMap; + ASCollectionElement *e = [map supplementaryElementOfKind:UICollectionElementKindSectionFooter + atIndexPath:ASIndexPathForSection(section)]; + return e ? [self sizeForElement:e] : ASFlowLayoutDefault(l, footerReferenceSize, CGSizeZero); +} - if (element.node.shouldUseUIKitCell && _asyncDelegateFlags.interop) { - if ([_asyncDelegate respondsToSelector:@selector(collectionView:layout:referenceSizeForFooterInSection:)]) { - return [(id)_asyncDelegate collectionView:collectionView layout:layout referenceSizeForFooterInSection:section]; - } +// 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 +{ + if ([_asyncDelegate respondsToSelector:selector]) { + return [_dataController.pendingMap convertIndexPath:ASIndexPathForSection(section) + fromMap:_dataController.visibleMap]; + } else { + return nil; } +} - return [self sizeForElement:element]; +- (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]; + } + return ASFlowLayoutDefault(l, sectionInset, UIEdgeInsetsZero); +} + +- (CGFloat)collectionView:(UICollectionView *)cv layout:(UICollectionViewLayout *)l +minimumInteritemSpacingForSectionAtIndex:(NSInteger)section +{ + NSIndexPath *indexPath = [self delegateIndexPathForSection:section withSelector:_cmd]; + if (indexPath) { + return [(id)_asyncDelegate collectionView:cv layout:l + minimumInteritemSpacingForSectionAtIndex:indexPath.section]; + } + return ASFlowLayoutDefault(l, minimumInteritemSpacing, 10.0); // Default is documented as 10.0 +} + +- (CGFloat)collectionView:(UICollectionView *)cv layout:(UICollectionViewLayout *)l +minimumLineSpacingForSectionAtIndex:(NSInteger)section +{ + NSIndexPath *indexPath = [self delegateIndexPathForSection:section withSelector:_cmd]; + if (indexPath) { + return [(id)_asyncDelegate collectionView:cv layout:l + minimumLineSpacingForSectionAtIndex:indexPath.section]; + } + return ASFlowLayoutDefault(l, minimumLineSpacing, 10.0); // Default is documented as 10.0 } - (UICollectionReusableView *)collectionView:(UICollectionView *)collectionView viewForSupplementaryElementOfKind:(NSString *)kind atIndexPath:(NSIndexPath *)indexPath @@ -1013,7 +1036,7 @@ static NSString * const kReuseIdentifier = @"_ASCollectionReuseIdentifier"; if ([_registeredSupplementaryKinds containsObject:kind] == NO) { [self registerSupplementaryNodeOfKind:kind]; } - + UICollectionReusableView *view = nil; ASCollectionElement *element = [_dataController.visibleMap supplementaryElementOfKind:kind atIndexPath:indexPath]; ASCellNode *node = element.node; @@ -1678,11 +1701,19 @@ static NSString * const kReuseIdentifier = @"_ASCollectionReuseIdentifier"; if (block == nil) { if (_asyncDataSourceFlags.interop) { + UICollectionViewLayout *layout = self.collectionViewLayout; + CGSize preferredSize = CGSizeZero; + SEL sizeForItem = @selector(collectionView:layout:sizeForItemAtIndexPath:); + if ([_asyncDelegate respondsToSelector:sizeForItem]) { + preferredSize = [(id)_asyncDelegate collectionView:self layout:layout sizeForItemAtIndexPath:indexPath]; + } else { + preferredSize = ASFlowLayoutDefault(layout, itemSize, CGSizeZero); + } block = ^{ - ASCellNode *cell = [[ASCellNode alloc] init]; - cell.shouldUseUIKitCell = YES; - cell.style.preferredSize = CGSizeZero; - return cell; + ASCellNode *node = [[ASCellNode alloc] init]; + node.shouldUseUIKitCell = YES; + node.style.preferredSize = preferredSize; + return node; }; } else { ASDisplayNodeFailAssert(@"ASCollection could not get a node block for item at index path %@: %@, %@. If you are trying to display a UICollectionViewCell, make sure your dataSource conforms to the protocol!", indexPath, cell, block); @@ -1773,9 +1804,31 @@ static NSString * const kReuseIdentifier = @"_ASCollectionReuseIdentifier"; nodeBlock = ^{ return node; }; } else { BOOL useUIKitCell = _asyncDataSourceFlags.interop; + CGSize preferredSize = CGSizeZero; + if (useUIKitCell) { + UICollectionViewLayout *layout = self.collectionViewLayout; + if ([kind isEqualToString:UICollectionElementKindSectionHeader]) { + SEL sizeForHeader = @selector(collectionView:layout:referenceSizeForHeaderInSection:); + if ([_asyncDelegate respondsToSelector:sizeForHeader]) { + preferredSize = [(id)_asyncDelegate collectionView:self layout:layout + referenceSizeForHeaderInSection:indexPath.section]; + } else { + preferredSize = ASFlowLayoutDefault(layout, headerReferenceSize, CGSizeZero); + } + } else if ([kind isEqualToString:UICollectionElementKindSectionFooter]) { + SEL sizeForFooter = @selector(collectionView:layout:referenceSizeForFooterInSection:); + if ([_asyncDelegate respondsToSelector:sizeForFooter]) { + preferredSize = [(id)_asyncDelegate collectionView:self layout:layout + referenceSizeForFooterInSection:indexPath.section]; + } else { + preferredSize = ASFlowLayoutDefault(layout, footerReferenceSize, CGSizeZero); + } + } + } nodeBlock = ^{ ASCellNode *node = [[ASCellNode alloc] init]; node.shouldUseUIKitCell = useUIKitCell; + node.style.preferredSize = preferredSize; return node; }; } diff --git a/Source/Details/ASDelegateProxy.m b/Source/Details/ASDelegateProxy.m index f9cf09ee8e..b8314153a6 100644 --- a/Source/Details/ASDelegateProxy.m +++ b/Source/Details/ASDelegateProxy.m @@ -76,6 +76,9 @@ // handled by ASCollectionView node<->cell machinery selector == @selector(collectionView:cellForItemAtIndexPath:) || selector == @selector(collectionView:layout:sizeForItemAtIndexPath:) || + selector == @selector(collectionView:layout:insetForSectionAtIndex:) || + selector == @selector(collectionView:layout:minimumLineSpacingForSectionAtIndex:) || + selector == @selector(collectionView:layout:minimumInteritemSpacingForSectionAtIndex:) || selector == @selector(collectionView:layout:referenceSizeForHeaderInSection:) || selector == @selector(collectionView:layout:referenceSizeForFooterInSection:) || selector == @selector(collectionView:viewForSupplementaryElementOfKind:atIndexPath:) || diff --git a/Source/Details/ASElementMap.m b/Source/Details/ASElementMap.m index a4195e1e1d..716e40301c 100644 --- a/Source/Details/ASElementMap.m +++ b/Source/Details/ASElementMap.m @@ -113,7 +113,7 @@ - (nullable NSIndexPath *)indexPathForElement:(ASCollectionElement *)element { - return [_elementToIndexPathMap objectForKey:element]; + return element ? [_elementToIndexPathMap objectForKey:element] : nil; } - (nullable NSIndexPath *)indexPathForElementIfCell:(ASCollectionElement *)element From 7302816b2ff256b9027a347d13d0f81162459ca9 Mon Sep 17 00:00:00 2001 From: Huy Nguyen Date: Mon, 17 Jul 2017 11:37:40 +0000 Subject: [PATCH 02/16] [ASDataController] Avoid asking for size ranges of soon-to-be-delete elements during relayouts (#442) * ASDataController to avoid asking for size ranges of soon-to-be-deleted elements during relayouts * Remove outdated TODOs --- Source/ASCellNode.h | 1 - Source/Details/ASCollectionElement.h | 1 - Source/Details/ASDataController.mm | 38 ++++++++++++++++------------ 3 files changed, 22 insertions(+), 18 deletions(-) diff --git a/Source/ASCellNode.h b/Source/ASCellNode.h index 80dbe2fb83..ca9dda6294 100644 --- a/Source/ASCellNode.h +++ b/Source/ASCellNode.h @@ -87,7 +87,6 @@ typedef NS_ENUM(NSUInteger, ASCellNodeVisibilityEvent) { * * @return The supplementary element kind, or @c nil if this node does not represent a supplementary element. */ -//TODO change this to be a generic "kind" or "elementKind" that exposes `nil` for row kind @property (atomic, copy, readonly, nullable) NSString *supplementaryElementKind; /* diff --git a/Source/Details/ASCollectionElement.h b/Source/Details/ASCollectionElement.h index 337e6b0be0..252d394331 100644 --- a/Source/Details/ASCollectionElement.h +++ b/Source/Details/ASCollectionElement.h @@ -26,7 +26,6 @@ NS_ASSUME_NONNULL_BEGIN AS_SUBCLASSING_RESTRICTED @interface ASCollectionElement : NSObject -//TODO change this to be a generic "kind" or "elementKind" that exposes `nil` for row kind @property (nonatomic, readonly, copy, nullable) NSString *supplementaryElementKind; @property (nonatomic, assign) ASSizeRange constrainedSize; @property (nonatomic, readonly, weak) id owningNode; diff --git a/Source/Details/ASDataController.mm b/Source/Details/ASDataController.mm index a83a94da28..2c2c5e75c8 100644 --- a/Source/Details/ASDataController.mm +++ b/Source/Details/ASDataController.mm @@ -433,21 +433,16 @@ typedef dispatch_block_t ASDataControllerCompletionBlock; return @[]; } -- (ASSizeRange)constrainedSizeForElement:(ASCollectionElement *)element inElementMap:(ASElementMap *)map -{ - ASDisplayNodeAssertMainThread(); - NSString *kind = element.supplementaryElementKind ?: ASDataControllerRowNodeKind; - NSIndexPath *indexPath = [map indexPathForElement:element]; - return [self constrainedSizeForNodeOfKind:kind atIndexPath:indexPath]; -} - - +/** + * Returns constrained size for the node of the given kind and at the given index path. + * NOTE: index path must be in the data-source index space. + */ - (ASSizeRange)constrainedSizeForNodeOfKind:(NSString *)kind atIndexPath:(NSIndexPath *)indexPath { ASDisplayNodeAssertMainThread(); id dataSource = _dataSource; - if (dataSource == nil) { + if (dataSource == nil || indexPath == nil) { return ASSizeRangeZero; } @@ -750,15 +745,18 @@ typedef dispatch_block_t ASDataControllerCompletionBlock; auto pendingMap = self.pendingMap; for (ASCellNode *node in nodes) { auto element = node.collectionElement; + NSIndexPath *indexPathInPendingMap = [pendingMap indexPathForElement:element]; // Ensure the element is present in both maps or skip it. If it's not in the visible map, // then we can't check the presented size. If it's not in the pending map, we can't get the constrained size. // This will only happen if the element has been deleted, so the specifics of this behavior aren't important. - if ([visibleMap indexPathForElement:element] == nil || [pendingMap indexPathForElement:element] == nil) { + if (indexPathInPendingMap == nil || [visibleMap indexPathForElement:element] == nil) { continue; } - ASSizeRange constrainedSize = [self constrainedSizeForElement:element inElementMap:pendingMap]; + NSString *kind = element.supplementaryElementKind ?: ASDataControllerRowNodeKind; + ASSizeRange constrainedSize = [self constrainedSizeForNodeOfKind:kind atIndexPath:indexPathInPendingMap]; [self _layoutNode:node withConstrainedSize:constrainedSize]; + BOOL matchesSize = [dataSource dataController:self presentedSizeForElement:element matchesSize:node.frame.size]; if (! matchesSize) { [nodesSizesChanged addObject:node]; @@ -785,15 +783,23 @@ typedef dispatch_block_t ASDataControllerCompletionBlock; { ASDisplayNodeAssertMainThread(); for (ASCollectionElement *element in _visibleMap) { - ASSizeRange constrainedSize = [self constrainedSizeForElement:element inElementMap:_visibleMap]; - if (ASSizeRangeHasSignificantArea(constrainedSize)) { - element.constrainedSize = constrainedSize; + // Ignore this element if it is no longer in the latest data. It is still recognized in the UIKit world but will be deleted soon. + NSIndexPath *indexPathInPendingMap = [_pendingMap indexPathForElement:element]; + if (indexPathInPendingMap == nil) { + continue; + } + + NSString *kind = element.supplementaryElementKind ?: ASDataControllerRowNodeKind; + ASSizeRange newConstrainedSize = [self constrainedSizeForNodeOfKind:kind atIndexPath:indexPathInPendingMap]; + + if (ASSizeRangeHasSignificantArea(newConstrainedSize)) { + element.constrainedSize = newConstrainedSize; // Node may not be allocated yet (e.g node virtualization or same size optimization) // Call context.nodeIfAllocated here to avoid immature node allocation and layout ASCellNode *node = element.nodeIfAllocated; if (node) { - [self _layoutNode:node withConstrainedSize:constrainedSize]; + [self _layoutNode:node withConstrainedSize:newConstrainedSize]; } } } From 292dc3c70b728c62e784845a3b5b844414b7a0e3 Mon Sep 17 00:00:00 2001 From: Huy Nguyen Date: Mon, 17 Jul 2017 11:42:52 +0000 Subject: [PATCH 03/16] [ASDataController] Clean up (#443) * Clean up ASDataController - Parameters passed to ASDataControllerCompletionBlock are no longer used. Remove them. - The value returned by _allocateNodesFromElements:andLayout:completion is not used. Remove it. - Elements are not allocated and measured in batches anymore. Remove the code that does batch allocation. - Remove RETURN_IF_NO_DATASOURCE. It's used only once and is not worth it. * Remove +parallelProcessorCount * Update CHANGELOG --- CHANGELOG.md | 1 + Source/Details/ASDataController.mm | 97 ++++++++++-------------------- 2 files changed, 34 insertions(+), 64 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index b3bd346c4e..a24bc323f2 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -12,6 +12,7 @@ - Fix a crash where scrolling a table view after entering editing mode could lead to bad internal states in the table. [Huy Nguyen](https://github.com/nguyenhuy) [#416](https://github.com/TextureGroup/Texture/pull/416/) - Fix a crash in collection view that occurs if batch updates are performed while scrolling [Huy Nguyen](https://github.com/nguyenhuy) [#378](https://github.com/TextureGroup/Texture/issues/378) - Some improvements in ASCollectionView [Huy Nguyen](https://github.com/nguyenhuy) [#407](https://github.com/TextureGroup/Texture/pull/407) +- Small refactors in ASDataController [Huy Nguyen](https://github.com/TextureGroup/Texture/pull/443) [#443](https://github.com/TextureGroup/Texture/pull/443) ##2.3.4 - [Yoga] Rewrite YOGA_TREE_CONTIGUOUS mode with improved behavior and cleaner integration [Scott Goodson](https://github.com/appleguy) diff --git a/Source/Details/ASDataController.mm b/Source/Details/ASDataController.mm index 2c2c5e75c8..33b69b8396 100644 --- a/Source/Details/ASDataController.mm +++ b/Source/Details/ASDataController.mm @@ -44,10 +44,8 @@ //#define LOG(...) NSLog(__VA_ARGS__) #define LOG(...) -#define RETURN_IF_NO_DATASOURCE(val) if (_dataSource == nil) { return val; } #define ASSERT_ON_EDITING_QUEUE ASDisplayNodeAssertNotNil(dispatch_get_specific(&kASDataControllerEditingQueueKey), @"%@ must be called on the editing transaction queue.", NSStringFromSelector(_cmd)) -const static NSUInteger kASDataControllerSizingCountPerProcessor = 5; const static char * kASDataControllerEditingQueueKey = "kASDataControllerEditingQueueKey"; const static char * kASDataControllerEditingQueueContext = "kASDataControllerEditingQueueContext"; @@ -123,18 +121,6 @@ typedef dispatch_block_t ASDataControllerCompletionBlock; return self; } -+ (NSUInteger)parallelProcessorCount -{ - static NSUInteger parallelProcessorCount; - - static dispatch_once_t onceToken; - dispatch_once(&onceToken, ^{ - parallelProcessorCount = [[NSProcessInfo processInfo] activeProcessorCount]; - }); - - return parallelProcessorCount; -} - - (id)layoutDelegate { ASDisplayNodeAssertMainThread(); @@ -151,34 +137,47 @@ typedef dispatch_block_t ASDataControllerCompletionBlock; #pragma mark - Cell Layout -- (void)batchAllocateNodesFromElements:(NSArray *)elements batchSize:(NSInteger)batchSize batchCompletion:(ASDataControllerCompletionBlock)batchCompletionHandler +- (void)_allocateNodesFromElements:(NSArray *)elements completion:(ASDataControllerCompletionBlock)completionHandler { ASSERT_ON_EDITING_QUEUE; - - if (elements.count == 0 || _dataSource == nil) { - batchCompletionHandler(); + + NSUInteger nodeCount = elements.count; + __weak id weakDataSource = _dataSource; + if (nodeCount == 0 || weakDataSource == nil) { + completionHandler(); return; } ASSignpostStart(ASSignpostDataControllerBatch); - if (batchSize == 0) { - batchSize = [[ASDataController class] parallelProcessorCount] * kASDataControllerSizingCountPerProcessor; - } - NSUInteger count = elements.count; - - // Processing in batches - for (NSUInteger i = 0; i < count; i += batchSize) { - NSRange batchedRange = NSMakeRange(i, MIN(count - i, batchSize)); - NSArray *batchedElements = [elements subarrayWithRange:batchedRange]; - { - as_activity_create_for_scope("Data controller batch"); - [self _allocateNodesFromElements:batchedElements]; - } - batchCompletionHandler(); + { + as_activity_create_for_scope("Data controller batch"); + + dispatch_queue_t queue = dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0); + ASDispatchApply(nodeCount, queue, 0, ^(size_t i) { + __strong id strongDataSource = weakDataSource; + if (strongDataSource == nil) { + return; + } + + // Allocate the node. + ASCollectionElement *context = elements[i]; + ASCellNode *node = context.node; + if (node == nil) { + ASDisplayNodeAssertNotNil(node, @"Node block created nil node; %@, %@", self, strongDataSource); + node = [[ASCellNode alloc] init]; // Fallback to avoid crash for production apps. + } + + // Layout the node if the size range is valid. + ASSizeRange sizeRange = context.constrainedSize; + if (ASSizeRangeHasSignificantArea(sizeRange)) { + [self _layoutNode:node withConstrainedSize:sizeRange]; + } + }); } - ASSignpostEndCustom(ASSignpostDataControllerBatch, self, 0, (_dataSource != nil ? ASSignpostColorDefault : ASSignpostColorRed)); + completionHandler(); + ASSignpostEndCustom(ASSignpostDataControllerBatch, self, 0, (weakDataSource != nil ? ASSignpostColorDefault : ASSignpostColorRed)); } /** @@ -193,36 +192,6 @@ typedef dispatch_block_t ASDataControllerCompletionBlock; node.frame = frame; } -// TODO Is returned array still needed? Can it be removed? -- (void)_allocateNodesFromElements:(NSArray *)elements -{ - ASSERT_ON_EDITING_QUEUE; - - NSUInteger nodeCount = elements.count; - if (!nodeCount || _dataSource == nil) { - return; - } - - dispatch_queue_t queue = dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0); - ASDispatchApply(nodeCount, queue, 0, ^(size_t i) { - RETURN_IF_NO_DATASOURCE(); - - // Allocate the node. - ASCollectionElement *context = elements[i]; - ASCellNode *node = context.node; - if (node == nil) { - ASDisplayNodeAssertNotNil(node, @"Node block created nil node; %@, %@", self, self.dataSource); - node = [[ASCellNode alloc] init]; // Fallback to avoid crash for production apps. - } - - // Layout the node if the size range is valid. - ASSizeRange sizeRange = context.constrainedSize; - if (ASSizeRangeHasSignificantArea(sizeRange)) { - [self _layoutNode:node withConstrainedSize:sizeRange]; - } - }); -} - #pragma mark - Data Source Access (Calling _dataSource) - (NSArray *)_allIndexPathsForItemsOfKind:(NSString *)kind inSections:(NSIndexSet *)sections @@ -592,7 +561,7 @@ typedef dispatch_block_t ASDataControllerCompletionBlock; NSArray *elementsToProcess = ASArrayByFlatMapping(newMap, ASCollectionElement *element, (element.nodeIfAllocated.calculatedLayout == nil ? element : nil)); - [self batchAllocateNodesFromElements:elementsToProcess batchSize:elementsToProcess.count batchCompletion:completion]; + [self _allocateNodesFromElements:elementsToProcess completion:completion]; } }); From eb5bde0791157f7b340bf3a25fef023649468b7f Mon Sep 17 00:00:00 2001 From: Huy Nguyen Date: Mon, 17 Jul 2017 11:43:31 +0000 Subject: [PATCH 04/16] [ASDataController ] Merge willUpdateWithChangeSet and didUpdateWithChangeSet delegate methods #trivial (#445) * Merge willUpdateWithChangeSet and didUpdateWithChangeSet delegate methods into one - After #420, there is no change occurs between those 2 methods. Having them separately doesn't achieve anything and can cause confusions. * Minor change --- Source/ASCollectionView.mm | 51 +++++++++++------------------ Source/ASTableView.mm | 23 +++++-------- Source/Details/ASDataController.h | 9 +---- Source/Details/ASDataController.mm | 5 +-- Source/Details/ASRangeController.h | 11 ++----- Source/Details/ASRangeController.mm | 10 ++---- 6 files changed, 33 insertions(+), 76 deletions(-) diff --git a/Source/ASCollectionView.mm b/Source/ASCollectionView.mm index 3d518b1a8a..da6eadcdc4 100644 --- a/Source/ASCollectionView.mm +++ b/Source/ASCollectionView.mm @@ -1954,37 +1954,7 @@ minimumLineSpacingForSectionAtIndex:(NSInteger)section #pragma mark - ASRangeControllerDelegate -- (void)rangeController:(ASRangeController *)rangeController willUpdateWithChangeSet:(_ASHierarchyChangeSet *)changeSet -{ - ASDisplayNodeAssertMainThread(); - - if (!self.asyncDataSource || _superIsPendingDataLoad) { - return; // if the asyncDataSource has become invalid while we are processing, ignore this request to avoid crashes - } - - if (changeSet.includesReloadData) { - //TODO Do we need to notify _layoutFacilitator? - return; - } - - for (_ASHierarchyItemChange *change in [changeSet itemChangesOfType:_ASHierarchyChangeTypeDelete]) { - [_layoutFacilitator collectionViewWillEditCellsAtIndexPaths:change.indexPaths batched:YES]; - } - - for (_ASHierarchySectionChange *change in [changeSet sectionChangesOfType:_ASHierarchyChangeTypeDelete]) { - [_layoutFacilitator collectionViewWillEditSectionsAtIndexSet:change.indexSet batched:YES]; - } - - for (_ASHierarchySectionChange *change in [changeSet sectionChangesOfType:_ASHierarchyChangeTypeInsert]) { - [_layoutFacilitator collectionViewWillEditSectionsAtIndexSet:change.indexSet batched:YES]; - } - - for (_ASHierarchyItemChange *change in [changeSet itemChangesOfType:_ASHierarchyChangeTypeInsert]) { - [_layoutFacilitator collectionViewWillEditCellsAtIndexPaths:change.indexPaths batched:YES]; - } -} - -- (void)rangeController:(ASRangeController *)rangeController didUpdateWithChangeSet:(_ASHierarchyChangeSet *)changeSet updates:(dispatch_block_t)updates +- (void)rangeController:(ASRangeController *)rangeController updateWithChangeSet:(_ASHierarchyChangeSet *)changeSet updates:(dispatch_block_t)updates { ASDisplayNodeAssertMainThread(); if (!self.asyncDataSource || _superIsPendingDataLoad) { @@ -1992,7 +1962,24 @@ minimumLineSpacingForSectionAtIndex:(NSInteger)section [changeSet executeCompletionHandlerWithFinished:NO]; return; // if the asyncDataSource has become invalid while we are processing, ignore this request to avoid crashes } - + + //TODO Do we need to notify _layoutFacilitator before reloadData? + for (_ASHierarchyItemChange *change in [changeSet itemChangesOfType:_ASHierarchyChangeTypeDelete]) { + [_layoutFacilitator collectionViewWillEditCellsAtIndexPaths:change.indexPaths batched:YES]; + } + + for (_ASHierarchySectionChange *change in [changeSet sectionChangesOfType:_ASHierarchyChangeTypeDelete]) { + [_layoutFacilitator collectionViewWillEditSectionsAtIndexSet:change.indexSet batched:YES]; + } + + for (_ASHierarchySectionChange *change in [changeSet sectionChangesOfType:_ASHierarchyChangeTypeInsert]) { + [_layoutFacilitator collectionViewWillEditSectionsAtIndexSet:change.indexSet batched:YES]; + } + + for (_ASHierarchyItemChange *change in [changeSet itemChangesOfType:_ASHierarchyChangeTypeInsert]) { + [_layoutFacilitator collectionViewWillEditCellsAtIndexPaths:change.indexPaths batched:YES]; + } + ASPerformBlockWithoutAnimation(!changeSet.animated, ^{ as_activity_scope(as_activity_create("Commit collection update", changeSet.rootActivity, OS_ACTIVITY_FLAG_DEFAULT)); if (changeSet.includesReloadData) { diff --git a/Source/ASTableView.mm b/Source/ASTableView.mm index c0dfd1428e..7cf6abcc30 100644 --- a/Source/ASTableView.mm +++ b/Source/ASTableView.mm @@ -1474,19 +1474,7 @@ static NSString * const kCellReuseIdentifier = @"_ASTableViewCell"; #pragma mark - ASRangeControllerDelegate -- (void)rangeController:(ASRangeController *)rangeController willUpdateWithChangeSet:(_ASHierarchyChangeSet *)changeSet -{ - ASDisplayNodeAssertMainThread(); - if (!self.asyncDataSource) { - return; // if the asyncDataSource has become invalid while we are processing, ignore this request to avoid crashes - } - - if (_automaticallyAdjustsContentOffset && !changeSet.includesReloadData) { - [self beginAdjustingContentOffset]; - } -} - -- (void)rangeController:(ASRangeController *)rangeController didUpdateWithChangeSet:(_ASHierarchyChangeSet *)changeSet updates:(dispatch_block_t)updates +- (void)rangeController:(ASRangeController *)rangeController updateWithChangeSet:(_ASHierarchyChangeSet *)changeSet updates:(dispatch_block_t)updates { ASDisplayNodeAssertMainThread(); if (!self.asyncDataSource || _updatingInResponseToInteractiveMove) { @@ -1494,7 +1482,7 @@ static NSString * const kCellReuseIdentifier = @"_ASTableViewCell"; [changeSet executeCompletionHandlerWithFinished:NO]; return; // if the asyncDataSource has become invalid while we are processing, ignore this request to avoid crashes } - + if (changeSet.includesReloadData) { LOG(@"UITableView reloadData"); ASPerformBlockWithoutAnimation(!changeSet.animated, ^{ @@ -1510,6 +1498,11 @@ static NSString * const kCellReuseIdentifier = @"_ASTableViewCell"; }); return; } + + BOOL shouldAdjustContentOffset = (_automaticallyAdjustsContentOffset && !changeSet.includesReloadData); + if (shouldAdjustContentOffset) { + [self beginAdjustingContentOffset]; + } NSUInteger numberOfUpdates = 0; @@ -1620,7 +1613,7 @@ static NSString * const kCellReuseIdentifier = @"_ASTableViewCell"; [_rangeController updateIfNeeded]; [self _scheduleCheckForBatchFetchingForNumberOfChanges:numberOfUpdates]; }); - if (_automaticallyAdjustsContentOffset) { + if (shouldAdjustContentOffset) { [self endAdjustingContentOffsetAnimated:changeSet.animated]; } [changeSet executeCompletionHandlerWithFinished:YES]; diff --git a/Source/Details/ASDataController.h b/Source/Details/ASDataController.h index e0e7142e3b..6fd85e89ae 100644 --- a/Source/Details/ASDataController.h +++ b/Source/Details/ASDataController.h @@ -108,13 +108,6 @@ extern NSString * const ASCollectionInvalidUpdateException; */ @protocol ASDataControllerDelegate -/** - * Called before updating with given change set. - * - * @param changeSet The change set that includes all updates - */ -- (void)dataController:(ASDataController *)dataController willUpdateWithChangeSet:(_ASHierarchyChangeSet *)changeSet; - /** * Called for change set updates. * @@ -126,7 +119,7 @@ extern NSString * const ASCollectionInvalidUpdateException; * It should be called at the time the backing view is ready to process the updates, * i.e inside the updates block of `-[UICollectionView performBatchUpdates:completion:] or after calling `-[UITableView beginUpdates]`. */ -- (void)dataController:(ASDataController *)dataController didUpdateWithChangeSet:(_ASHierarchyChangeSet *)changeSet updates:(dispatch_block_t)updates; +- (void)dataController:(ASDataController *)dataController updateWithChangeSet:(_ASHierarchyChangeSet *)changeSet updates:(dispatch_block_t)updates; @end diff --git a/Source/Details/ASDataController.mm b/Source/Details/ASDataController.mm index 33b69b8396..9e9b9348c2 100644 --- a/Source/Details/ASDataController.mm +++ b/Source/Details/ASDataController.mm @@ -536,11 +536,8 @@ typedef dispatch_block_t ASDataControllerCompletionBlock; dispatch_block_t completion = ^() { [_mainSerialQueue performBlockOnMainThread:^{ as_activity_scope_leave(&preparationScope); - // TODO Merge the two delegate methods below - [_delegate dataController:self willUpdateWithChangeSet:changeSet]; - // Step 4: Inform the delegate - [_delegate dataController:self didUpdateWithChangeSet:changeSet updates:^{ + [_delegate dataController:self updateWithChangeSet:changeSet updates:^{ // Step 5: Deploy the new data as "completed" // // Note that since the backing collection view might be busy responding to user events (e.g scrolling), diff --git a/Source/Details/ASRangeController.h b/Source/Details/ASRangeController.h index 7d707c538c..46a5cbf693 100644 --- a/Source/Details/ASRangeController.h +++ b/Source/Details/ASRangeController.h @@ -146,14 +146,7 @@ AS_SUBCLASSING_RESTRICTED @protocol ASRangeControllerDelegate /** - * Called before updating with given change set. - * - * @param changeSet The change set that includes all updates - */ -- (void)rangeController:(ASRangeController *)rangeController willUpdateWithChangeSet:(_ASHierarchyChangeSet *)changeSet; - -/** - * Called after updating with given change set. + * Called to update with given change set. * * @param changeSet The change set that includes all updates * @@ -163,7 +156,7 @@ AS_SUBCLASSING_RESTRICTED * It should be called at the time the backing view is ready to process the updates, * i.e inside the updates block of `-[UICollectionView performBatchUpdates:completion:] or after calling `-[UITableView beginUpdates]`. */ -- (void)rangeController:(ASRangeController *)rangeController didUpdateWithChangeSet:(_ASHierarchyChangeSet *)changeSet updates:(dispatch_block_t)updates; +- (void)rangeController:(ASRangeController *)rangeController updateWithChangeSet:(_ASHierarchyChangeSet *)changeSet updates:(dispatch_block_t)updates; @end diff --git a/Source/Details/ASRangeController.mm b/Source/Details/ASRangeController.mm index 0be9bf5b6f..818fb3a1d1 100644 --- a/Source/Details/ASRangeController.mm +++ b/Source/Details/ASRangeController.mm @@ -493,20 +493,14 @@ static UIApplicationState __ApplicationState = UIApplicationStateActive; #pragma mark - ASDataControllerDelegete -- (void)dataController:(ASDataController *)dataController willUpdateWithChangeSet:(_ASHierarchyChangeSet *)changeSet +- (void)dataController:(ASDataController *)dataController updateWithChangeSet:(_ASHierarchyChangeSet *)changeSet updates:(dispatch_block_t)updates { ASDisplayNodeAssertMainThread(); if (changeSet.includesReloadData) { [self _setVisibleNodes:nil]; } - [_delegate rangeController:self willUpdateWithChangeSet:changeSet]; -} - -- (void)dataController:(ASDataController *)dataController didUpdateWithChangeSet:(_ASHierarchyChangeSet *)changeSet updates:(dispatch_block_t)updates -{ - ASDisplayNodeAssertMainThread(); _rangeIsValid = NO; - [_delegate rangeController:self didUpdateWithChangeSet:changeSet updates:updates]; + [_delegate rangeController:self updateWithChangeSet:changeSet updates:updates]; } #pragma mark - Memory Management From 7af8f91e624054659a79147b92325f4e645cfba7 Mon Sep 17 00:00:00 2001 From: Huy Nguyen Date: Mon, 17 Jul 2017 21:01:00 +0000 Subject: [PATCH 05/16] Remove unused flow layout reference in ASPagerNode (#452) --- Source/ASPagerNode.m | 5 ----- 1 file changed, 5 deletions(-) diff --git a/Source/ASPagerNode.m b/Source/ASPagerNode.m index e144090aa2..712902371b 100644 --- a/Source/ASPagerNode.m +++ b/Source/ASPagerNode.m @@ -27,8 +27,6 @@ @interface ASPagerNode () { - ASPagerFlowLayout *_flowLayout; - __weak id _pagerDataSource; ASPagerNodeProxy *_proxyDataSource; struct { @@ -65,9 +63,6 @@ { ASDisplayNodeAssert([flowLayout isKindOfClass:[ASPagerFlowLayout class]], @"ASPagerNode requires a flow layout."); self = [super initWithCollectionViewLayout:flowLayout]; - if (self != nil) { - _flowLayout = flowLayout; - } return self; } From 78c133e44c33b80ddf702d29248b278bbd8be8da Mon Sep 17 00:00:00 2001 From: Huy Nguyen Date: Tue, 18 Jul 2017 10:08:12 +0000 Subject: [PATCH 06/16] [ASCollectionLayout] Add ASCollectionGalleryLayoutSizeProviding (#451) * Add ASCollectionGalleryLayoutSizeProviding - This allows users to return different sizes based on certain conditions, such as the collection node's bounds or grid constants. - ASPagerNode will also act as a size provider to ensure all pages have an up-to-date size that is its bounds. * Update CHANGELOG * ASPagerNode to use gallery layout delegate if told to --- AsyncDisplayKit.xcodeproj/project.pbxproj | 4 +++ CHANGELOG.md | 2 +- Source/ASPagerNode+Beta.h | 19 +++++++++++++ Source/ASPagerNode.m | 24 ++++++++++++++++- .../Details/ASCollectionFlowLayoutDelegate.m | 6 ++--- .../ASCollectionGalleryLayoutDelegate.h | 21 ++++++++++++++- .../ASCollectionGalleryLayoutDelegate.m | 27 ++++++++++++------- Source/Details/ASCollectionLayoutContext.m | 1 - Source/Details/ASCollectionLayoutDelegate.h | 2 ++ Source/Details/ASCollectionLayoutState.h | 7 +++++ Source/Details/ASCollectionLayoutState.mm | 7 +++++ Source/Private/ASCollectionLayout.mm | 5 +--- .../ASCollectionView/Sample/ViewController.m | 14 +++++++--- .../Sample/MosaicCollectionLayoutDelegate.m | 2 ++ 14 files changed, 117 insertions(+), 24 deletions(-) create mode 100644 Source/ASPagerNode+Beta.h diff --git a/AsyncDisplayKit.xcodeproj/project.pbxproj b/AsyncDisplayKit.xcodeproj/project.pbxproj index edcbb825ed..ba9df64e5f 100644 --- a/AsyncDisplayKit.xcodeproj/project.pbxproj +++ b/AsyncDisplayKit.xcodeproj/project.pbxproj @@ -425,6 +425,7 @@ DECBD6EA1BE56E1900CF4905 /* ASButtonNode.mm in Sources */ = {isa = PBXBuildFile; fileRef = DECBD6E61BE56E1900CF4905 /* ASButtonNode.mm */; }; DEFAD8131CC48914000527C4 /* ASVideoNode.mm in Sources */ = {isa = PBXBuildFile; fileRef = AEEC47E01C20C2DD00EC1693 /* ASVideoNode.mm */; }; E51B78BF1F028ABF00E32604 /* ASLayoutFlatteningTests.m in Sources */ = {isa = PBXBuildFile; fileRef = E51B78BD1F01A0EE00E32604 /* ASLayoutFlatteningTests.m */; }; + E54E00721F1D3828000B30D7 /* ASPagerNode+Beta.h in Headers */ = {isa = PBXBuildFile; fileRef = E54E00711F1D3828000B30D7 /* ASPagerNode+Beta.h */; settings = {ATTRIBUTES = (Public, ); }; }; E54E81FC1EB357BD00FFE8E1 /* ASPageTable.h in Headers */ = {isa = PBXBuildFile; fileRef = E54E81FA1EB357BD00FFE8E1 /* ASPageTable.h */; }; E54E81FD1EB357BD00FFE8E1 /* ASPageTable.m in Sources */ = {isa = PBXBuildFile; fileRef = E54E81FB1EB357BD00FFE8E1 /* ASPageTable.m */; }; E55D86331CA8A14000A0C26F /* ASLayoutElement.mm in Sources */ = {isa = PBXBuildFile; fileRef = E55D86311CA8A14000A0C26F /* ASLayoutElement.mm */; }; @@ -912,6 +913,7 @@ E51B78BD1F01A0EE00E32604 /* ASLayoutFlatteningTests.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = ASLayoutFlatteningTests.m; sourceTree = ""; }; E52405B21C8FEF03004DC8E7 /* ASLayoutTransition.mm */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.cpp.objcpp; path = ASLayoutTransition.mm; sourceTree = ""; }; E52405B41C8FEF16004DC8E7 /* ASLayoutTransition.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = ASLayoutTransition.h; sourceTree = ""; }; + E54E00711F1D3828000B30D7 /* ASPagerNode+Beta.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = "ASPagerNode+Beta.h"; sourceTree = ""; }; E54E81FA1EB357BD00FFE8E1 /* ASPageTable.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = ASPageTable.h; sourceTree = ""; }; E54E81FB1EB357BD00FFE8E1 /* ASPageTable.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = ASPageTable.m; sourceTree = ""; }; E55D86311CA8A14000A0C26F /* ASLayoutElement.mm */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.cpp.objcpp; path = ASLayoutElement.mm; sourceTree = ""; }; @@ -1101,6 +1103,7 @@ 698371DA1E4379CD00437585 /* ASNodeController+Beta.m */, 25E327541C16819500A2170C /* ASPagerNode.h */, 25E327551C16819500A2170C /* ASPagerNode.m */, + E54E00711F1D3828000B30D7 /* ASPagerNode+Beta.h */, A2763D771CBDD57D00A9ADBD /* ASPINRemoteImageDownloader.h */, A2763D781CBDD57D00A9ADBD /* ASPINRemoteImageDownloader.m */, CCBBBF5C1EB161760069AA91 /* ASRangeManagingNode.h */, @@ -1708,6 +1711,7 @@ isa = PBXHeadersBuildPhase; buildActionMask = 2147483647; files = ( + E54E00721F1D3828000B30D7 /* ASPagerNode+Beta.h in Headers */, E5B225281F1790D6001E1431 /* ASHashing.h in Headers */, CC034A131E649F1300626263 /* AsyncDisplayKit+IGListKitMethods.h in Headers */, 693A1DCA1ECC944E00D0C9D2 /* IGListAdapter+AsyncDisplayKit.h in Headers */, diff --git a/CHANGELOG.md b/CHANGELOG.md index a24bc323f2..d9bb2deddd 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -4,7 +4,7 @@ - [ASCollectionView] Add delegate bridging and index space translation for missing UICollectionViewLayout properties. [Scott Goodson](https://github.com/appleguy) - [ASTextNode2] Add initial implementation for link handling. [Scott Goodson](https://github.com/appleguy) [#396](https://github.com/TextureGroup/Texture/pull/396) - [ASTextNode2] Provide compile flag to globally enable new implementation of ASTextNode: ASTEXTNODE_EXPERIMENT_GLOBAL_ENABLE. [Scott Goodson](https://github.com/appleguy) [#396](https://github.com/TextureGroup/Texture/pull/410) - - Add ASCollectionGalleryLayoutDelegate - an async collection layout that makes same-size collections (e.g photo galleries, pagers, etc) fast and lightweight! [Huy Nguyen](https://github.com/nguyenhuy/) [#76](https://github.com/TextureGroup/Texture/pull/76) + - Add ASCollectionGalleryLayoutDelegate - an async collection layout that makes same-size collections (e.g photo galleries, pagers, etc) fast and lightweight! [Huy Nguyen](https://github.com/nguyenhuy/) [#76](https://github.com/TextureGroup/Texture/pull/76) [#451](https://github.com/TextureGroup/Texture/pull/451) ##2.3.5 - Fix an issue where inserting/deleting sections could lead to inconsistent supplementary element behavior. [Adlai Holler](https://github.com/Adlai-Holler) diff --git a/Source/ASPagerNode+Beta.h b/Source/ASPagerNode+Beta.h new file mode 100644 index 0000000000..bd963ec19f --- /dev/null +++ b/Source/ASPagerNode+Beta.h @@ -0,0 +1,19 @@ +// +// ASPagerNode+Beta.h +// Texture +// +// Copyright (c) 2017-present, Pinterest, Inc. All rights reserved. +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// + +#import + +@interface ASPagerNode (Beta) + +- (instancetype)initUsingAsyncCollectionLayout; + +@end diff --git a/Source/ASPagerNode.m b/Source/ASPagerNode.m index 712902371b..a5f84178c7 100644 --- a/Source/ASPagerNode.m +++ b/Source/ASPagerNode.m @@ -16,6 +16,10 @@ // #import +#import + +#import +#import #import #import #import @@ -25,7 +29,7 @@ #import #import -@interface ASPagerNode () +@interface ASPagerNode () { __weak id _pagerDataSource; ASPagerNodeProxy *_proxyDataSource; @@ -66,6 +70,16 @@ return self; } +- (instancetype)initUsingAsyncCollectionLayout +{ + ASCollectionGalleryLayoutDelegate *layoutDelegate = [[ASCollectionGalleryLayoutDelegate alloc] initWithScrollableDirections:ASScrollDirectionHorizontalDirections]; + self = [super initWithLayoutDelegate:layoutDelegate layoutFacilitator:nil]; + if (self) { + layoutDelegate.sizeProvider = self; + } + return self; +} + #pragma mark - ASDisplayNode - (void)didLoad @@ -123,6 +137,14 @@ return indexPath.row; } +#pragma mark - ASCollectionGalleryLayoutSizeProviding + +- (CGSize)sizeForElements:(ASElementMap *)elements +{ + ASDisplayNodeAssertMainThread(); + return self.bounds.size; +} + #pragma mark - ASCollectionDataSource - (ASCellNodeBlock)collectionNode:(ASCollectionNode *)collectionNode nodeBlockForItemAtIndexPath:(NSIndexPath *)indexPath diff --git a/Source/Details/ASCollectionFlowLayoutDelegate.m b/Source/Details/ASCollectionFlowLayoutDelegate.m index 855f3791c2..79287a5944 100644 --- a/Source/Details/ASCollectionFlowLayoutDelegate.m +++ b/Source/Details/ASCollectionFlowLayoutDelegate.m @@ -46,11 +46,13 @@ - (ASScrollDirection)scrollableDirections { + ASDisplayNodeAssertMainThread(); return _scrollableDirections; } - (id)additionalInfoForLayoutWithElements:(ASElementMap *)elements { + ASDisplayNodeAssertMainThread(); return nil; } @@ -59,9 +61,7 @@ ASElementMap *elements = context.elements; NSMutableArray *children = ASArrayByFlatMapping(elements.itemElements, ASCollectionElement *element, element.node); if (children.count == 0) { - return [[ASCollectionLayoutState alloc] initWithContext:context - contentSize:CGSizeZero - elementToLayoutAttributesTable:[NSMapTable elementToLayoutAttributesTable]]; + return [[ASCollectionLayoutState alloc] initWithContext:context]; } ASStackLayoutSpec *stackSpec = [ASStackLayoutSpec stackLayoutSpecWithDirection:ASStackLayoutDirectionHorizontal diff --git a/Source/Details/ASCollectionGalleryLayoutDelegate.h b/Source/Details/ASCollectionGalleryLayoutDelegate.h index 2b9e64c7c1..d2d41b7882 100644 --- a/Source/Details/ASCollectionGalleryLayoutDelegate.h +++ b/Source/Details/ASCollectionGalleryLayoutDelegate.h @@ -13,8 +13,25 @@ #import #import +@class ASElementMap; + NS_ASSUME_NONNULL_BEGIN +@protocol ASCollectionGalleryLayoutSizeProviding + +/** + * Returns the fixed size of each and every element. + * + * @discussion This method will only be called on main thread. + * + * @param elements All elements to be sized. + * + * @return The elements' size + */ +- (CGSize)sizeForElements:(ASElementMap *)elements; + +@end + /** * A thread-safe layout delegate that arranges items with the same size into a flow layout. * @@ -23,7 +40,9 @@ NS_ASSUME_NONNULL_BEGIN AS_SUBCLASSING_RESTRICTED @interface ASCollectionGalleryLayoutDelegate : NSObject -- (instancetype)initWithScrollableDirections:(ASScrollDirection)scrollableDirections itemSize:(CGSize)itemSize NS_DESIGNATED_INITIALIZER; +@property (nonatomic, weak) id sizeProvider; + +- (instancetype)initWithScrollableDirections:(ASScrollDirection)scrollableDirections NS_DESIGNATED_INITIALIZER; - (instancetype)init __unavailable; diff --git a/Source/Details/ASCollectionGalleryLayoutDelegate.m b/Source/Details/ASCollectionGalleryLayoutDelegate.m index 0c98f0fbf1..5ab5c23d22 100644 --- a/Source/Details/ASCollectionGalleryLayoutDelegate.m +++ b/Source/Details/ASCollectionGalleryLayoutDelegate.m @@ -31,40 +31,47 @@ CGSize _itemSize; } -- (instancetype)initWithScrollableDirections:(ASScrollDirection)scrollableDirections itemSize:(CGSize)itemSize +- (instancetype)initWithScrollableDirections:(ASScrollDirection)scrollableDirections { self = [super init]; if (self) { - ASDisplayNodeAssertFalse(CGSizeEqualToSize(CGSizeZero, itemSize)); _scrollableDirections = scrollableDirections; - _itemSize = itemSize; } return self; } - (ASScrollDirection)scrollableDirections { + ASDisplayNodeAssertMainThread(); return _scrollableDirections; } - (id)additionalInfoForLayoutWithElements:(ASElementMap *)elements { - return [NSValue valueWithCGSize:_itemSize]; + ASDisplayNodeAssertMainThread(); + if (_sizeProvider == nil) { + return nil; + } + + return [NSValue valueWithCGSize:[_sizeProvider sizeForElements:elements]]; } + (ASCollectionLayoutState *)calculateLayoutWithContext:(ASCollectionLayoutContext *)context { ASElementMap *elements = context.elements; CGSize pageSize = context.viewportSize; - CGSize itemSize = ((NSValue *)context.additionalInfo).CGSizeValue; ASScrollDirection scrollableDirections = context.scrollableDirections; + + CGSize itemSize = context.additionalInfo ? ((NSValue *)context.additionalInfo).CGSizeValue : CGSizeZero; + if (CGSizeEqualToSize(CGSizeZero, itemSize)) { + return [[ASCollectionLayoutState alloc] initWithContext:context]; + } + NSMutableArray<_ASGalleryLayoutItem *> *children = ASArrayByFlatMapping(elements.itemElements, - ASCollectionElement *element, - [[_ASGalleryLayoutItem alloc] initWithItemSize:itemSize collectionElement:element]); + ASCollectionElement *element, + [[_ASGalleryLayoutItem alloc] initWithItemSize:itemSize collectionElement:element]); if (children.count == 0) { - return [[ASCollectionLayoutState alloc] initWithContext:context - contentSize:CGSizeZero - elementToLayoutAttributesTable:[NSMapTable weakToStrongObjectsMapTable]]; + return [[ASCollectionLayoutState alloc] initWithContext:context]; } // Use a stack spec to calculate layout content size and frames of all elements without actually measuring each element diff --git a/Source/Details/ASCollectionLayoutContext.m b/Source/Details/ASCollectionLayoutContext.m index 6620e391e8..eff48afb64 100644 --- a/Source/Details/ASCollectionLayoutContext.m +++ b/Source/Details/ASCollectionLayoutContext.m @@ -37,7 +37,6 @@ { self = [super init]; if (self) { - ASDisplayNodeAssertTrue([layoutDelegateClass conformsToProtocol:@protocol(ASCollectionLayoutDelegate)]); _viewportSize = viewportSize; _scrollableDirections = scrollableDirections; _elements = elements; diff --git a/Source/Details/ASCollectionLayoutDelegate.h b/Source/Details/ASCollectionLayoutDelegate.h index 9ef3da8747..4e75b25fcd 100644 --- a/Source/Details/ASCollectionLayoutDelegate.h +++ b/Source/Details/ASCollectionLayoutDelegate.h @@ -30,6 +30,8 @@ NS_ASSUME_NONNULL_BEGIN * It will be available in the context parameter in +calculateLayoutWithContext: * * @return The scrollable directions. + * + * @discusstion This method will be called on main thread. */ - (ASScrollDirection)scrollableDirections; diff --git a/Source/Details/ASCollectionLayoutState.h b/Source/Details/ASCollectionLayoutState.h index 1804c58a31..c21a37dff4 100644 --- a/Source/Details/ASCollectionLayoutState.h +++ b/Source/Details/ASCollectionLayoutState.h @@ -56,6 +56,13 @@ AS_SUBCLASSING_RESTRICTED contentSize:(CGSize)contentSize elementToLayoutAttributesTable:(NSMapTable *)table NS_DESIGNATED_INITIALIZER; +/** + * Convenience initializer. Returns an object with zero content size and an empty table. + * + * @param context The context used to calculate this object + */ +- (instancetype)initWithContext:(ASCollectionLayoutContext *)context; + /** * Convenience initializer. * diff --git a/Source/Details/ASCollectionLayoutState.mm b/Source/Details/ASCollectionLayoutState.mm index 0b2616a220..81f003ab41 100644 --- a/Source/Details/ASCollectionLayoutState.mm +++ b/Source/Details/ASCollectionLayoutState.mm @@ -39,6 +39,13 @@ ASPageToLayoutAttributesTable *_unmeasuredPageToLayoutAttributesTable; } +- (instancetype)initWithContext:(ASCollectionLayoutContext *)context +{ + return [self initWithContext:context + contentSize:CGSizeZero +elementToLayoutAttributesTable:[NSMapTable elementToLayoutAttributesTable]]; +} + - (instancetype)initWithContext:(ASCollectionLayoutContext *)context layout:(ASLayout *)layout getElementBlock:(ASCollectionElement *(^)(ASLayout *))getElementBlock diff --git a/Source/Private/ASCollectionLayout.mm b/Source/Private/ASCollectionLayout.mm index 935a3d680d..2ecc312c78 100644 --- a/Source/Private/ASCollectionLayout.mm +++ b/Source/Private/ASCollectionLayout.mm @@ -85,12 +85,9 @@ static const ASScrollDirection kASStaticScrollDirection = (ASScrollDirectionRigh + (ASCollectionLayoutState *)calculateLayoutWithContext:(ASCollectionLayoutContext *)context { if (context.elements == nil) { - return [[ASCollectionLayoutState alloc] initWithContext:context - contentSize:CGSizeZero - elementToLayoutAttributesTable:[NSMapTable elementToLayoutAttributesTable]]; + return [[ASCollectionLayoutState alloc] initWithContext:context]; } - ASDisplayNodeAssertTrue([context.layoutDelegateClass conformsToProtocol:@protocol(ASCollectionLayoutDelegate)]); ASCollectionLayoutState *layout = [context.layoutDelegateClass calculateLayoutWithContext:context]; [context.layoutCache setLayout:layout forContext:context]; diff --git a/examples/ASCollectionView/Sample/ViewController.m b/examples/ASCollectionView/Sample/ViewController.m index 13bfe64c72..36391e8f37 100644 --- a/examples/ASCollectionView/Sample/ViewController.m +++ b/examples/ASCollectionView/Sample/ViewController.m @@ -23,7 +23,7 @@ #define ASYNC_COLLECTION_LAYOUT 0 -@interface ViewController () +@interface ViewController () @property (nonatomic, strong) ASCollectionNode *collectionNode; @property (nonatomic, strong) NSArray *data; @@ -47,8 +47,8 @@ [super viewDidLoad]; #if ASYNC_COLLECTION_LAYOUT - id layoutDelegate = [[ASCollectionGalleryLayoutDelegate alloc] initWithScrollableDirections:ASScrollDirectionVerticalDirections - itemSize:CGSizeMake(180, 90)]; + ASCollectionGalleryLayoutDelegate *layoutDelegate = [[ASCollectionGalleryLayoutDelegate alloc] initWithScrollableDirections:ASScrollDirectionVerticalDirections]; + layoutDelegate.sizeProvider = self; self.collectionNode = [[ASCollectionNode alloc] initWithLayoutDelegate:layoutDelegate layoutFacilitator:nil]; #else UICollectionViewFlowLayout *layout = [[UICollectionViewFlowLayout alloc] init]; @@ -108,6 +108,14 @@ [self.collectionNode reloadData]; } +#pragma mark - ASCollectionGalleryLayoutSizeProviding + +- (CGSize)sizeForElements:(ASElementMap *)elements +{ + ASDisplayNodeAssertMainThread(); + return CGSizeMake(180, 90); +} + #pragma mark - ASCollectionView Data Source - (ASCellNodeBlock)collectionNode:(ASCollectionNode *)collectionNode nodeBlockForItemAtIndexPath:(NSIndexPath *)indexPath; diff --git a/examples/CustomCollectionView/Sample/MosaicCollectionLayoutDelegate.m b/examples/CustomCollectionView/Sample/MosaicCollectionLayoutDelegate.m index a7383f294b..196925c912 100644 --- a/examples/CustomCollectionView/Sample/MosaicCollectionLayoutDelegate.m +++ b/examples/CustomCollectionView/Sample/MosaicCollectionLayoutDelegate.m @@ -36,11 +36,13 @@ - (ASScrollDirection)scrollableDirections { + ASDisplayNodeAssertMainThread(); return ASScrollDirectionVerticalDirections; } - (id)additionalInfoForLayoutWithElements:(ASElementMap *)elements { + ASDisplayNodeAssertMainThread(); return _info; } From 6880ed4aa91df98439b17918347782a259d24bde Mon Sep 17 00:00:00 2001 From: Huy Nguyen Date: Tue, 18 Jul 2017 17:05:59 +0000 Subject: [PATCH 07/16] Add ASPagerNode+Beta to umbrella header (#454) --- Source/AsyncDisplayKit.h | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/Source/AsyncDisplayKit.h b/Source/AsyncDisplayKit.h index ed61ea4c5c..73a5288bcc 100644 --- a/Source/AsyncDisplayKit.h +++ b/Source/AsyncDisplayKit.h @@ -42,6 +42,7 @@ #import #import #import +#import #import #import #import @@ -61,6 +62,7 @@ #import #import +#import #import #import @@ -125,7 +127,5 @@ #import #import -#import - #import #import From 01715f09d883c2b768d0eb711824244adb46bc7c Mon Sep 17 00:00:00 2001 From: Huy Nguyen Date: Tue, 18 Jul 2017 19:44:27 +0000 Subject: [PATCH 08/16] [ASDisplayNode] Fix infinite layout loop (#455) * Fix infinite layout loop - The problem will occur if a node does either of the followings: 1. Keeps using a stale pending layout over a calculated layout 2. Doesn't update the next layout's version after calling _setNeedsLayoutFromAbove. * Update CHANGELOG --- CHANGELOG.md | 3 ++- Source/ASDisplayNode+Layout.mm | 13 +++++++++---- Source/ASDisplayNode.mm | 2 +- 3 files changed, 12 insertions(+), 6 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index d9bb2deddd..8b83bea11e 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -4,7 +4,8 @@ - [ASCollectionView] Add delegate bridging and index space translation for missing UICollectionViewLayout properties. [Scott Goodson](https://github.com/appleguy) - [ASTextNode2] Add initial implementation for link handling. [Scott Goodson](https://github.com/appleguy) [#396](https://github.com/TextureGroup/Texture/pull/396) - [ASTextNode2] Provide compile flag to globally enable new implementation of ASTextNode: ASTEXTNODE_EXPERIMENT_GLOBAL_ENABLE. [Scott Goodson](https://github.com/appleguy) [#396](https://github.com/TextureGroup/Texture/pull/410) - - Add ASCollectionGalleryLayoutDelegate - an async collection layout that makes same-size collections (e.g photo galleries, pagers, etc) fast and lightweight! [Huy Nguyen](https://github.com/nguyenhuy/) [#76](https://github.com/TextureGroup/Texture/pull/76) [#451](https://github.com/TextureGroup/Texture/pull/451) +- Add ASCollectionGalleryLayoutDelegate - an async collection layout that makes same-size collections (e.g photo galleries, pagers, etc) fast and lightweight! [Huy Nguyen](https://github.com/nguyenhuy/) [#76](https://github.com/TextureGroup/Texture/pull/76) [#451](https://github.com/TextureGroup/Texture/pull/451) +- Fix an issue that causes infinite layout loop in ASDisplayNode after [#428](https://github.com/TextureGroup/Texture/pull/428) [Huy Nguyen](https://github.com/nguyenhuy) [#455](https://github.com/TextureGroup/Texture/pull/455) ##2.3.5 - Fix an issue where inserting/deleting sections could lead to inconsistent supplementary element behavior. [Adlai Holler](https://github.com/Adlai-Holler) diff --git a/Source/ASDisplayNode+Layout.mm b/Source/ASDisplayNode+Layout.mm index 6b73c1bc5e..6d44b4a157 100644 --- a/Source/ASDisplayNode+Layout.mm +++ b/Source/ASDisplayNode+Layout.mm @@ -302,10 +302,10 @@ ASPrimitiveTraitCollectionDeprecatedImplementation } CGSize boundsSizeForLayout = ASCeilSizeValues(bounds.size); - + // Prefer _pendingDisplayNodeLayout over _calculatedDisplayNodeLayout (if exists, it's the newest) // If there is no _pending, check if _calculated is valid to reuse (avoiding recalculation below). - if (_pendingDisplayNodeLayout == nullptr) { + if (_pendingDisplayNodeLayout == nullptr || _pendingDisplayNodeLayout->version < _layoutVersion) { if (_calculatedDisplayNodeLayout->version >= _layoutVersion && (_calculatedDisplayNodeLayout->requestedLayoutFromAbove == YES || CGSizeEqualToSize(_calculatedDisplayNodeLayout->layout.size, boundsSizeForLayout))) { @@ -352,8 +352,10 @@ ASPrimitiveTraitCollectionDeprecatedImplementation ASLayout *layout = [self calculateLayoutThatFits:constrainedSize restrictedToSize:self.style.size relativeToParentSize:boundsSizeForLayout]; - nextLayout = std::make_shared(layout, constrainedSize, boundsSizeForLayout, version); + // Now that the constrained size of pending layout might have been reused, the layout is useless + // Release it and any orphaned subnodes it retains + _pendingDisplayNodeLayout = nullptr; } if (didCreateNewContext) { @@ -373,6 +375,9 @@ ASPrimitiveTraitCollectionDeprecatedImplementation // particular ASLayout object, and shouldn't loop asking again unless we have a different ASLayout. nextLayout->requestedLayoutFromAbove = YES; [self _setNeedsLayoutFromAbove]; + // Update the layout's version here because _setNeedsLayoutFromAbove calls __setNeedsLayout which in turn increases _layoutVersion + // Failing to do this will cause the layout to be invalid immediately + nextLayout->version = _layoutVersion; } // Prepare to transition to nextLayout @@ -956,7 +961,7 @@ ASPrimitiveTraitCollectionDeprecatedImplementation _unflattenedLayout = displayNodeLayout->layout; displayNodeLayout->layout = [_unflattenedLayout filteredNodeLayoutTree]; } - + _calculatedDisplayNodeLayout = displayNodeLayout; } diff --git a/Source/ASDisplayNode.mm b/Source/ASDisplayNode.mm index 84862316b6..2855fa91a8 100644 --- a/Source/ASDisplayNode.mm +++ b/Source/ASDisplayNode.mm @@ -920,7 +920,7 @@ static ASDisplayNodeMethodOverrides GetASDisplayNodeMethodOverrides(Class c) // This method will confirm that the layout is up to date (and update if needed). // Importantly, it will also APPLY the layout to all of our subnodes if (unless parent is transitioning). [self _locked_measureNodeWithBoundsIfNecessary:bounds]; - + [self _locked_layoutPlaceholderIfNecessary]; } From 8b397cd04eb71fe2aa2b3434d234bc1c55576abd Mon Sep 17 00:00:00 2001 From: Huy Nguyen Date: Mon, 24 Jul 2017 18:00:43 +0000 Subject: [PATCH 09/16] [Layout transition] Invalidate calculated layout if transitioning using the same size range (#464) * Invalidate the calculated layout transitioning using the same size range - It's possible that -transitionLayoutWithSizeRange: can be called with the same size range used for previous layout passes. In that case, it's necessary to invalidate the current layout to avoid it being reused later on in the process. * Fix CHANGELOG --- CHANGELOG.md | 1 + Source/ASDisplayNode+Layout.mm | 40 ++++++++++++++++++++++++---------- 2 files changed, 30 insertions(+), 11 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 8b83bea11e..f7267c240c 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -6,6 +6,7 @@ - [ASTextNode2] Provide compile flag to globally enable new implementation of ASTextNode: ASTEXTNODE_EXPERIMENT_GLOBAL_ENABLE. [Scott Goodson](https://github.com/appleguy) [#396](https://github.com/TextureGroup/Texture/pull/410) - Add ASCollectionGalleryLayoutDelegate - an async collection layout that makes same-size collections (e.g photo galleries, pagers, etc) fast and lightweight! [Huy Nguyen](https://github.com/nguyenhuy/) [#76](https://github.com/TextureGroup/Texture/pull/76) [#451](https://github.com/TextureGroup/Texture/pull/451) - Fix an issue that causes infinite layout loop in ASDisplayNode after [#428](https://github.com/TextureGroup/Texture/pull/428) [Huy Nguyen](https://github.com/nguyenhuy) [#455](https://github.com/TextureGroup/Texture/pull/455) +- Fix an issue in layout transition that causes it to unexpectedly use the old layout [Huy Nguyen](https://github.com/nguyenhuy) [#464](https://github.com/TextureGroup/Texture/pull/464) ##2.3.5 - Fix an issue where inserting/deleting sections could lead to inconsistent supplementary element behavior. [Adlai Holler](https://github.com/Adlai-Holler) diff --git a/Source/ASDisplayNode+Layout.mm b/Source/ASDisplayNode+Layout.mm index 6d44b4a157..31f0739c73 100644 --- a/Source/ASDisplayNode+Layout.mm +++ b/Source/ASDisplayNode+Layout.mm @@ -185,6 +185,11 @@ ASPrimitiveTraitCollectionDeprecatedImplementation - (ASSizeRange)constrainedSizeForCalculatedLayout { ASDN::MutexLocker l(__instanceLock__); + return [self _locked_constrainedSizeForCalculatedLayout]; +} + +- (ASSizeRange)_locked_constrainedSizeForCalculatedLayout +{ if (_pendingDisplayNodeLayout != nullptr) { return _pendingDisplayNodeLayout->constrainedSize; } @@ -478,6 +483,11 @@ ASPrimitiveTraitCollectionDeprecatedImplementation - (BOOL)_isLayoutTransitionInvalid { ASDN::MutexLocker l(__instanceLock__); + return [self _locked_isLayoutTransitionValid]; +} + +- (BOOL)_locked_isLayoutTransitionValid +{ if (ASHierarchyStateIncludesLayoutPending(_hierarchyState)) { ASLayoutElementContext *context = ASLayoutElementGetCurrentContext(); if (context == nil || _pendingTransitionID != context.transitionID) { @@ -510,9 +520,6 @@ ASPrimitiveTraitCollectionDeprecatedImplementation measurementCompletion:(void(^)())completion { ASDisplayNodeAssertMainThread(); - - [self setNeedsLayout]; - [self transitionLayoutWithSizeRange:[self _locked_constrainedSizeForLayoutPass] animated:animated shouldMeasureAsync:shouldMeasureAsync @@ -536,17 +543,28 @@ ASPrimitiveTraitCollectionDeprecatedImplementation return; } - // Check if we are a subnode in a layout transition. - // In this case no measurement is needed as we're part of the layout transition. - if ([self _isLayoutTransitionInvalid]) { - return; - } - + BOOL shouldInvalidateLayout = NO; { ASDN::MutexLocker l(__instanceLock__); - ASDisplayNodeAssert(ASHierarchyStateIncludesLayoutPending(_hierarchyState) == NO, @"Can't start a transition when one of the supernodes is performing one."); + + // Check if we are a subnode in a layout transition. + // In this case no measurement is needed as we're part of the layout transition. + if ([self _locked_isLayoutTransitionValid]) { + return; + } + + if (ASHierarchyStateIncludesLayoutPending(_hierarchyState)) { + ASDisplayNodeAssert(NO, @"Can't start a transition when one of the supernodes is performing one."); + return; + } + + shouldInvalidateLayout = ASSizeRangeEqualToSizeRange([self _locked_constrainedSizeForCalculatedLayout], constrainedSize); } - + + if (shouldInvalidateLayout) { + [self setNeedsLayout]; + } + // Every new layout transition has a transition id associated to check in subsequent transitions for cancelling int32_t transitionID = [self _startNewTransition]; as_log_verbose(ASLayoutLog(), "Transition ID is %d", transitionID); From 7b054582cdc681d60dc5d1d658c3b459b07bda49 Mon Sep 17 00:00:00 2001 From: Ofer Morag Date: Wed, 26 Jul 2017 00:41:35 +0300 Subject: [PATCH 10/16] Update image-modification-block.md (#474) * Update image-modification-block.md Just some minor proposals for improvements: - Removed white spaces, which caused the first line in the image modification block to be shown a bit "messy" - Using the Null coalescing operator in the following line * Update image-modification-block.md Now, with the second change as well. --- docs/_docs/image-modification-block.md | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/docs/_docs/image-modification-block.md b/docs/_docs/image-modification-block.md index d4595831c0..1e2e2716aa 100755 --- a/docs/_docs/image-modification-block.md +++ b/docs/_docs/image-modification-block.md @@ -17,10 +17,10 @@ By assigning an `imageModificationBlock` to your imageNode, you can define a set
 _backgroundImageNode.imageModificationBlock = ^(UIImage *image) {
 	UIImage *newImage = [image applyBlurWithRadius:30
-										 tintColor:[UIColor colorWithWhite:0.5 alpha:0.3]
-							 saturationDeltaFactor:1.8
-							 			 maskImage:nil];
-	return newImage ? newImage : image;
+		tintColor:[UIColor colorWithWhite:0.5 alpha:0.3]
+		saturationDeltaFactor:1.8
+		maskImage:nil];
+	return newImage ?: image;
 };
 
 //some time later...

From 01c14f0fc2a61341ba1bae6f752791f77bba64f5 Mon Sep 17 00:00:00 2001
From: Adlai Holler 
Date: Wed, 26 Jul 2017 22:27:58 -0700
Subject: [PATCH 11/16] Invalidate layouts more aggressively when transitioning
 with animation (#476)

* Be more aggressive at invalidating layouts during transitions, add a debug method, fix some build errors when verbose logging

* Add a changelog entry
---
 CHANGELOG.md                   |  1 +
 Source/ASDisplayNode+Layout.mm | 14 +++++++-------
 Source/ASDisplayNode.h         |  5 +++++
 Source/ASDisplayNode.mm        | 32 ++++++++++++++++++++++++++++++++
 Source/ASNetworkImageNode.mm   |  2 +-
 5 files changed, 46 insertions(+), 8 deletions(-)

diff --git a/CHANGELOG.md b/CHANGELOG.md
index f7267c240c..efbc62d548 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -7,6 +7,7 @@
 - Add ASCollectionGalleryLayoutDelegate - an async collection layout that makes same-size collections (e.g photo galleries, pagers, etc) fast and lightweight! [Huy Nguyen](https://github.com/nguyenhuy/) [#76](https://github.com/TextureGroup/Texture/pull/76) [#451](https://github.com/TextureGroup/Texture/pull/451)
 - Fix an issue that causes infinite layout loop in ASDisplayNode after [#428](https://github.com/TextureGroup/Texture/pull/428) [Huy Nguyen](https://github.com/nguyenhuy) [#455](https://github.com/TextureGroup/Texture/pull/455) 
 - Fix an issue in layout transition that causes it to unexpectedly use the old layout [Huy Nguyen](https://github.com/nguyenhuy) [#464](https://github.com/TextureGroup/Texture/pull/464)
+- Add -[ASDisplayNode detailedLayoutDescription] property to aid debugging. [Adlai Holler](https://github.com/Adlai-Holler) [#476](https://github.com/TextureGroup/Texture/pull/476)
 
 ##2.3.5
 - Fix an issue where inserting/deleting sections could lead to inconsistent supplementary element behavior. [Adlai Holler](https://github.com/Adlai-Holler)
diff --git a/Source/ASDisplayNode+Layout.mm b/Source/ASDisplayNode+Layout.mm
index 31f0739c73..66a5687685 100644
--- a/Source/ASDisplayNode+Layout.mm
+++ b/Source/ASDisplayNode+Layout.mm
@@ -319,7 +319,7 @@ ASPrimitiveTraitCollectionDeprecatedImplementation
   }
   
   as_activity_create_for_scope("Update node layout for current bounds");
-  as_log_verbose(ASLayoutLog(), "Node %@, bounds size %@, calculatedSize %@, calculatedIsDirty %d", self, NSStringFromCGSize(boundsSizeForLayout), NSStringFromCGSize(_calculatedDisplayNodeLayout->layout.size), _calculatedDisplayNodeLayout->isDirty());
+  as_log_verbose(ASLayoutLog(), "Node %@, bounds size %@, calculatedSize %@, calculatedIsDirty %d", self, NSStringFromCGSize(boundsSizeForLayout), NSStringFromCGSize(_calculatedDisplayNodeLayout->layout.size), _calculatedDisplayNodeLayout->version < _layoutVersion.load());
   // _calculatedDisplayNodeLayout is not reusable we need to transition to a new one
   [self cancelLayoutTransition];
   
@@ -543,7 +543,6 @@ ASPrimitiveTraitCollectionDeprecatedImplementation
     return;
   }
     
-  BOOL shouldInvalidateLayout = NO;
   {
     ASDN::MutexLocker l(__instanceLock__);
 
@@ -557,13 +556,14 @@ ASPrimitiveTraitCollectionDeprecatedImplementation
       ASDisplayNodeAssert(NO, @"Can't start a transition when one of the supernodes is performing one.");
       return;
     }
-
-    shouldInvalidateLayout = ASSizeRangeEqualToSizeRange([self _locked_constrainedSizeForCalculatedLayout], constrainedSize);
   }
 
-  if (shouldInvalidateLayout) {
-    [self setNeedsLayout];
-  }
+  // Invalidate calculated layout because this method acts as an animated "setNeedsLayout" for nodes.
+  // If the user has reconfigured the node and calls this, we should never return a stale layout
+  // for subsequent calls to layoutThatFits: regardless of size range. We choose this method rather than
+  // -setNeedsLayout because that method also triggers a CA layout invalidation, which isn't necessary at this time.
+  // See https://github.com/TextureGroup/Texture/issues/463
+  [self invalidateCalculatedLayout];
 
   // Every new layout transition has a transition id associated to check in subsequent transitions for cancelling
   int32_t transitionID = [self _startNewTransition];
diff --git a/Source/ASDisplayNode.h b/Source/ASDisplayNode.h
index f97f4dfc94..24347d09b5 100644
--- a/Source/ASDisplayNode.h
+++ b/Source/ASDisplayNode.h
@@ -584,6 +584,11 @@ extern NSInteger const ASDefaultDrawingPriority;
  */
 - (NSString *)displayNodeRecursiveDescription AS_WARN_UNUSED_RESULT;
 
+/**
+ * A detailed description of this node's layout state. This is useful when debugging.
+ */
+@property (atomic, copy, readonly) NSString *detailedLayoutDescription;
+
 @end
 
 /**
diff --git a/Source/ASDisplayNode.mm b/Source/ASDisplayNode.mm
index 2855fa91a8..cf90d518b3 100644
--- a/Source/ASDisplayNode.mm
+++ b/Source/ASDisplayNode.mm
@@ -3377,6 +3377,38 @@ ASDISPLAYNODE_INLINE BOOL subtreeIsRasterized(ASDisplayNode *node) {
   return subtree;
 }
 
+- (NSString *)detailedLayoutDescription
+{
+  ASPushMainThreadAssertionsDisabled();
+  ASDN::MutexLocker l(__instanceLock__);
+  auto props = [NSMutableArray array];
+
+  [props addObject:@{ @"layoutVersion": @(_layoutVersion.load()) }];
+  [props addObject:@{ @"bounds": [NSValue valueWithCGRect:self.bounds] }];
+
+  if (_calculatedDisplayNodeLayout != nullptr) {
+    ASDisplayNodeLayout c = *_calculatedDisplayNodeLayout;
+    [props addObject:@{ @"calculatedLayout": c.layout }];
+    [props addObject:@{ @"calculatedVersion": @(c.version) }];
+    [props addObject:@{ @"calculatedConstrainedSize" : NSStringFromASSizeRange(c.constrainedSize) }];
+    if (c.requestedLayoutFromAbove) {
+      [props addObject:@{ @"calculatedRequestedLayoutFromAbove": @"YES" }];
+    }
+  }
+  if (_pendingDisplayNodeLayout != nullptr) {
+    ASDisplayNodeLayout p = *_pendingDisplayNodeLayout;
+    [props addObject:@{ @"pendingLayout": p.layout }];
+    [props addObject:@{ @"pendingVersion": @(p.version) }];
+    [props addObject:@{ @"pendingConstrainedSize" : NSStringFromASSizeRange(p.constrainedSize) }];
+    if (p.requestedLayoutFromAbove) {
+      [props addObject:@{ @"pendingRequestedLayoutFromAbove": (id)kCFNull }];
+    }
+  }
+
+  ASPopMainThreadAssertionsDisabled();
+  return ASObjectDescriptionMake(self, props);
+}
+
 @end
 
 #pragma mark - ASDisplayNode UIKit / CA Categories
diff --git a/Source/ASNetworkImageNode.mm b/Source/ASNetworkImageNode.mm
index d6bd4dfbe8..c68cb792e6 100755
--- a/Source/ASNetworkImageNode.mm
+++ b/Source/ASNetworkImageNode.mm
@@ -671,7 +671,7 @@ static const CGSize kMinReleaseImageOnBackgroundSize = {20.0, 20.0};
           return;
         }
 
-        as_log_verbose(ASImageLoadingLog(), "Downloaded image for %@ img: %@ url: %@", self, [imageContainer asdk_image], url);
+        as_log_verbose(ASImageLoadingLog(), "Downloaded image for %@ img: %@ url: %@", self, [imageContainer asdk_image], URL);
         
         // Grab the lock for the rest of the block
         ASDN::MutexLocker l(strongSelf->__instanceLock__);

From 9a14f279aad779cd0fecafee6fe9966ce66ee12f Mon Sep 17 00:00:00 2001
From: appleguy 
Date: Thu, 27 Jul 2017 01:07:53 -0700
Subject: [PATCH 12/16] [ASNodeController] Add -nodeDidLayout callback. Allow
 switching retain behavior at runtime. (#470)

With these changes, I'd also like to propose that we move ASNodeController
out of Beta (renaming the files without +Beta). Let me know what you think!

Because we don't support ASNodeController directly in ASCV / ASTV, it is still
important to allow flipping the ownership in certain cases (in particular, for
root CellNodeController objects that should follow the lifecycle of the
ASCellNode rather than owning the ASCellNode).
---
 CHANGELOG.md                      |   1 +
 Source/ASDisplayNode+Subclasses.h |   7 ++
 Source/ASDisplayNode.mm           |   5 +-
 Source/ASNodeController+Beta.h    |  21 +++---
 Source/ASNodeController+Beta.m    | 109 +++++++++++++++++++++++-------
 5 files changed, 107 insertions(+), 36 deletions(-)

diff --git a/CHANGELOG.md b/CHANGELOG.md
index efbc62d548..4164075f53 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -1,6 +1,7 @@
 ## master
 
 * Add your own contributions to the next release on the line below this with your name.
+- [ASNodeController] Add -nodeDidLayout callback. Allow switching retain behavior at runtime. [Scott Goodson](https://github.com/appleguy)
 - [ASCollectionView] Add delegate bridging and index space translation for missing UICollectionViewLayout properties. [Scott Goodson](https://github.com/appleguy)
 - [ASTextNode2] Add initial implementation for link handling. [Scott Goodson](https://github.com/appleguy) [#396](https://github.com/TextureGroup/Texture/pull/396)
 - [ASTextNode2] Provide compile flag to globally enable new implementation of ASTextNode: ASTEXTNODE_EXPERIMENT_GLOBAL_ENABLE. [Scott Goodson](https://github.com/appleguy) [#396](https://github.com/TextureGroup/Texture/pull/410)
diff --git a/Source/ASDisplayNode+Subclasses.h b/Source/ASDisplayNode+Subclasses.h
index c89d7fef35..b506124d48 100644
--- a/Source/ASDisplayNode+Subclasses.h
+++ b/Source/ASDisplayNode+Subclasses.h
@@ -96,6 +96,13 @@ NS_ASSUME_NONNULL_BEGIN
  */
 - (void)didExitPreloadState;
 
+/**
+ * @abstract Called when the node has completed applying the layout.
+ * @discussion Can be used for operations that are performed after layout has completed.
+ * @note This method is guaranteed to be called on main.
+ */
+- (void)nodeDidLayout;
+
 @end
 
 @interface ASDisplayNode (Subclassing) 
diff --git a/Source/ASDisplayNode.mm b/Source/ASDisplayNode.mm
index cf90d518b3..d902740973 100644
--- a/Source/ASDisplayNode.mm
+++ b/Source/ASDisplayNode.mm
@@ -916,7 +916,9 @@ static ASDisplayNodeMethodOverrides GetASDisplayNodeMethodOverrides(Class c)
     if (_transitionID != ASLayoutElementContextInvalidTransitionID) {
       return;
     }
-    
+
+    as_activity_create_for_scope("-[ASDisplayNode __layout]");
+
     // This method will confirm that the layout is up to date (and update if needed).
     // Importantly, it will also APPLY the layout to all of our subnodes if (unless parent is transitioning).
     [self _locked_measureNodeWithBoundsIfNecessary:bounds];
@@ -1122,6 +1124,7 @@ static ASDisplayNodeMethodOverrides GetASDisplayNodeMethodOverrides(Class c)
   ASDisplayNodeAssertMainThread();
   ASDisplayNodeAssertLockUnownedByCurrentThread(__instanceLock__);
   ASDisplayNodeAssertTrue(self.isNodeLoaded);
+  [_interfaceStateDelegate nodeDidLayout];
 }
 
 #pragma mark Layout Transition
diff --git a/Source/ASNodeController+Beta.h b/Source/ASNodeController+Beta.h
index 7c75c5eec1..b21a86f41e 100644
--- a/Source/ASNodeController+Beta.h
+++ b/Source/ASNodeController+Beta.h
@@ -18,18 +18,15 @@
 #import 
 #import  // for ASInterfaceState protocol
 
-// Until an ASNodeController can be provided in place of an ASCellNode, some apps may prefer to have
-// nodes keep their controllers alive (and a weak reference from controller to node)
-#define INVERT_NODE_CONTROLLER_OWNERSHIP 0
-
 /* ASNodeController is currently beta and open to change in the future */
 @interface ASNodeController<__covariant DisplayNodeType : ASDisplayNode *> : NSObject 
 
-#if INVERT_NODE_CONTROLLER_OWNERSHIP
-@property (nonatomic, weak) DisplayNodeType node;
-#else
-@property (nonatomic, strong) DisplayNodeType node;
-#endif
+@property (nonatomic, strong /* may be weak! */) DisplayNodeType node;
+
+// Until an ASNodeController can be provided in place of an ASCellNode, some apps may prefer to have
+// nodes keep their controllers alive (and a weak reference from controller to node)
+
+@property (nonatomic, assign) BOOL shouldInvertStrongReference;
 
 - (void)loadNode;
 
@@ -47,3 +44,9 @@
                       fromState:(ASInterfaceState)oldState ASDISPLAYNODE_REQUIRES_SUPER;
 
 @end
+
+@interface ASDisplayNode (ASNodeController)
+
+@property(nonatomic, readonly) ASNodeController *nodeController;
+
+@end
diff --git a/Source/ASNodeController+Beta.m b/Source/ASNodeController+Beta.m
index 02efdc2250..bf14df0705 100644
--- a/Source/ASNodeController+Beta.m
+++ b/Source/ASNodeController+Beta.m
@@ -15,34 +15,28 @@
 //      http://www.apache.org/licenses/LICENSE-2.0
 //
 
-#import "ASNodeController+Beta.h"
-#import "ASDisplayNode+FrameworkPrivate.h"
+#import 
+#import 
+#import 
 
-#if INVERT_NODE_CONTROLLER_OWNERSHIP
+#define _node (_shouldInvertStrongReference ? _weakNode : _strongNode)
 
-@interface ASDisplayNode (ASNodeController)
-@property (nonatomic, strong) ASNodeController *asdkNodeController;
-@end
+@interface ASDisplayNode (ASNodeControllerOwnership)
 
-@implementation ASDisplayNode (ASNodeController)
+// This property exists for debugging purposes. Don't use __nodeController in production code.
+@property (nonatomic, readonly) ASNodeController *__nodeController;
 
-- (ASNodeController *)asdkNodeController
-{
-  return objc_getAssociatedObject(self, @selector(asdkNodeController));
-}
-
-- (void)setAsdkNodeController:(ASNodeController *)asdkNodeController
-{
-  objc_setAssociatedObject(self, @selector(asdkNodeController), asdkNodeController, OBJC_ASSOCIATION_RETAIN_NONATOMIC);
-}
+// These setters are mutually exclusive. Setting one will clear the relationship of the other.
+- (void)__setNodeControllerStrong:(ASNodeController *)nodeController;
+- (void)__setNodeControllerWeak:(ASNodeController *)nodeController;
 
 @end
 
-#endif
-
 @implementation ASNodeController
-
-@synthesize node = _node;
+{
+  ASDisplayNode *_strongNode;
+  __weak ASDisplayNode *_weakNode;
+}
 
 - (instancetype)init
 {
@@ -66,16 +60,41 @@
   return _node;
 }
 
--(void)setNode:(ASDisplayNode *)node
+- (void)setupReferencesWithNode:(ASDisplayNode *)node
 {
-  _node = node;
-  _node.interfaceStateDelegate = self;
-#if INVERT_NODE_CONTROLLER_OWNERSHIP
-  _node.asdkNodeController = self;
-#endif
+  if (_shouldInvertStrongReference) {
+    // The node should own the controller; weak reference from controller to node.
+    _weakNode = node;
+    [node __setNodeControllerStrong:self];
+    _strongNode = nil;
+  } else {
+    // The controller should own the node; weak reference from node to controller.
+    _strongNode = node;
+    [node __setNodeControllerWeak:self];
+    _weakNode = nil;
+  }
+
+  node.interfaceStateDelegate = self;
+}
+
+- (void)setNode:(ASDisplayNode *)node
+{
+  [self setupReferencesWithNode:node];
+}
+
+- (void)setShouldInvertStrongReference:(BOOL)shouldInvertStrongReference
+{
+  if (_shouldInvertStrongReference != shouldInvertStrongReference) {
+    // Because the BOOL controls which ivar we access, get the node before toggling.
+    ASDisplayNode *node = _node;
+    _shouldInvertStrongReference = shouldInvertStrongReference;
+    [self setupReferencesWithNode:node];
+  }
 }
 
 // subclass overrides
+- (void)nodeDidLayout {}
+
 - (void)didEnterVisibleState {}
 - (void)didExitVisibleState  {}
 
@@ -89,3 +108,41 @@
                       fromState:(ASInterfaceState)oldState {}
 
 @end
+
+@implementation ASDisplayNode (ASNodeControllerOwnership)
+
+- (ASNodeController *)__nodeController
+{
+  ASNodeController *nodeController = nil;
+  id object = objc_getAssociatedObject(self, @selector(__nodeController));
+
+  if ([object isKindOfClass:[ASWeakProxy class]]) {
+    nodeController = (ASNodeController *)[(ASWeakProxy *)object target];
+  } else {
+    nodeController = (ASNodeController *)object;
+  }
+
+  return nodeController;
+}
+
+- (void)__setNodeControllerStrong:(ASNodeController *)nodeController
+{
+  objc_setAssociatedObject(self, @selector(__nodeController), nodeController, OBJC_ASSOCIATION_RETAIN_NONATOMIC);
+}
+
+- (void)__setNodeControllerWeak:(ASNodeController *)nodeController
+{
+  // Associated objects don't support weak references. Since assign can become a dangling pointer, use ASWeakProxy.
+  ASWeakProxy *nodeControllerProxy = [ASWeakProxy weakProxyWithTarget:nodeController];
+  objc_setAssociatedObject(self, @selector(__nodeController), nodeControllerProxy, OBJC_ASSOCIATION_RETAIN_NONATOMIC);
+}
+
+@end
+
+@implementation ASDisplayNode (ASNodeController)
+
+- (ASNodeController *)nodeController {
+  return self.__nodeController;
+}
+
+@end

From c2f6b90c9d909856958c8b7af87b134f0d060171 Mon Sep 17 00:00:00 2001
From: Ofer Morag 
Date: Fri, 28 Jul 2017 19:36:49 +0300
Subject: [PATCH 13/16] Update subclassing.md (#479)

* Update subclassing.md

* Update subclassing.md

Lowered-case the "i".
---
 docs/_docs/subclassing.md | 2 +-
 1 file changed, 1 insertion(+), 1 deletion(-)

diff --git a/docs/_docs/subclassing.md b/docs/_docs/subclassing.md
index e284f252c5..966d83144d 100755
--- a/docs/_docs/subclassing.md
+++ b/docs/_docs/subclassing.md
@@ -19,7 +19,7 @@ The most important thing to remember is that your init method must be capable of
 
 ### `-didLoad`
 
-This method is conceptually similar to UIViewController's `-viewDidLoad` method and is the point where the backing view has been loaded.  It is guaranteed to be called on the **main thread** and is the appropriate place to do any UIKit things (such as adding gesture recognizers, touching the view / layer, initializing UIKIt objects). 
+This method is conceptually similar to UIViewController's `-viewDidLoad` method; it’s called once and is the point where the backing view has been loaded.  It is guaranteed to be called on the **main thread** and is the appropriate place to do any UIKit things (such as adding gesture recognizers, touching the view / layer, initializing UIKIt objects). 
 
 ### `-layoutSpecThatFits:`
 

From 858fa11ac30a2ed9a54fa3ccb700fd467496fc76 Mon Sep 17 00:00:00 2001
From: Ofer Morag 
Date: Sat, 29 Jul 2017 00:44:16 +0300
Subject: [PATCH 14/16] Update adoption-guide-2-0-beta1.md (#483)
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit

* Update adoption-guide-2-0-beta1.md

- Removed the “number” looks like a mistake
- Changed `"` to `“` to match the ending sign

* Update adoption-guide-2-0-beta1.md

Changing both sign to non-smart
---
 docs/_docs/adoption-guide-2-0-beta1.md | 4 ++--
 1 file changed, 2 insertions(+), 2 deletions(-)

diff --git a/docs/_docs/adoption-guide-2-0-beta1.md b/docs/_docs/adoption-guide-2-0-beta1.md
index 18591a1ca6..f3c199fff3 100755
--- a/docs/_docs/adoption-guide-2-0-beta1.md
+++ b/docs/_docs/adoption-guide-2-0-beta1.md
@@ -96,7 +96,7 @@ Texture's collection and table APIs have been moved from the view space (`collec
 
 - Search your project for `tableView` and `collectionView`. Most, if not all, of the data source / delegate methods have new node versions. 
 
-It is important that developers using Texture understand that an ASCollectionNode is backed by an ASCollectionView (a subclass of UICollectionView). ASCollectionNode runs asynchronously, so calling number -numberOfRowsInSection on the collectionNode is different than calling it on the collectionView. 
+It is important that developers using Texture understand that an ASCollectionNode is backed by an ASCollectionView (a subclass of UICollectionView). ASCollectionNode runs asynchronously, so calling -numberOfRowsInSection on the collectionNode is different than calling it on the collectionView. 
 
 For example, let's say you have an empty table. You insert `100` rows and then immediately call -tableView:numberOfRowsInSection. This will return `0` rows. If you call -waitUntilAllUpdatesAreCommitted after insertion (waits until the collectionNode synchronizes with the collectionView), you will get 100, _but_ you might block the main thread. A good developer should rarely (or never) need to use -waitUntilAllUpdatesAreCommitted. If you update the collectionNode and then need to read back immediately, you should use the collectionNode API. You shouldn't need to talk to the collectionView.  
 
@@ -122,7 +122,7 @@ Other updates include:
 
 - Renamed `pagerNode:constrainedSizeForNodeAtIndexPath:` to `pagerNode:constrainedSizeForNodeAtIndex:`
 
-- collection view update validation assertions are now enabled. If you see something like `"Invalid number of items in section 2. The number of items after the update (7) must be equal to the number of items before the update (4) plus or minus the number of items inserted or removed from the section (4 inserted, 0 removed)”`, please check the data source logic. If you have any questions, reach out to us on GitHub. 
+- collection view update validation assertions are now enabled. If you see something like `"Invalid number of items in section 2. The number of items after the update (7) must be equal to the number of items before the update (4) plus or minus the number of items inserted or removed from the section (4 inserted, 0 removed)"`, please check the data source logic. If you have any questions, reach out to us on GitHub. 
 
 Best Practices:
 

From 0f9645c5a2b667570619266134b3d190cb55172f Mon Sep 17 00:00:00 2001
From: Ofer Morag 
Date: Mon, 31 Jul 2017 20:37:28 +0300
Subject: [PATCH 15/16] Update scroll-node.md (#484)
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit

* Update scroll-node.md

Regarding the last sentence: “As you can see, the `scrollNode`'s underlying view is a `ASScrollNode`.”:
1. I think that `ASScrollNode` should be replaced with `UIScrollView`.
2. But — not sure how can it be seen from this example...

* Update scroll-node.md

Removed the last sentence, as it did not make sense, and probably left there from previous editions.
---
 docs/_docs/scroll-node.md | 3 ---
 1 file changed, 3 deletions(-)

diff --git a/docs/_docs/scroll-node.md b/docs/_docs/scroll-node.md
index 344faebc13..465ac00e62 100755
--- a/docs/_docs/scroll-node.md
+++ b/docs/_docs/scroll-node.md
@@ -76,6 +76,3 @@ scrollNode.layoutSpecBlock = { node, constrainedSize in
 
- -As you can see, the `scrollNode`'s underlying view is a `ASScrollNode`. - From ba08ae1318cac3a40c4c0fe6c920e1675117dc2f Mon Sep 17 00:00:00 2001 From: Flo Date: Thu, 3 Aug 2017 12:24:08 +0200 Subject: [PATCH 16/16] [ASStackLayoutSpec] Flex wrap fix and lineSpacing property (#472) * ASStackUnpositionedLayout: Take spacing into account when laying out a wrapped stack. * ASStackLayoutSpec: Add the lineSpacing property. * Update CHANGELOG.md. --- CHANGELOG.md | 2 ++ Source/Layout/ASStackLayoutSpec.h | 21 +++++++++++++++++++ Source/Layout/ASStackLayoutSpec.mm | 16 +++++++++----- .../Layout/ASStackLayoutSpecUtilities.h | 1 + .../Private/Layout/ASStackPositionedLayout.mm | 3 ++- .../Layout/ASStackUnpositionedLayout.mm | 16 ++++++++------ 6 files changed, 47 insertions(+), 12 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 4164075f53..9f85d51016 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,6 +1,8 @@ ## master * Add your own contributions to the next release on the line below this with your name. +- [ASStackLayoutSpec] Add lineSpacing property working with flex wrap. [Flo Vouin](https://github.com/flovouin) +- [ASStackLayoutSpec] Fix flex wrap overflow in some cases using item spacing. [Flo Vouin](https://github.com/flovouin) - [ASNodeController] Add -nodeDidLayout callback. Allow switching retain behavior at runtime. [Scott Goodson](https://github.com/appleguy) - [ASCollectionView] Add delegate bridging and index space translation for missing UICollectionViewLayout properties. [Scott Goodson](https://github.com/appleguy) - [ASTextNode2] Add initial implementation for link handling. [Scott Goodson](https://github.com/appleguy) [#396](https://github.com/TextureGroup/Texture/pull/396) diff --git a/Source/Layout/ASStackLayoutSpec.h b/Source/Layout/ASStackLayoutSpec.h index 3ac682dc64..b4a27697ea 100644 --- a/Source/Layout/ASStackLayoutSpec.h +++ b/Source/Layout/ASStackLayoutSpec.h @@ -70,6 +70,8 @@ NS_ASSUME_NONNULL_BEGIN @property (nonatomic, assign) ASStackLayoutFlexWrap flexWrap; /** Orientation of lines along cross axis if there are multiple lines. Defaults to ASStackLayoutAlignContentStart */ @property (nonatomic, assign) ASStackLayoutAlignContent alignContent; +/** If the stack spreads on multiple lines using flexWrap, the amount of space between lines. */ +@property (nonatomic, assign) CGFloat lineSpacing; /** Whether this stack can dispatch to other threads, regardless of which thread it's running on */ @property (nonatomic, assign, getter=isConcurrent) BOOL concurrent; @@ -105,6 +107,25 @@ NS_ASSUME_NONNULL_BEGIN alignContent:(ASStackLayoutAlignContent)alignContent children:(NSArray> *)children AS_WARN_UNUSED_RESULT; +/** + @param direction The direction of the stack view (horizontal or vertical) + @param spacing The spacing between the children + @param justifyContent If no children are flexible, this describes how to fill any extra space + @param alignItems Orientation of the children along the cross axis + @param flexWrap Whether children are stacked into a single or multiple lines + @param alignContent Orientation of lines along cross axis if there are multiple lines + @param lineSpacing The spacing between lines + @param children ASLayoutElement children to be positioned. + */ ++ (instancetype)stackLayoutSpecWithDirection:(ASStackLayoutDirection)direction + spacing:(CGFloat)spacing + justifyContent:(ASStackLayoutJustifyContent)justifyContent + alignItems:(ASStackLayoutAlignItems)alignItems + flexWrap:(ASStackLayoutFlexWrap)flexWrap + alignContent:(ASStackLayoutAlignContent)alignContent + lineSpacing:(CGFloat)lineSpacing + children:(NSArray> *)children AS_WARN_UNUSED_RESULT; + /** * @return A stack layout spec with direction of ASStackLayoutDirectionVertical **/ diff --git a/Source/Layout/ASStackLayoutSpec.mm b/Source/Layout/ASStackLayoutSpec.mm index 4a3de0c707..fc89d6f970 100644 --- a/Source/Layout/ASStackLayoutSpec.mm +++ b/Source/Layout/ASStackLayoutSpec.mm @@ -33,17 +33,22 @@ - (instancetype)init { - return [self initWithDirection:ASStackLayoutDirectionHorizontal spacing:0.0 justifyContent:ASStackLayoutJustifyContentStart alignItems:ASStackLayoutAlignItemsStretch flexWrap:ASStackLayoutFlexWrapNoWrap alignContent:ASStackLayoutAlignContentStart children:nil]; + return [self initWithDirection:ASStackLayoutDirectionHorizontal spacing:0.0 justifyContent:ASStackLayoutJustifyContentStart alignItems:ASStackLayoutAlignItemsStretch flexWrap:ASStackLayoutFlexWrapNoWrap alignContent:ASStackLayoutAlignContentStart lineSpacing:0.0 children:nil]; } + (instancetype)stackLayoutSpecWithDirection:(ASStackLayoutDirection)direction spacing:(CGFloat)spacing justifyContent:(ASStackLayoutJustifyContent)justifyContent alignItems:(ASStackLayoutAlignItems)alignItems children:(NSArray *)children { - return [[self alloc] initWithDirection:direction spacing:spacing justifyContent:justifyContent alignItems:alignItems flexWrap:ASStackLayoutFlexWrapNoWrap alignContent:ASStackLayoutAlignContentStart children:children]; + return [[self alloc] initWithDirection:direction spacing:spacing justifyContent:justifyContent alignItems:alignItems flexWrap:ASStackLayoutFlexWrapNoWrap alignContent:ASStackLayoutAlignContentStart lineSpacing: 0.0 children:children]; } + (instancetype)stackLayoutSpecWithDirection:(ASStackLayoutDirection)direction spacing:(CGFloat)spacing justifyContent:(ASStackLayoutJustifyContent)justifyContent alignItems:(ASStackLayoutAlignItems)alignItems flexWrap:(ASStackLayoutFlexWrap)flexWrap alignContent:(ASStackLayoutAlignContent)alignContent children:(NSArray> *)children { - return [[self alloc] initWithDirection:direction spacing:spacing justifyContent:justifyContent alignItems:alignItems flexWrap:flexWrap alignContent:alignContent children:children]; + return [[self alloc] initWithDirection:direction spacing:spacing justifyContent:justifyContent alignItems:alignItems flexWrap:flexWrap alignContent:alignContent lineSpacing:0.0 children:children]; +} + ++ (instancetype)stackLayoutSpecWithDirection:(ASStackLayoutDirection)direction spacing:(CGFloat)spacing justifyContent:(ASStackLayoutJustifyContent)justifyContent alignItems:(ASStackLayoutAlignItems)alignItems flexWrap:(ASStackLayoutFlexWrap)flexWrap alignContent:(ASStackLayoutAlignContent)alignContent lineSpacing:(CGFloat)lineSpacing children:(NSArray> *)children +{ + return [[self alloc] initWithDirection:direction spacing:spacing justifyContent:justifyContent alignItems:alignItems flexWrap:flexWrap alignContent:alignContent lineSpacing:lineSpacing children:children]; } + (instancetype)verticalStackLayoutSpec @@ -60,7 +65,7 @@ return stackLayoutSpec; } -- (instancetype)initWithDirection:(ASStackLayoutDirection)direction spacing:(CGFloat)spacing justifyContent:(ASStackLayoutJustifyContent)justifyContent alignItems:(ASStackLayoutAlignItems)alignItems flexWrap:(ASStackLayoutFlexWrap)flexWrap alignContent:(ASStackLayoutAlignContent)alignContent children:(NSArray *)children +- (instancetype)initWithDirection:(ASStackLayoutDirection)direction spacing:(CGFloat)spacing justifyContent:(ASStackLayoutJustifyContent)justifyContent alignItems:(ASStackLayoutAlignItems)alignItems flexWrap:(ASStackLayoutFlexWrap)flexWrap alignContent:(ASStackLayoutAlignContent)alignContent lineSpacing:(CGFloat)lineSpacing children:(NSArray *)children { if (!(self = [super init])) { return nil; @@ -73,6 +78,7 @@ _justifyContent = justifyContent; _flexWrap = flexWrap; _alignContent = alignContent; + _lineSpacing = lineSpacing; [self setChildren:children]; return self; @@ -144,7 +150,7 @@ return {child, style, style.size}; }); - const ASStackLayoutSpecStyle style = {.direction = _direction, .spacing = _spacing, .justifyContent = _justifyContent, .alignItems = _alignItems, .flexWrap = _flexWrap, .alignContent = _alignContent}; + const ASStackLayoutSpecStyle style = {.direction = _direction, .spacing = _spacing, .justifyContent = _justifyContent, .alignItems = _alignItems, .flexWrap = _flexWrap, .alignContent = _alignContent, .lineSpacing = _lineSpacing}; const auto unpositionedLayout = ASStackUnpositionedLayout::compute(stackChildren, style, constrainedSize, _concurrent); const auto positionedLayout = ASStackPositionedLayout::compute(unpositionedLayout, style, constrainedSize); diff --git a/Source/Private/Layout/ASStackLayoutSpecUtilities.h b/Source/Private/Layout/ASStackLayoutSpecUtilities.h index 3b677be7e8..dbb2c74352 100644 --- a/Source/Private/Layout/ASStackLayoutSpecUtilities.h +++ b/Source/Private/Layout/ASStackLayoutSpecUtilities.h @@ -24,6 +24,7 @@ typedef struct { ASStackLayoutAlignItems alignItems; ASStackLayoutFlexWrap flexWrap; ASStackLayoutAlignContent alignContent; + CGFloat lineSpacing; } ASStackLayoutSpecStyle; inline CGFloat stackDimension(const ASStackLayoutDirection direction, const CGSize size) diff --git a/Source/Private/Layout/ASStackPositionedLayout.mm b/Source/Private/Layout/ASStackPositionedLayout.mm index 382e416860..9b1ba020a7 100644 --- a/Source/Private/Layout/ASStackPositionedLayout.mm +++ b/Source/Private/Layout/ASStackPositionedLayout.mm @@ -160,6 +160,7 @@ ASStackPositionedLayout ASStackPositionedLayout::compute(const ASStackUnposition const auto numOfLines = lines.size(); const auto direction = style.direction; const auto alignContent = style.alignContent; + const auto lineSpacing = style.lineSpacing; const auto justifyContent = style.justifyContent; const auto crossViolation = ASStackUnpositionedLayout::computeCrossViolation(layout.crossDimensionSum, style, sizeRange); CGFloat crossOffset; @@ -171,7 +172,7 @@ ASStackPositionedLayout ASStackPositionedLayout::compute(const ASStackUnposition BOOL first = YES; for (const auto &line : lines) { if (!first) { - p = p + directionPoint(direction, 0, crossSpacing); + p = p + directionPoint(direction, 0, crossSpacing + lineSpacing); } first = NO; diff --git a/Source/Private/Layout/ASStackUnpositionedLayout.mm b/Source/Private/Layout/ASStackUnpositionedLayout.mm index 6f43a4535f..8bed958aa4 100644 --- a/Source/Private/Layout/ASStackUnpositionedLayout.mm +++ b/Source/Private/Layout/ASStackUnpositionedLayout.mm @@ -113,9 +113,12 @@ static void dispatchApplyIfNeeded(size_t iterationCount, BOOL forced, void(^work @param lines unpositioned lines */ -static CGFloat computeLinesCrossDimensionSum(const std::vector &lines) +static CGFloat computeLinesCrossDimensionSum(const std::vector &lines, + const ASStackLayoutSpecStyle &style) { - return std::accumulate(lines.begin(), lines.end(), 0.0, + return std::accumulate(lines.begin(), lines.end(), + // Start from default spacing between each line: + lines.empty() ? 0 : style.lineSpacing * (lines.size() - 1), [&](CGFloat x, const ASStackUnpositionedLine &l) { return x + l.crossSize; }); @@ -236,7 +239,7 @@ static void stretchLinesAlongCrossDimension(std::vector { ASDisplayNodeCAssertFalse(lines.empty()); const std::size_t numOfLines = lines.size(); - const CGFloat violation = ASStackUnpositionedLayout::computeCrossViolation(computeLinesCrossDimensionSum(lines), style, sizeRange); + const CGFloat violation = ASStackUnpositionedLayout::computeCrossViolation(computeLinesCrossDimensionSum(lines, style), style, sizeRange); // Don't stretch if the stack is single line, because the line's cross size was clamped against the stack's constrained size. const BOOL shouldStretchLines = (numOfLines > 1 && style.alignContent == ASStackLayoutAlignContentStretch @@ -648,7 +651,8 @@ static std::vector collectChildrenIntoLines(const std:: for(auto it = items.begin(); it != items.end(); ++it) { const auto &item = *it; const CGFloat itemStackDimension = stackDimension(style.direction, item.layout.size); - const BOOL negativeViolationIfAddItem = (ASStackUnpositionedLayout::computeStackViolation(lineStackDimensionSum + itemStackDimension, style, sizeRange) < 0); + const CGFloat itemAndSpacingStackDimension = (lineItems.empty() ? 0.0 : style.spacing) + item.child.style.spacingBefore + itemStackDimension + item.child.style.spacingAfter; + const BOOL negativeViolationIfAddItem = (ASStackUnpositionedLayout::computeStackViolation(lineStackDimensionSum + itemAndSpacingStackDimension, style, sizeRange) < 0); const BOOL breakCurrentLine = negativeViolationIfAddItem && !lineItems.empty(); if (breakCurrentLine) { @@ -658,7 +662,7 @@ static std::vector collectChildrenIntoLines(const std:: } lineItems.push_back(std::move(item)); - lineStackDimensionSum += itemStackDimension; + lineStackDimensionSum += itemAndSpacingStackDimension; } // Handle last line @@ -752,7 +756,7 @@ ASStackUnpositionedLayout ASStackUnpositionedLayout::compute(const std::vector