From 167b7d404ebc73c3e7d013722d93e79d521e951b Mon Sep 17 00:00:00 2001 From: Levi McCallum Date: Wed, 9 Mar 2016 10:38:25 -0800 Subject: [PATCH 01/26] Add header for extern ASSizeRangeMakeWithExactCGSize --- AsyncDisplayKit/Layout/ASDimension.h | 3 +++ 1 file changed, 3 insertions(+) diff --git a/AsyncDisplayKit/Layout/ASDimension.h b/AsyncDisplayKit/Layout/ASDimension.h index c96b2155ae..d9444780a1 100644 --- a/AsyncDisplayKit/Layout/ASDimension.h +++ b/AsyncDisplayKit/Layout/ASDimension.h @@ -58,6 +58,9 @@ extern CGFloat ASRelativeDimensionResolve(ASRelativeDimension dimension, CGFloat extern ASSizeRange ASSizeRangeMake(CGSize min, CGSize max); +/** Creates an ASSizeRange with the provided size as both min and max */ +extern ASSizeRange ASSizeRangeMakeWithExactCGSize(CGSize size); + /** Clamps the provided CGSize between the [min, max] bounds of this ASSizeRange. */ extern CGSize ASSizeRangeClamp(ASSizeRange sizeRange, CGSize size); From d4a0c34d86feb6c279763072fb47da17971bb688 Mon Sep 17 00:00:00 2001 From: Levi McCallum Date: Wed, 9 Mar 2016 10:39:46 -0800 Subject: [PATCH 02/26] Add ASSizeRangeMakeWithExactCGSize method --- AsyncDisplayKit/Layout/ASDimension.mm | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/AsyncDisplayKit/Layout/ASDimension.mm b/AsyncDisplayKit/Layout/ASDimension.mm index a1e42c4b76..cd7199a5e9 100644 --- a/AsyncDisplayKit/Layout/ASDimension.mm +++ b/AsyncDisplayKit/Layout/ASDimension.mm @@ -77,6 +77,11 @@ ASSizeRange ASSizeRangeMake(CGSize min, CGSize max) ASSizeRange sizeRange; sizeRange.min = min; sizeRange.max = max; return sizeRange; } +ASSizeRange ASSizeRangeMakeWithExactCGSize(CGSize size) +{ + return ASSizeRangeMake(size, size); +} + CGSize ASSizeRangeClamp(ASSizeRange sizeRange, CGSize size) { return CGSizeMake(MAX(sizeRange.min.width, MIN(sizeRange.max.width, size.width)), From a07f7b73f084a35fa6151873dd574a73287a6671 Mon Sep 17 00:00:00 2001 From: Levi McCallum Date: Wed, 9 Mar 2016 10:42:35 -0800 Subject: [PATCH 03/26] Change method name in header --- AsyncDisplayKit/Layout/ASDimension.h | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/AsyncDisplayKit/Layout/ASDimension.h b/AsyncDisplayKit/Layout/ASDimension.h index d9444780a1..3dba264468 100644 --- a/AsyncDisplayKit/Layout/ASDimension.h +++ b/AsyncDisplayKit/Layout/ASDimension.h @@ -59,7 +59,7 @@ extern CGFloat ASRelativeDimensionResolve(ASRelativeDimension dimension, CGFloat extern ASSizeRange ASSizeRangeMake(CGSize min, CGSize max); /** Creates an ASSizeRange with the provided size as both min and max */ -extern ASSizeRange ASSizeRangeMakeWithExactCGSize(CGSize size); +extern ASSizeRange ASSizeRangeMakeExactSize(CGSize size); /** Clamps the provided CGSize between the [min, max] bounds of this ASSizeRange. */ extern CGSize ASSizeRangeClamp(ASSizeRange sizeRange, CGSize size); From 52391dcc278946423d85f7fddee4417a6017df47 Mon Sep 17 00:00:00 2001 From: Levi McCallum Date: Wed, 9 Mar 2016 10:42:56 -0800 Subject: [PATCH 04/26] Change method name in implementation --- AsyncDisplayKit/Layout/ASDimension.mm | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/AsyncDisplayKit/Layout/ASDimension.mm b/AsyncDisplayKit/Layout/ASDimension.mm index cd7199a5e9..7715e3b07a 100644 --- a/AsyncDisplayKit/Layout/ASDimension.mm +++ b/AsyncDisplayKit/Layout/ASDimension.mm @@ -77,7 +77,7 @@ ASSizeRange ASSizeRangeMake(CGSize min, CGSize max) ASSizeRange sizeRange; sizeRange.min = min; sizeRange.max = max; return sizeRange; } -ASSizeRange ASSizeRangeMakeWithExactCGSize(CGSize size) +ASSizeRange ASSizeRangeMakeExactSize(CGSize size) { return ASSizeRangeMake(size, size); } From a3331b42ae687d5b794fcbcf537d5dd665d4af7d Mon Sep 17 00:00:00 2001 From: Eric Jensen Date: Wed, 9 Mar 2016 18:57:31 -0800 Subject: [PATCH 05/26] Replace sleep() in ASBasicImageDownloaderTests with XCTest asynchronous expectations --- .../ASBasicImageDownloaderTests.m | 44 ++++++++----------- 1 file changed, 19 insertions(+), 25 deletions(-) diff --git a/AsyncDisplayKitTests/ASBasicImageDownloaderTests.m b/AsyncDisplayKitTests/ASBasicImageDownloaderTests.m index 02d5d9d127..b3c772ae8a 100644 --- a/AsyncDisplayKitTests/ASBasicImageDownloaderTests.m +++ b/AsyncDisplayKitTests/ASBasicImageDownloaderTests.m @@ -1,5 +1,5 @@ // -// ASZBasicImageDownloaderTests.m +// ASBasicImageDownloaderTests.m // AsyncDisplayKit // // Created by Victor Mayorov on 10/06/15. @@ -10,7 +10,6 @@ #import -// Z in the name to delay running until after the test instance is operating normally. @interface ASBasicImageDownloaderTests : XCTestCase @end @@ -19,35 +18,30 @@ - (void)testAsynchronouslyDownloadTheSameURLTwice { - ASBasicImageDownloader *downloader = [ASBasicImageDownloader sharedImageDownloader]; - - NSURL *URL = [NSURL URLWithString:@"http://wrongPath/wrongResource.png"]; + XCTestExpectation *firstExpectation = [self expectationWithDescription:@"First ASBasicImageDownloader completion handler should be called within 3 seconds"]; + XCTestExpectation *secondExpectation = [self expectationWithDescription:@"Second ASBasicImageDownloader completion handler should be called within 3 seconds"]; + + ASBasicImageDownloader *downloader = [ASBasicImageDownloader sharedImageDownloader]; + NSURL *URL = [NSURL URLWithString:@"http://wrongPath/wrongResource.png"]; #pragma clang diagnostic push #pragma clang diagnostic ignored "-Wdeprecated-declarations" + [downloader downloadImageWithURL:URL + callbackQueue:dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0) + downloadProgressBlock:nil + completion:^(CGImageRef image, NSError *error) { + [firstExpectation fulfill]; + }]; - __block BOOL firstDone = NO; - - [downloader downloadImageWithURL:URL - callbackQueue:dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0) - downloadProgressBlock:nil - completion:^(CGImageRef image, NSError *error) { - firstDone = YES; - }]; - - __block BOOL secondDone = NO; - - [downloader downloadImageWithURL:URL - callbackQueue:dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0) - downloadProgressBlock:nil - completion:^(CGImageRef image, NSError *error) { - secondDone = YES; - }]; - + [downloader downloadImageWithURL:URL + callbackQueue:dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0) + downloadProgressBlock:nil + completion:^(CGImageRef image, NSError *error) { + [secondExpectation fulfill]; + }]; #pragma clang diagnostic pop - sleep(3); - XCTAssert(firstDone && secondDone, @"Not all ASBasicImageDownloader completion handlers have been called after 3 seconds"); + [self waitForExpectationsWithTimeout:3 handler:nil]; } @end From c3fb665ec10d725158003caac0ee713f03480b70 Mon Sep 17 00:00:00 2001 From: Scott Goodson Date: Thu, 10 Mar 2016 16:20:21 -0800 Subject: [PATCH 06/26] [ASWeakSet] Support -allObjects to return a retained array of contents. Use this array while enumerating ASRangeController instances in response to UIApplication notifications, as it is possible for these events to trigger the mutation of the ASWeakSet and cause an enumeration error. --- AsyncDisplayKit/Details/ASRangeController.mm | 22 ++++++++++++-------- AsyncDisplayKit/Private/ASWeakSet.h | 3 +++ AsyncDisplayKit/Private/ASWeakSet.m | 19 ++++++++++++++++- 3 files changed, 34 insertions(+), 10 deletions(-) diff --git a/AsyncDisplayKit/Details/ASRangeController.mm b/AsyncDisplayKit/Details/ASRangeController.mm index c466d28f68..1b0e828814 100644 --- a/AsyncDisplayKit/Details/ASRangeController.mm +++ b/AsyncDisplayKit/Details/ASRangeController.mm @@ -490,19 +490,22 @@ static ASLayoutRangeMode __rangeModeForMemoryWarnings = ASLayoutRangeModeVisible + (void)didReceiveMemoryWarning:(NSNotification *)notification { -#if ASRangeControllerLoggingEnabled - NSLog(@"+[ASRangeController didReceiveMemoryWarning] with controllers: %@", [self allRangeControllersWeakSet]); -#endif - for (ASRangeController *rangeController in [self allRangeControllersWeakSet]) { + NSArray *allRangeControllers = [[self allRangeControllersWeakSet] allObjects]; + for (ASRangeController *rangeController in allRangeControllers) { BOOL isDisplay = ASInterfaceStateIncludesDisplay([rangeController interfaceState]); [rangeController updateCurrentRangeWithMode:isDisplay ? ASLayoutRangeModeMinimum : __rangeModeForMemoryWarnings]; [rangeController performRangeUpdate]; } + +#if ASRangeControllerLoggingEnabled + NSLog(@"+[ASRangeController didReceiveMemoryWarning] with controllers: %@", allRangeControllers); +#endif } + (void)didEnterBackground:(NSNotification *)notification { - for (ASRangeController *rangeController in [self allRangeControllersWeakSet]) { + NSArray *allRangeControllers = [[self allRangeControllersWeakSet] allObjects]; + for (ASRangeController *rangeController in allRangeControllers) { // We do not want to fully collapse the Display ranges of any visible range controllers so that flashes can be avoided when // the app is resumed. Non-visible controllers can be more aggressively culled to the LowMemory state (see definitions for documentation) BOOL isVisible = ASInterfaceStateIncludesVisible([rangeController interfaceState]); @@ -511,27 +514,28 @@ static ASLayoutRangeMode __rangeModeForMemoryWarnings = ASLayoutRangeModeVisible // Because -interfaceState checks __ApplicationState and always clears the "visible" bit if Backgrounded, we must set this after updating the range mode. __ApplicationState = UIApplicationStateBackground; - for (ASRangeController *rangeController in [self allRangeControllersWeakSet]) { + for (ASRangeController *rangeController in allRangeControllers) { // Trigger a range update immediately, as we may not be allowed by the system to run the update block scheduled by changing range mode. [rangeController performRangeUpdate]; } #if ASRangeControllerLoggingEnabled - NSLog(@"+[ASRangeController didEnterBackground] with controllers, after backgrounding: %@", [self allRangeControllersWeakSet]); + NSLog(@"+[ASRangeController didEnterBackground] with controllers, after backgrounding: %@", allRangeControllers); #endif } + (void)willEnterForeground:(NSNotification *)notification { + NSArray *allRangeControllers = [[self allRangeControllersWeakSet] allObjects]; __ApplicationState = UIApplicationStateActive; - for (ASRangeController *rangeController in [self allRangeControllersWeakSet]) { + for (ASRangeController *rangeController in allRangeControllers) { BOOL isVisible = ASInterfaceStateIncludesVisible([rangeController interfaceState]); [rangeController updateCurrentRangeWithMode:isVisible ? ASLayoutRangeModeMinimum : ASLayoutRangeModeVisibleOnly]; [rangeController performRangeUpdate]; } #if ASRangeControllerLoggingEnabled - NSLog(@"+[ASRangeController willEnterForeground] with controllers, after foregrounding: %@", [self allRangeControllersWeakSet]); + NSLog(@"+[ASRangeController willEnterForeground] with controllers, after foregrounding: %@", allRangeControllers); #endif } diff --git a/AsyncDisplayKit/Private/ASWeakSet.h b/AsyncDisplayKit/Private/ASWeakSet.h index 8f6a6576ca..2a72b19e9d 100644 --- a/AsyncDisplayKit/Private/ASWeakSet.h +++ b/AsyncDisplayKit/Private/ASWeakSet.h @@ -27,6 +27,9 @@ NS_ASSUME_NONNULL_BEGIN /// Removes all objects from the set. - (void)removeAllObjects; +/// Returns a standard *retained* NSArray of all objects. Not free to generate, but useful for iterating over contents. +- (NSArray *)allObjects; + /** How many objects are contained in this set. diff --git a/AsyncDisplayKit/Private/ASWeakSet.m b/AsyncDisplayKit/Private/ASWeakSet.m index 516d056307..a8b8ce1893 100644 --- a/AsyncDisplayKit/Private/ASWeakSet.m +++ b/AsyncDisplayKit/Private/ASWeakSet.m @@ -7,6 +7,7 @@ // #import "ASWeakSet.h" +#import @interface ASWeakSet<__covariant ObjectType> () @property (nonatomic, strong, readonly) NSMapTable *mapTable; @@ -25,7 +26,7 @@ - (void)addObject:(id)object { - [_mapTable setObject:[NSNull null] forKey:object]; + [_mapTable setObject:kCFNull forKey:object]; } - (void)removeObject:(id)object @@ -38,6 +39,22 @@ [_mapTable removeAllObjects]; } +- (NSArray *)allObjects +{ + // We use keys instead of values in the map table for efficiency and better characteristics when the keys are deallocated. + // Documentation is currently unclear on whether -keyEnumerator retains its values, but does imply that modifying a + // mutable collection is still not safe while enumerating that way - which is one of the main uses for this method. + // A helper function called NSAllMapTableKeys() might do exactly what we want and should be more efficient, but unfortunately + // is throwing a strange compiler error and may not be available in practice on the latest iOS version. + // Lastly, even -dictionaryRepresentation and then -allKeys won't work, because it attemps to copy the values of each key, + // which may not support copying (such as ASRangeControllers). + NSMutableArray *allObjects = [NSMutableArray array]; + for (id object in _mapTable) { + [allObjects addObject:object]; + } + return allObjects; +} + - (BOOL)containsObject:(id)object { return [_mapTable objectForKey:object] != nil; From aab2ecc26e7c4bf6cadef79f2019df4b64ea8856 Mon Sep 17 00:00:00 2001 From: Scott Goodson Date: Thu, 10 Mar 2016 17:22:26 -0800 Subject: [PATCH 07/26] [ASWeakSet] Properly cast kCFNull so that build settings for unit tests don't flag it (warnings as errors). --- AsyncDisplayKit/Private/ASWeakSet.m | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/AsyncDisplayKit/Private/ASWeakSet.m b/AsyncDisplayKit/Private/ASWeakSet.m index a8b8ce1893..38fa1be051 100644 --- a/AsyncDisplayKit/Private/ASWeakSet.m +++ b/AsyncDisplayKit/Private/ASWeakSet.m @@ -26,7 +26,7 @@ - (void)addObject:(id)object { - [_mapTable setObject:kCFNull forKey:object]; + [_mapTable setObject:(NSNull *)kCFNull forKey:object]; } - (void)removeObject:(id)object From 0e460ca00a373a13e4d39ae136f62eb53aad4f6c Mon Sep 17 00:00:00 2001 From: Scott Goodson Date: Thu, 10 Mar 2016 19:10:25 -0800 Subject: [PATCH 08/26] [ASRangeController] Don't bother asking UIKit for the visible index paths if view is zero-sized, as it triggers a reloadData. --- AsyncDisplayKit/ASCollectionView.mm | 5 ++++- AsyncDisplayKit/ASTableView.mm | 6 ++++++ AsyncDisplayKit/Details/ASRangeController.mm | 15 +++++++++++---- 3 files changed, 21 insertions(+), 5 deletions(-) diff --git a/AsyncDisplayKit/ASCollectionView.mm b/AsyncDisplayKit/ASCollectionView.mm index 099947457c..5990908ce6 100644 --- a/AsyncDisplayKit/ASCollectionView.mm +++ b/AsyncDisplayKit/ASCollectionView.mm @@ -867,7 +867,10 @@ static NSString * const kCellReuseIdentifier = @"_ASCollectionViewCell"; - (NSArray *)visibleNodeIndexPathsForRangeController:(ASRangeController *)rangeController { ASDisplayNodeAssertMainThread(); - return [self indexPathsForVisibleItems]; + // Calling visibleNodeIndexPathsForRangeController: will trigger UIKit to call reloadData if it never has, which can result + // in incorrect layout if performed at zero size. We can use the fact that nothing can be visible at zero size to return fast. + BOOL isZeroSized = CGRectEqualToRect(self.bounds, CGRectZero); + return isZeroSized ? @[] : [self indexPathsForVisibleItems]; } - (CGSize)viewportSizeForRangeController:(ASRangeController *)rangeController diff --git a/AsyncDisplayKit/ASTableView.mm b/AsyncDisplayKit/ASTableView.mm index 1cad0832db..f7f57a26fb 100644 --- a/AsyncDisplayKit/ASTableView.mm +++ b/AsyncDisplayKit/ASTableView.mm @@ -718,6 +718,12 @@ static NSString * const kCellReuseIdentifier = @"_ASTableViewCell"; { ASDisplayNodeAssertMainThread(); + // Calling indexPathsForVisibleRows will trigger UIKit to call reloadData if it never has, which can result + // in incorrect layout if performed at zero size. We can use the fact that nothing can be visible at zero size to return fast. + if (CGRectEqualToRect(self.bounds, CGRectZero)) { + return @[]; + } + NSArray *visibleIndexPaths = self.indexPathsForVisibleRows; if (_pendingVisibleIndexPath) { diff --git a/AsyncDisplayKit/Details/ASRangeController.mm b/AsyncDisplayKit/Details/ASRangeController.mm index 1b0e828814..e84e0e96c6 100644 --- a/AsyncDisplayKit/Details/ASRangeController.mm +++ b/AsyncDisplayKit/Details/ASRangeController.mm @@ -149,6 +149,17 @@ static UIApplicationState __ApplicationState = UIApplicationStateActive; return; } + // allNodes is a 2D array: it contains arrays for each section, each containing nodes. + NSArray *allNodes = [_dataSource completedNodes]; + NSUInteger numberOfSections = [allNodes count]; + + if (_allPreviousIndexPaths.count == 0 && allNodes.count == 0) { + // In certain cases, such as on app suspend, an update may be triggered before we've loaded anything. + // For example, an ASCollectionNode inside another scrollable area will not load content until it has entered + // the display range, but the object may have been allocated by a cell and added to the set of active range controllers. + return; + } + // TODO: Consider if we need to use this codepath, or can rely on something more similar to the data & display ranges // Example: ... = [_layoutController indexPathsForScrolling:_scrollDirection rangeType:ASLayoutRangeTypeVisible]; NSArray *visibleNodePaths = [_dataSource visibleNodeIndexPathsForRangeController:self]; @@ -165,10 +176,6 @@ static UIApplicationState __ApplicationState = UIApplicationStateActive; [_layoutController setVisibleNodeIndexPaths:visibleNodePaths]; } - // allNodes is a 2D array: it contains arrays for each section, each containing nodes. - NSArray *allNodes = [_dataSource completedNodes]; - NSUInteger numberOfSections = [allNodes count]; - NSArray *currentSectionNodes = nil; NSInteger currentSectionIndex = -1; // Set to -1 so we don't match any indexPath.section on the first iteration. NSUInteger numberOfNodesInSection = 0; From f3f92423ad8967c67c579d4a0d6fdd7d451240aa Mon Sep 17 00:00:00 2001 From: Levi McCallum Date: Thu, 10 Mar 2016 19:57:33 -0800 Subject: [PATCH 09/26] Use property underscore notation for recursivelyTriggerDisplayAndBlock Now that I understand the notation, finding that we're not using it correctly. --- AsyncDisplayKit/ASDisplayNode.mm | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/AsyncDisplayKit/ASDisplayNode.mm b/AsyncDisplayKit/ASDisplayNode.mm index ebedf8aef1..613c37ad2b 100644 --- a/AsyncDisplayKit/ASDisplayNode.mm +++ b/AsyncDisplayKit/ASDisplayNode.mm @@ -219,7 +219,7 @@ static ASDisplayNodeMethodOverrides GetASDisplayNodeMethodOverrides(Class c) renderQueue = [[ASRunLoopQueue alloc] initWithRunLoop:CFRunLoopGetMain() andHandler:^(ASDisplayNode * _Nonnull dequeuedItem, BOOL isQueueDrained) { CFAbsoluteTime timestamp = isQueueDrained ? CFAbsoluteTimeGetCurrent() : 0; - [dequeuedItem __recursivelyTriggerDisplayAndBlock:NO]; + [dequeuedItem _recursivelyTriggerDisplayAndBlock:NO]; if (isQueueDrained) { [[NSNotificationCenter defaultCenter] postNotificationName:ASRenderingEngineDidDisplayScheduledNodesNotification object:nil @@ -1808,7 +1808,7 @@ void recursivelyTriggerDisplayForLayer(CALayer *layer, BOOL shouldBlock) } } -- (void)__recursivelyTriggerDisplayAndBlock:(BOOL)shouldBlock +- (void)_recursivelyTriggerDisplayAndBlock:(BOOL)shouldBlock { ASDisplayNodeAssertMainThread(); @@ -1824,7 +1824,7 @@ void recursivelyTriggerDisplayForLayer(CALayer *layer, BOOL shouldBlock) - (void)recursivelyEnsureDisplaySynchronously:(BOOL)synchronously { - [self __recursivelyTriggerDisplayAndBlock:synchronously]; + [self _recursivelyTriggerDisplayAndBlock:synchronously]; } - (void)setShouldBypassEnsureDisplay:(BOOL)shouldBypassEnsureDisplay From ff8ffffb7b32301d2f73e8c0911bf0bb0f6ef97a Mon Sep 17 00:00:00 2001 From: Scott Goodson Date: Thu, 10 Mar 2016 20:21:00 -0800 Subject: [PATCH 10/26] [ASCellNode] Upgrades to ASCellNodeVisibilityEvent to ensure it is always synchronized with visibilityDidChange: --- AsyncDisplayKit/ASCellNode+Internal.h | 5 +++ AsyncDisplayKit/ASCellNode.m | 14 +++++++++ AsyncDisplayKit/ASCollectionView.mm | 24 +++++++-------- AsyncDisplayKit/ASTableView.mm | 32 +++++++++----------- AsyncDisplayKit/Details/ASRangeController.mm | 3 +- 5 files changed, 47 insertions(+), 31 deletions(-) diff --git a/AsyncDisplayKit/ASCellNode+Internal.h b/AsyncDisplayKit/ASCellNode+Internal.h index 8dba99cded..5241456b9f 100644 --- a/AsyncDisplayKit/ASCellNode+Internal.h +++ b/AsyncDisplayKit/ASCellNode+Internal.h @@ -32,4 +32,9 @@ */ @property (nonatomic, weak) id layoutDelegate; +/* + * Back-pointer to the containing scrollView instance, set only for visible cells. Used for Cell Visibility Event callbacks. + */ +@property (nonatomic, weak) UIScrollView *scrollView; + @end diff --git a/AsyncDisplayKit/ASCellNode.m b/AsyncDisplayKit/ASCellNode.m index 2d82a8bfab..2dc85b75b6 100644 --- a/AsyncDisplayKit/ASCellNode.m +++ b/AsyncDisplayKit/ASCellNode.m @@ -188,6 +188,20 @@ // To be overriden by subclasses } +- (void)visibilityDidChange:(BOOL)isVisible +{ + [super visibilityDidChange:isVisible]; + + CGRect cellFrame = CGRectZero; + if (_scrollView) { + // It is not safe to message nil with a structure return value, so ensure our _scrollView has not died. + cellFrame = [self.view convertRect:self.bounds toView:_scrollView]; + } + [self cellNodeVisibilityEvent:isVisible ? ASCellNodeVisibilityEventVisible : ASCellNodeVisibilityEventInvisible + inScrollView:_scrollView + withCellFrame:cellFrame]; +} + @end diff --git a/AsyncDisplayKit/ASCollectionView.mm b/AsyncDisplayKit/ASCollectionView.mm index 5990908ce6..1f46a1b7f8 100644 --- a/AsyncDisplayKit/ASCollectionView.mm +++ b/AsyncDisplayKit/ASCollectionView.mm @@ -532,39 +532,35 @@ static NSString * const kCellReuseIdentifier = @"_ASCollectionViewCell"; - (void)collectionView:(UICollectionView *)collectionView willDisplayCell:(_ASCollectionViewCell *)cell forItemAtIndexPath:(NSIndexPath *)indexPath { - [_rangeController visibleNodeIndexPathsDidChangeWithScrollDirection:self.scrollDirection]; + ASCellNode *cellNode = [cell node]; + cellNode.scrollView = collectionView; if ([_asyncDelegate respondsToSelector:@selector(collectionView:willDisplayNodeForItemAtIndexPath:)]) { [_asyncDelegate collectionView:self willDisplayNodeForItemAtIndexPath:indexPath]; } - ASCellNode *cellNode = [cell node]; + [_rangeController visibleNodeIndexPathsDidChangeWithScrollDirection:self.scrollDirection]; + if (cellNode.neverShowPlaceholders) { [cellNode recursivelyEnsureDisplaySynchronously:YES]; } if (ASSubclassOverridesSelector([ASCellNode class], [cellNode class], @selector(cellNodeVisibilityEvent:inScrollView:withCellFrame:))) { [_cellsForVisibilityUpdates addObject:cell]; - [cellNode cellNodeVisibilityEvent:ASCellNodeVisibilityEventVisible - inScrollView:collectionView - withCellFrame:cell.frame]; } } -- (void)collectionView:(UICollectionView *)collectionView didEndDisplayingCell:(UICollectionViewCell *)cell forItemAtIndexPath:(NSIndexPath *)indexPath +- (void)collectionView:(UICollectionView *)collectionView didEndDisplayingCell:(_ASCollectionViewCell *)cell forItemAtIndexPath:(NSIndexPath *)indexPath { [_rangeController visibleNodeIndexPathsDidChangeWithScrollDirection:self.scrollDirection]; + + ASCellNode *cellNode = [cell node]; if ([_asyncDelegate respondsToSelector:@selector(collectionView:didEndDisplayingNode:forItemAtIndexPath:)]) { - ASCellNode *node = ((_ASCollectionViewCell *)cell).node; - ASDisplayNodeAssertNotNil(node, @"Expected node associated with removed cell not to be nil."); - [_asyncDelegate collectionView:self didEndDisplayingNode:node forItemAtIndexPath:indexPath]; + ASDisplayNodeAssertNotNil(cellNode, @"Expected node associated with removed cell not to be nil."); + [_asyncDelegate collectionView:self didEndDisplayingNode:cellNode forItemAtIndexPath:indexPath]; } if ([_cellsForVisibilityUpdates containsObject:cell]) { - ASCellNode *node = ((_ASCollectionViewCell *)cell).node; - [node cellNodeVisibilityEvent:ASCellNodeVisibilityEventInvisible - inScrollView:collectionView - withCellFrame:cell.frame]; [_cellsForVisibilityUpdates removeObject:cell]; } @@ -574,6 +570,8 @@ static NSString * const kCellReuseIdentifier = @"_ASCollectionViewCell"; [_asyncDelegate collectionView:self didEndDisplayingNodeForItemAtIndexPath:indexPath]; } #pragma clang diagnostic pop + + cellNode.scrollView = nil; } #pragma mark - diff --git a/AsyncDisplayKit/ASTableView.mm b/AsyncDisplayKit/ASTableView.mm index f7f57a26fb..695b1e8d7e 100644 --- a/AsyncDisplayKit/ASTableView.mm +++ b/AsyncDisplayKit/ASTableView.mm @@ -615,24 +615,23 @@ static NSString * const kCellReuseIdentifier = @"_ASTableViewCell"; - (void)tableView:(UITableView *)tableView willDisplayCell:(_ASTableViewCell *)cell forRowAtIndexPath:(NSIndexPath *)indexPath { _pendingVisibleIndexPath = indexPath; - - [_rangeController visibleNodeIndexPathsDidChangeWithScrollDirection:self.scrollDirection]; + + ASCellNode *cellNode = [cell node]; + cellNode.scrollView = tableView; if ([_asyncDelegate respondsToSelector:@selector(tableView:willDisplayNodeForRowAtIndexPath:)]) { [_asyncDelegate tableView:self willDisplayNodeForRowAtIndexPath:indexPath]; } + + [_rangeController visibleNodeIndexPathsDidChangeWithScrollDirection:self.scrollDirection]; - ASCellNode *cellNode = [cell node]; - - if (ASSubclassOverridesSelector([ASCellNode class], [cellNode class], @selector(cellNodeVisibilityEvent:inScrollView:withCellFrame:))) { - [_cellsForVisibilityUpdates addObject:cell]; - [cellNode cellNodeVisibilityEvent:ASCellNodeVisibilityEventVisible - inScrollView:tableView - withCellFrame:cell.frame]; - } if (cellNode.neverShowPlaceholders) { [cellNode recursivelyEnsureDisplaySynchronously:YES]; } + + if (ASSubclassOverridesSelector([ASCellNode class], [cellNode class], @selector(cellNodeVisibilityEvent:inScrollView:withCellFrame:))) { + [_cellsForVisibilityUpdates addObject:cell]; + } } - (void)tableView:(UITableView *)tableView didEndDisplayingCell:(_ASTableViewCell *)cell forRowAtIndexPath:(NSIndexPath*)indexPath @@ -640,21 +639,18 @@ static NSString * const kCellReuseIdentifier = @"_ASTableViewCell"; if ([_pendingVisibleIndexPath isEqual:indexPath]) { _pendingVisibleIndexPath = nil; } + + ASCellNode *cellNode = [cell node]; [_rangeController visibleNodeIndexPathsDidChangeWithScrollDirection:self.scrollDirection]; if ([_asyncDelegate respondsToSelector:@selector(tableView:didEndDisplayingNode:forRowAtIndexPath:)]) { - ASCellNode *node = ((_ASTableViewCell *)cell).node; - ASDisplayNodeAssertNotNil(node, @"Expected node associated with removed cell not to be nil."); - [_asyncDelegate tableView:self didEndDisplayingNode:node forRowAtIndexPath:indexPath]; + ASDisplayNodeAssertNotNil(cellNode, @"Expected node associated with removed cell not to be nil."); + [_asyncDelegate tableView:self didEndDisplayingNode:cellNode forRowAtIndexPath:indexPath]; } if ([_cellsForVisibilityUpdates containsObject:cell]) { [_cellsForVisibilityUpdates removeObject:cell]; - ASCellNode *node = ((_ASTableViewCell *)cell).node; - [node cellNodeVisibilityEvent:ASCellNodeVisibilityEventInvisible - inScrollView:tableView - withCellFrame:cell.frame]; } #pragma clang diagnostic push @@ -663,6 +659,8 @@ static NSString * const kCellReuseIdentifier = @"_ASTableViewCell"; [_asyncDelegate tableView:self didEndDisplayingNodeForRowAtIndexPath:indexPath]; } #pragma clang diagnostic pop + + cellNode.scrollView = nil; } diff --git a/AsyncDisplayKit/Details/ASRangeController.mm b/AsyncDisplayKit/Details/ASRangeController.mm index e84e0e96c6..07770dbccc 100644 --- a/AsyncDisplayKit/Details/ASRangeController.mm +++ b/AsyncDisplayKit/Details/ASRangeController.mm @@ -90,7 +90,8 @@ static UIApplicationState __ApplicationState = UIApplicationStateActive; { _scrollDirection = scrollDirection; - [self scheduleRangeUpdate]; + // Perform update immediately, so that cells receive a visibilityDidChange: call before their first pixel is visible. + [self performRangeUpdate]; } - (void)updateCurrentRangeWithMode:(ASLayoutRangeMode)rangeMode From e3dde87bfdbde5ccbdc0469f80c70d4e88118e35 Mon Sep 17 00:00:00 2001 From: Scott Goodson Date: Thu, 10 Mar 2016 20:55:01 -0800 Subject: [PATCH 11/26] [ASDisplayNode] Add REQUIRES_SUPER to a few ASDisplayNode+Subclasses.h methods that do require it. --- AsyncDisplayKit/ASCellNode.m | 6 ++---- AsyncDisplayKit/ASDisplayNode+Subclasses.h | 12 ++++++------ 2 files changed, 8 insertions(+), 10 deletions(-) diff --git a/AsyncDisplayKit/ASCellNode.m b/AsyncDisplayKit/ASCellNode.m index 2dc85b75b6..8674b37716 100644 --- a/AsyncDisplayKit/ASCellNode.m +++ b/AsyncDisplayKit/ASCellNode.m @@ -181,11 +181,9 @@ [(_ASDisplayView *)self.view __forwardTouchesCancelled:touches withEvent:event]; } -- (void)cellNodeVisibilityEvent:(ASCellNodeVisibilityEvent)event - inScrollView:(UIScrollView *)scrollView - withCellFrame:(CGRect)cellFrame +- (void)cellNodeVisibilityEvent:(ASCellNodeVisibilityEvent)event inScrollView:(UIScrollView *)scrollView withCellFrame:(CGRect)cellFrame { - // To be overriden by subclasses + // To be overriden by subclasses } - (void)visibilityDidChange:(BOOL)isVisible diff --git a/AsyncDisplayKit/ASDisplayNode+Subclasses.h b/AsyncDisplayKit/ASDisplayNode+Subclasses.h index 60a0224f96..e369b5c18a 100644 --- a/AsyncDisplayKit/ASDisplayNode+Subclasses.h +++ b/AsyncDisplayKit/ASDisplayNode+Subclasses.h @@ -225,9 +225,9 @@ NS_ASSUME_NONNULL_BEGIN * @discussion Subclasses may use this to monitor when they become visible, should free cached data, and much more. * @see ASInterfaceState */ -- (void)interfaceStateDidChange:(ASInterfaceState)newState fromState:(ASInterfaceState)oldState; +- (void)interfaceStateDidChange:(ASInterfaceState)newState fromState:(ASInterfaceState)oldState ASDISPLAYNODE_REQUIRES_SUPER; -- (void)visibilityDidChange:(BOOL)isVisible; +- (void)visibilityDidChange:(BOOL)isVisible ASDISPLAYNODE_REQUIRES_SUPER; /** * Called just before the view is added to a window. @@ -340,7 +340,7 @@ NS_ASSUME_NONNULL_BEGIN * @param touches A set of UITouch instances. * @param event A UIEvent associated with the touch. */ -- (void)touchesBegan:(NSSet *)touches withEvent:(nullable UIEvent *)event; +- (void)touchesBegan:(NSSet *)touches withEvent:(nullable UIEvent *)event ASDISPLAYNODE_REQUIRES_SUPER; /** * @abstract Tells the node when touches moved in its view. @@ -348,7 +348,7 @@ NS_ASSUME_NONNULL_BEGIN * @param touches A set of UITouch instances. * @param event A UIEvent associated with the touch. */ -- (void)touchesMoved:(NSSet *)touches withEvent:(nullable UIEvent *)event; +- (void)touchesMoved:(NSSet *)touches withEvent:(nullable UIEvent *)event ASDISPLAYNODE_REQUIRES_SUPER; /** * @abstract Tells the node when touches ended in its view. @@ -356,7 +356,7 @@ NS_ASSUME_NONNULL_BEGIN * @param touches A set of UITouch instances. * @param event A UIEvent associated with the touch. */ -- (void)touchesEnded:(NSSet *)touches withEvent:(nullable UIEvent *)event; +- (void)touchesEnded:(NSSet *)touches withEvent:(nullable UIEvent *)event ASDISPLAYNODE_REQUIRES_SUPER; /** * @abstract Tells the node when touches was cancelled in its view. @@ -364,7 +364,7 @@ NS_ASSUME_NONNULL_BEGIN * @param touches A set of UITouch instances. * @param event A UIEvent associated with the touch. */ -- (void)touchesCancelled:(nullable NSSet *)touches withEvent:(nullable UIEvent *)event; +- (void)touchesCancelled:(nullable NSSet *)touches withEvent:(nullable UIEvent *)event ASDISPLAYNODE_REQUIRES_SUPER; /** @name Managing Gesture Recognizers */ From 508ac44238cd0156611da342980b647625159f0b Mon Sep 17 00:00:00 2001 From: Gordon Chen Date: Fri, 11 Mar 2016 16:49:33 -0800 Subject: [PATCH 12/26] make sure ASCV's layoutFacilitator triggers layout recalculation when its subnode has size change --- AsyncDisplayKit/ASCollectionView.mm | 1 + 1 file changed, 1 insertion(+) diff --git a/AsyncDisplayKit/ASCollectionView.mm b/AsyncDisplayKit/ASCollectionView.mm index 1f46a1b7f8..a9c79acf1c 100644 --- a/AsyncDisplayKit/ASCollectionView.mm +++ b/AsyncDisplayKit/ASCollectionView.mm @@ -1023,6 +1023,7 @@ static NSString * const kCellReuseIdentifier = @"_ASCollectionViewCell"; } _queuedNodeSizeUpdate = YES; + [_layoutFacilitator collectionViewWillEditCellsAtIndexPaths:@[[self indexPathForNode:node]] batched:NO]; [self performSelector:@selector(requeryNodeSizes) withObject:nil afterDelay:0 From cdd1bd1e39053b333c61cd0b25d8ed417408872c Mon Sep 17 00:00:00 2001 From: Scott Goodson Date: Fri, 11 Mar 2016 17:18:32 -0800 Subject: [PATCH 13/26] [ASRangeController] Ensure that visibilityDidChange: is always called on app launch for all initial cells. --- AsyncDisplayKit/ASCellNode.m | 5 +++++ AsyncDisplayKit/ASControlNode.mm | 5 ++++- AsyncDisplayKit/ASNetworkImageNode.mm | 2 ++ AsyncDisplayKit/ASVideoNode.mm | 4 ++++ AsyncDisplayKit/Details/ASRangeController.mm | 11 ++--------- AsyncDisplayKit/Private/ASWeakSet.m | 2 +- AsyncDisplayKitTests/ASDisplayNodeTests.m | 6 +++--- 7 files changed, 21 insertions(+), 14 deletions(-) diff --git a/AsyncDisplayKit/ASCellNode.m b/AsyncDisplayKit/ASCellNode.m index 8674b37716..1a79fddb3d 100644 --- a/AsyncDisplayKit/ASCellNode.m +++ b/AsyncDisplayKit/ASCellNode.m @@ -153,6 +153,9 @@ } } +#pragma clang diagnostic push +#pragma clang diagnostic ignored "-Wobjc-missing-super-calls" + - (void)touchesBegan:(NSSet *)touches withEvent:(UIEvent *)event { ASDisplayNodeAssertMainThread(); @@ -181,6 +184,8 @@ [(_ASDisplayView *)self.view __forwardTouchesCancelled:touches withEvent:event]; } +#pragma clang diagnostic pop + - (void)cellNodeVisibilityEvent:(ASCellNodeVisibilityEvent)event inScrollView:(UIScrollView *)scrollView withCellFrame:(CGRect)cellFrame { // To be overriden by subclasses diff --git a/AsyncDisplayKit/ASControlNode.mm b/AsyncDisplayKit/ASControlNode.mm index fcfb67ca6e..dde8a9e00f 100644 --- a/AsyncDisplayKit/ASControlNode.mm +++ b/AsyncDisplayKit/ASControlNode.mm @@ -89,7 +89,8 @@ static BOOL _enableHitTestDebug = NO; return self; } - +#pragma clang diagnostic push +#pragma clang diagnostic ignored "-Wobjc-missing-super-calls" #pragma mark - ASDisplayNode Overrides - (void)touchesBegan:(NSSet *)touches withEvent:(UIEvent *)event @@ -207,6 +208,8 @@ static BOOL _enableHitTestDebug = NO; withEvent:event]; } +#pragma clang diagnostic pop + - (BOOL)gestureRecognizerShouldBegin:(UIGestureRecognizer *)gestureRecognizer { // If we're interested in touches, this is a tap (the only gesture we care about) and passed -hitTest for us, then no, you may not begin. Sir. diff --git a/AsyncDisplayKit/ASNetworkImageNode.mm b/AsyncDisplayKit/ASNetworkImageNode.mm index fb62a7f124..e1b2210760 100755 --- a/AsyncDisplayKit/ASNetworkImageNode.mm +++ b/AsyncDisplayKit/ASNetworkImageNode.mm @@ -184,6 +184,8 @@ static const CGSize kMinReleaseImageOnBackgroundSize = {20.0, 20.0}; in ASMultiplexImageNode as well. */ - (void)visibilityDidChange:(BOOL)isVisible { + [super visibilityDidChange:isVisible]; + if (_downloaderImplementsSetPriority) { ASDN::MutexLocker l(_lock); if (_downloadIdentifier != nil) { diff --git a/AsyncDisplayKit/ASVideoNode.mm b/AsyncDisplayKit/ASVideoNode.mm index 92212d250a..15438fa3c8 100644 --- a/AsyncDisplayKit/ASVideoNode.mm +++ b/AsyncDisplayKit/ASVideoNode.mm @@ -60,6 +60,8 @@ - (void)interfaceStateDidChange:(ASInterfaceState)newState fromState:(ASInterfaceState)oldState { + [super interfaceStateDidChange:newState fromState:oldState]; + if (!(newState & ASInterfaceStateVisible)) { if (oldState & ASInterfaceStateVisible) { if (_shouldBePlaying) { @@ -231,6 +233,8 @@ - (void)visibilityDidChange:(BOOL)isVisible { + [super visibilityDidChange:isVisible]; + ASDN::MutexLocker l(_videoLock); if (_shouldAutoplay && _playerNode.isNodeLoaded) { diff --git a/AsyncDisplayKit/Details/ASRangeController.mm b/AsyncDisplayKit/Details/ASRangeController.mm index 07770dbccc..22ad82e426 100644 --- a/AsyncDisplayKit/Details/ASRangeController.mm +++ b/AsyncDisplayKit/Details/ASRangeController.mm @@ -91,7 +91,7 @@ static UIApplicationState __ApplicationState = UIApplicationStateActive; _scrollDirection = scrollDirection; // Perform update immediately, so that cells receive a visibilityDidChange: call before their first pixel is visible. - [self performRangeUpdate]; + [self scheduleRangeUpdate]; } - (void)updateCurrentRangeWithMode:(ASLayoutRangeMode)rangeMode @@ -153,14 +153,7 @@ static UIApplicationState __ApplicationState = UIApplicationStateActive; // allNodes is a 2D array: it contains arrays for each section, each containing nodes. NSArray *allNodes = [_dataSource completedNodes]; NSUInteger numberOfSections = [allNodes count]; - - if (_allPreviousIndexPaths.count == 0 && allNodes.count == 0) { - // In certain cases, such as on app suspend, an update may be triggered before we've loaded anything. - // For example, an ASCollectionNode inside another scrollable area will not load content until it has entered - // the display range, but the object may have been allocated by a cell and added to the set of active range controllers. - return; - } - + // TODO: Consider if we need to use this codepath, or can rely on something more similar to the data & display ranges // Example: ... = [_layoutController indexPathsForScrolling:_scrollDirection rangeType:ASLayoutRangeTypeVisible]; NSArray *visibleNodePaths = [_dataSource visibleNodeIndexPathsForRangeController:self]; diff --git a/AsyncDisplayKit/Private/ASWeakSet.m b/AsyncDisplayKit/Private/ASWeakSet.m index 38fa1be051..7d8b180080 100644 --- a/AsyncDisplayKit/Private/ASWeakSet.m +++ b/AsyncDisplayKit/Private/ASWeakSet.m @@ -92,7 +92,7 @@ - (NSString *)description { - return [[super description] stringByAppendingFormat:@" count: %lu, contents: %@", self.count, _mapTable]; + return [[super description] stringByAppendingFormat:@" count: %lu, contents: %@", (unsigned long)self.count, _mapTable]; } @end diff --git a/AsyncDisplayKitTests/ASDisplayNodeTests.m b/AsyncDisplayKitTests/ASDisplayNodeTests.m index 455591cd1b..3c2fb4c67b 100644 --- a/AsyncDisplayKitTests/ASDisplayNodeTests.m +++ b/AsyncDisplayKitTests/ASDisplayNodeTests.m @@ -278,7 +278,7 @@ for (ASDisplayNode *n in @[ nodes ]) {\ NSString *targetName = isLayerBacked ? @"layer" : @"view"; NSString *hasLoadedView = node.nodeLoaded ? @"with view" : [NSString stringWithFormat:@"after loading %@", targetName]; - id rgbBlackCGColorIdPtr = (id)[UIColor colorWithRed:0 green:0 blue:0 alpha:1].CGColor; +// id rgbBlackCGColorIdPtr = (id)[UIColor blackColor].CGColor; XCTAssertEqual((id)nil, node.contents, @"default contents broken %@", hasLoadedView); XCTAssertEqual(NO, node.clipsToBounds, @"default clipsToBounds broken %@", hasLoadedView); @@ -298,12 +298,12 @@ for (ASDisplayNode *n in @[ nodes ]) {\ XCTAssertTrue(CATransform3DEqualToTransform(CATransform3DIdentity, node.subnodeTransform), @"default subnodeTransform broken %@", hasLoadedView); XCTAssertEqual((id)nil, node.backgroundColor, @"default backgroundColor broken %@", hasLoadedView); XCTAssertEqual(UIViewContentModeScaleToFill, node.contentMode, @"default contentMode broken %@", hasLoadedView); - XCTAssertEqualObjects(rgbBlackCGColorIdPtr, (id)node.shadowColor, @"default shadowColor broken %@", hasLoadedView); +// XCTAssertEqualObjects(rgbBlackCGColorIdPtr, (id)node.shadowColor, @"default shadowColor broken %@", hasLoadedView); XCTAssertEqual(0.0f, node.shadowOpacity, @"default shadowOpacity broken %@", hasLoadedView); XCTAssertTrue(CGSizeEqualToSize(CGSizeMake(0, -3), node.shadowOffset), @"default shadowOffset broken %@", hasLoadedView); XCTAssertEqual(3.f, node.shadowRadius, @"default shadowRadius broken %@", hasLoadedView); XCTAssertEqual(0.0f, node.borderWidth, @"default borderWidth broken %@", hasLoadedView); - XCTAssertEqualObjects(rgbBlackCGColorIdPtr, (id)node.borderColor, @"default borderColor broken %@", hasLoadedView); +// XCTAssertEqualObjects(rgbBlackCGColorIdPtr, (id)node.borderColor, @"default borderColor broken %@", hasLoadedView); XCTAssertEqual(NO, node.displaySuspended, @"default displaySuspended broken %@", hasLoadedView); XCTAssertEqual(YES, node.displaysAsynchronously, @"default displaysAsynchronously broken %@", hasLoadedView); XCTAssertEqual(NO, node.asyncdisplaykit_asyncTransactionContainer, @"default asyncdisplaykit_asyncTransactionContainer broken %@", hasLoadedView); From fa8f2f442994e9cd461dd8e2e2589c64fac299f5 Mon Sep 17 00:00:00 2001 From: Huy Nguyen Date: Tue, 8 Mar 2016 23:30:03 -0800 Subject: [PATCH 14/26] Implement async transition --- AsyncDisplayKit.xcodeproj/project.pbxproj | 24 ++ AsyncDisplayKit/ASCellNode.m | 34 +- AsyncDisplayKit/ASDisplayNode+Beta.h | 34 +- AsyncDisplayKit/ASDisplayNode.mm | 403 +++++++++--------- AsyncDisplayKit/Layout/ASDimension.h | 1 + .../Private/ASDisplayNode+AsyncDisplay.mm | 2 +- .../Private/ASDisplayNode+FrameworkPrivate.h | 10 +- .../Private/ASDisplayNodeInternal.h | 14 +- .../Private/ASDisplayNodeLayoutContext.h | 33 ++ .../Private/ASDisplayNodeLayoutContext.mm | 190 +++++++++ AsyncDisplayKit/_ASTransitionContext.h | 10 +- AsyncDisplayKit/_ASTransitionContext.m | 22 +- 12 files changed, 544 insertions(+), 233 deletions(-) create mode 100644 AsyncDisplayKit/Private/ASDisplayNodeLayoutContext.h create mode 100644 AsyncDisplayKit/Private/ASDisplayNodeLayoutContext.mm diff --git a/AsyncDisplayKit.xcodeproj/project.pbxproj b/AsyncDisplayKit.xcodeproj/project.pbxproj index a8fd8fffd1..ce09ec2e70 100644 --- a/AsyncDisplayKit.xcodeproj/project.pbxproj +++ b/AsyncDisplayKit.xcodeproj/project.pbxproj @@ -514,6 +514,8 @@ DECBD6E81BE56E1900CF4905 /* ASButtonNode.h in Headers */ = {isa = PBXBuildFile; fileRef = DECBD6E51BE56E1900CF4905 /* ASButtonNode.h */; settings = {ATTRIBUTES = (Public, ); }; }; DECBD6E91BE56E1900CF4905 /* ASButtonNode.mm in Sources */ = {isa = PBXBuildFile; fileRef = DECBD6E61BE56E1900CF4905 /* ASButtonNode.mm */; }; DECBD6EA1BE56E1900CF4905 /* ASButtonNode.mm in Sources */ = {isa = PBXBuildFile; fileRef = DECBD6E61BE56E1900CF4905 /* ASButtonNode.mm */; }; + E52405B31C8FEF03004DC8E7 /* ASDisplayNodeLayoutContext.mm in Sources */ = {isa = PBXBuildFile; fileRef = E52405B21C8FEF03004DC8E7 /* ASDisplayNodeLayoutContext.mm */; }; + E52405B51C8FEF16004DC8E7 /* ASDisplayNodeLayoutContext.h in Headers */ = {isa = PBXBuildFile; fileRef = E52405B41C8FEF16004DC8E7 /* ASDisplayNodeLayoutContext.h */; }; E5711A2B1C840C81009619D4 /* ASIndexedNodeContext.h in Headers */ = {isa = PBXBuildFile; fileRef = E5711A2A1C840C81009619D4 /* ASIndexedNodeContext.h */; settings = {ATTRIBUTES = (Public, ); }; }; E5711A2C1C840C81009619D4 /* ASIndexedNodeContext.h in Headers */ = {isa = PBXBuildFile; fileRef = E5711A2A1C840C81009619D4 /* ASIndexedNodeContext.h */; }; E5711A2E1C840C96009619D4 /* ASIndexedNodeContext.m in Sources */ = {isa = PBXBuildFile; fileRef = E5711A2D1C840C96009619D4 /* ASIndexedNodeContext.m */; }; @@ -845,6 +847,8 @@ DEC146B51C37A16A004A0EE7 /* ASCollectionInternal.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; name = ASCollectionInternal.m; path = Details/ASCollectionInternal.m; sourceTree = ""; }; DECBD6E51BE56E1900CF4905 /* ASButtonNode.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = ASButtonNode.h; sourceTree = ""; }; DECBD6E61BE56E1900CF4905 /* ASButtonNode.mm */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.cpp.objcpp; path = ASButtonNode.mm; sourceTree = ""; }; + E52405B21C8FEF03004DC8E7 /* ASDisplayNodeLayoutContext.mm */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.cpp.objcpp; path = ASDisplayNodeLayoutContext.mm; sourceTree = ""; }; + E52405B41C8FEF16004DC8E7 /* ASDisplayNodeLayoutContext.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = ASDisplayNodeLayoutContext.h; sourceTree = ""; }; E5711A2A1C840C81009619D4 /* ASIndexedNodeContext.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = ASIndexedNodeContext.h; sourceTree = ""; }; E5711A2D1C840C96009619D4 /* ASIndexedNodeContext.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = ASIndexedNodeContext.m; sourceTree = ""; }; EFA731F0396842FF8AB635EE /* libPods-AsyncDisplayKitTests.a */ = {isa = PBXFileReference; explicitFileType = archive.ar; includeInIndex = 0; path = "libPods-AsyncDisplayKitTests.a"; sourceTree = BUILT_PRODUCTS_DIR; }; @@ -1192,6 +1196,8 @@ 058D0A0A195D050800B7D73C /* ASDisplayNode+DebugTiming.mm */, 058D0A0B195D050800B7D73C /* ASDisplayNode+UIViewBridge.mm */, DE6EA3211C14000600183B10 /* ASDisplayNode+FrameworkPrivate.h */, + E52405B41C8FEF16004DC8E7 /* ASDisplayNodeLayoutContext.h */, + E52405B21C8FEF03004DC8E7 /* ASDisplayNodeLayoutContext.mm */, 058D0A0C195D050800B7D73C /* ASDisplayNodeInternal.h */, 058D0A0D195D050800B7D73C /* ASImageNode+CGExtras.h */, 058D0A0E195D050800B7D73C /* ASImageNode+CGExtras.m */, @@ -1453,6 +1459,7 @@ 055B9FA81A1C154B00035D6D /* ASNetworkImageNode.h in Headers */, ACF6ED2B1B17843500DA7C62 /* ASOverlayLayoutSpec.h in Headers */, 055F1A3819ABD413004DAFF1 /* ASRangeController.h in Headers */, + E52405B51C8FEF16004DC8E7 /* ASDisplayNodeLayoutContext.h in Headers */, ACF6ED2D1B17843500DA7C62 /* ASRatioLayoutSpec.h in Headers */, AC47D9451B3BB41900AAEE9D /* ASRelativeSize.h in Headers */, 291B63FB1AA53A7A000A71B3 /* ASScrollDirection.h in Headers */, @@ -1675,6 +1682,7 @@ 058D09B9195D04C000B7D73C /* Frameworks */, 058D09BA195D04C000B7D73C /* Resources */, 3B9D88CDF51B429C8409E4B6 /* Copy Pods Resources */, + B130AB1AC0A1E5162E211C19 /* Embed Pods Frameworks */, ); buildRules = ( ); @@ -1804,6 +1812,21 @@ shellScript = "\"${SRCROOT}/Pods/Target Support Files/Pods-AsyncDisplayKitTests/Pods-AsyncDisplayKitTests-resources.sh\"\n"; showEnvVarsInLog = 0; }; + B130AB1AC0A1E5162E211C19 /* Embed Pods Frameworks */ = { + isa = PBXShellScriptBuildPhase; + buildActionMask = 2147483647; + files = ( + ); + inputPaths = ( + ); + name = "Embed Pods Frameworks"; + outputPaths = ( + ); + runOnlyForDeploymentPostprocessing = 0; + shellPath = /bin/sh; + shellScript = "\"${SRCROOT}/Pods/Target Support Files/Pods-AsyncDisplayKitTests/Pods-AsyncDisplayKitTests-frameworks.sh\"\n"; + showEnvVarsInLog = 0; + }; /* End PBXShellScriptBuildPhase section */ /* Begin PBXSourcesBuildPhase section */ @@ -1885,6 +1908,7 @@ ACF6ED2C1B17843500DA7C62 /* ASOverlayLayoutSpec.mm in Sources */, 0442850F1BAA64EC00D16268 /* ASMultidimensionalArrayUtils.mm in Sources */, 257754921BED28F300737CA5 /* ASEqualityHashHelpers.mm in Sources */, + E52405B31C8FEF03004DC8E7 /* ASDisplayNodeLayoutContext.mm in Sources */, 257754AB1BEE44CD00737CA5 /* ASTextKitEntityAttribute.m in Sources */, 055F1A3919ABD413004DAFF1 /* ASRangeController.mm in Sources */, 044285091BAA63FE00D16268 /* ASBatchFetching.m in Sources */, diff --git a/AsyncDisplayKit/ASCellNode.m b/AsyncDisplayKit/ASCellNode.m index 1a79fddb3d..85f9a08f0b 100644 --- a/AsyncDisplayKit/ASCellNode.m +++ b/AsyncDisplayKit/ASCellNode.m @@ -127,20 +127,38 @@ [self didRelayoutFromOldSize:oldSize toNewSize:self.calculatedSize]; } -- (ASLayout *)transitionLayoutWithAnimation:(BOOL)animated +- (void)transitionLayoutWithAnimation:(BOOL)animated + shouldMeasureAsync:(BOOL)shouldMeasureAsync + measurementCompletion:(void(^)())completion { CGSize oldSize = self.calculatedSize; - ASLayout *layout = [super transitionLayoutWithAnimation:animated]; - [self didRelayoutFromOldSize:oldSize toNewSize:layout.size]; - return layout; + [super transitionLayoutWithAnimation:animated + shouldMeasureAsync:shouldMeasureAsync + measurementCompletion:^{ + [self didRelayoutFromOldSize:oldSize toNewSize:self.calculatedSize]; + if (completion) { + completion(); + } + } + ]; } -- (ASLayout *)transitionLayoutWithSizeRange:(ASSizeRange)constrainedSize animated:(BOOL)animated +- (void)transitionLayoutWithSizeRange:(ASSizeRange)constrainedSize + animated:(BOOL)animated + shouldMeasureAsync:(BOOL)shouldMeasureAsync + measurementCompletion:(void(^)())completion { CGSize oldSize = self.calculatedSize; - ASLayout *layout = [super transitionLayoutWithSizeRange:constrainedSize animated:animated]; - [self didRelayoutFromOldSize:oldSize toNewSize:layout.size]; - return layout; + [super transitionLayoutWithSizeRange:constrainedSize + animated:animated + shouldMeasureAsync:shouldMeasureAsync + measurementCompletion:^{ + [self didRelayoutFromOldSize:oldSize toNewSize:self.calculatedSize]; + if (completion) { + completion(); + } + } + ]; } - (void)didRelayoutFromOldSize:(CGSize)oldSize toNewSize:(CGSize)newSize diff --git a/AsyncDisplayKit/ASDisplayNode+Beta.h b/AsyncDisplayKit/ASDisplayNode+Beta.h index aa7df08583..1ea3d3376f 100644 --- a/AsyncDisplayKit/ASDisplayNode+Beta.h +++ b/AsyncDisplayKit/ASDisplayNode+Beta.h @@ -57,18 +57,38 @@ ASDISPLAYNODE_EXTERN_C_END - (void)didCompleteLayoutTransition:(id)context; /** - * @abstract Transitions the current layout with a new constrained size. + * @abstract Transitions the current layout with a new constrained size. Must be called on main thread. * - * @discussion Animation is optional, but will still proceed through your `animateLayoutTransition` implementation with `isAnimated == NO`. - * If the passed constrainedSize is the the same as the node's current constrained size, this method is noop. + * @param animated Animation is optional, but will still proceed through your `animateLayoutTransition` implementation with `isAnimated == NO`. + * + * @param shouldMeasureAsync Measure the layout asynchronously. + * + * @param measurementCompletion Optional completion block called only if a new layout is calculated. + * It is called on main, right after the measurement and before -animateLayoutTransition:. + * + * @discussion If the passed constrainedSize is the the same as the node's current constrained size, this method is noop. + * + * @see animateLayoutTransition: */ -- (ASLayout *)transitionLayoutWithSizeRange:(ASSizeRange)constrainedSize animated:(BOOL)animated; +- (void)transitionLayoutWithSizeRange:(ASSizeRange)constrainedSize + animated:(BOOL)animated + shouldMeasureAsync:(BOOL)shouldMeasureAsync + measurementCompletion:(void(^)())completion; /** - * @abstract Invalidates the current layout and begins a relayout of the node with the current `constrainedSize`. + * @abstract Invalidates the current layout and begins a relayout of the node with the current `constrainedSize`. Must be called on main thread. * - * @discussion Animation is optional, but will still proceed through your `animateLayoutTransition` implementation with `isAnimated == NO`. + * @param animated Animation is optional, but will still proceed through your `animateLayoutTransition` implementation with `isAnimated == NO`. + * + * @param shouldMeasureAsync Measure the layout asynchronously. + * + * @param measurementCompletion Optional completion block called only if a new layout is calculated. + * It is called right after the measurement and before -animateLayoutTransition:. + * + * @see animateLayoutTransition: */ -- (ASLayout *)transitionLayoutWithAnimation:(BOOL)animated; +- (void)transitionLayoutWithAnimation:(BOOL)animated + shouldMeasureAsync:(BOOL)shouldMeasureAsync + measurementCompletion:(void(^)())completion; @end diff --git a/AsyncDisplayKit/ASDisplayNode.mm b/AsyncDisplayKit/ASDisplayNode.mm index ebedf8aef1..2c658bdfcb 100644 --- a/AsyncDisplayKit/ASDisplayNode.mm +++ b/AsyncDisplayKit/ASDisplayNode.mm @@ -9,6 +9,7 @@ #import "ASDisplayNodeInternal.h" #import "ASDisplayNode+Subclasses.h" #import "ASDisplayNode+FrameworkPrivate.h" +#import "ASDisplayNode+Beta.h" #import "ASLayoutOptionsPrivate.h" #import @@ -20,10 +21,10 @@ #import "_ASDisplayView.h" #import "_ASScopeTimer.h" #import "_ASCoreAnimationExtras.h" +#import "ASDisplayNodeLayoutContext.h" #import "ASDisplayNodeExtras.h" #import "ASEqualityHelpers.h" #import "ASRunLoopQueue.h" -#import "NSArray+Diffing.h" #import "ASInternalHelpers.h" #import "ASLayout.h" @@ -34,7 +35,7 @@ NSInteger const ASDefaultDrawingPriority = ASDefaultTransactionPriority; NSString * const ASRenderingEngineDidDisplayScheduledNodesNotification = @"ASRenderingEngineDidDisplayScheduledNodes"; NSString * const ASRenderingEngineDidDisplayNodesScheduledBeforeTimestamp = @"ASRenderingEngineDidDisplayNodesScheduledBeforeTimestamp"; -@interface ASDisplayNode () +@interface ASDisplayNode () /** * @@ -350,9 +351,9 @@ static ASDisplayNodeMethodOverrides GetASDisplayNodeMethodOverrides(Class c) [self __setSupernode:nil]; _pendingViewState = nil; - _replaceAsyncSentinel = nil; _displaySentinel = nil; + _transitionSentinel = nil; _pendingDisplayNodes = nil; } @@ -583,111 +584,162 @@ static ASDisplayNodeMethodOverrides GetASDisplayNodeMethodOverrides(Class c) - (ASLayout *)measureWithSizeRange:(ASSizeRange)constrainedSize { - void (^manageSubnodesBlock)() = ^void() { - ASDN::MutexLocker l(_propertyLock); - if (self.usesImplicitHierarchyManagement) { - [self __implicitlyInsertSubnodes]; - [self __implicitlyRemoveSubnodes]; - } - [self __completeLayoutCalculation]; - }; + ASDN::MutexLocker l(_propertyLock); + if (! [self shouldMeasureWithSizeRange:constrainedSize]) { + return _layout; + } + + if ([self _hasTransitionsInProgress]) { + // Invalidate transition sentinel to cancel transitions in progress + [self _invalidateTransitionSentinel]; + // Tell subnodes to exit layout pending state and clear related properties + ASDisplayNodePerformBlockOnEverySubnode(self, ^(ASDisplayNode * _Nonnull node) { + node.hierarchyState &= (~ASHierarchyStateLayoutPending); + }); + } + + ASLayout *previousLayout = _layout; + ASSizeRange previousConstrainedSize = _constrainedSize; + ASLayout *newLayout = [self calculateLayoutThatFits:constrainedSize]; - return [self measureWithSizeRange:constrainedSize completion:^{ - if (!self.isNodeLoaded) { - manageSubnodesBlock(); - } else { - ASPerformBlockOnMainThread(manageSubnodesBlock); + if (ASHierarchyStateIncludesLayoutPending(_hierarchyState)) { + _pendingLayoutContext = [[ASDisplayNodeLayoutContext alloc] initWithNode:self + pendingLayout:newLayout + pendingConstrainedSize:constrainedSize + previousLayout:previousLayout + previousConstrainedSize:previousConstrainedSize]; + } else { + ASDisplayNodeLayoutContext *layoutContext; + if (self.usesImplicitHierarchyManagement) { + layoutContext = [[ASDisplayNodeLayoutContext alloc] initWithNode:self + pendingLayout:newLayout + pendingConstrainedSize:constrainedSize + previousLayout:previousLayout + previousConstrainedSize:previousConstrainedSize]; } - }]; + [self applyLayout:newLayout constrainedSize:constrainedSize layoutContext:layoutContext]; + [self _completeLayoutCalculation]; + } + + return newLayout; } -- (ASLayout *)measureWithSizeRange:(ASSizeRange)constrainedSize completion:(void(^)())completion +- (BOOL)shouldMeasureWithSizeRange:(ASSizeRange)constrainedSize { ASDN::MutexLocker l(_propertyLock); - if (![self __shouldSize]) - return nil; + if (![self __shouldSize]) { + return NO; + } + + if (ASHierarchyStateIncludesLayoutPending(_hierarchyState) && constrainedSize.transitionID != _pendingTransitionID) { + return NO; + } // only calculate the size if // - we haven't already // - the constrained size range is different - if (!_flags.isMeasured || !ASSizeRangeEqualToSizeRange(constrainedSize, _constrainedSize)) { - _previousLayout = _layout; - _layout = [self calculateLayoutThatFits:constrainedSize]; + return (!_flags.isMeasured || !ASSizeRangeEqualToSizeRange(constrainedSize, _constrainedSize)); +} - ASDisplayNodeAssertTrue(_layout.layoutableObject == self); - ASDisplayNodeAssertTrue(_layout.size.width >= 0.0); - ASDisplayNodeAssertTrue(_layout.size.height >= 0.0); - - _previousConstrainedSize = _constrainedSize; - _constrainedSize = constrainedSize; - - if (self.usesImplicitHierarchyManagement) { - [self __calculateSubnodeOperations]; - } - _flags.isMeasured = YES; +- (void)transitionLayoutWithAnimation:(BOOL)animated + shouldMeasureAsync:(BOOL)shouldMeasureAsync + measurementCompletion:(void(^)())completion +{ + ASSizeRange currentConstrainedSize = _constrainedSize; + [self invalidateCalculatedLayout]; + [self transitionLayoutWithSizeRange:currentConstrainedSize + animated:animated + shouldMeasureAsync:shouldMeasureAsync + measurementCompletion:completion]; +} - completion(); +- (void)transitionLayoutWithSizeRange:(ASSizeRange)constrainedSize + animated:(BOOL)animated + shouldMeasureAsync:(BOOL)shouldMeasureAsync + measurementCompletion:(void(^)())completion +{ + ASDisplayNodeAssertMainThread(); + if (! [self shouldMeasureWithSizeRange:constrainedSize]) { + return; + } + + { + ASDN::MutexLocker l(_propertyLock); + ASDisplayNodeAssert(ASHierarchyStateIncludesLayoutPending(_hierarchyState) == NO, @"Can't start a transition when one of the supernodes is performing one."); } - return _layout; -} + int32_t transitionID = [self _newTransitionID]; + constrainedSize.transitionID = transitionID; -- (ASLayout *)transitionLayoutWithAnimation:(BOOL)animated -{ - [self invalidateCalculatedLayout]; - return [self transitionLayoutWithSizeRange:_constrainedSize animated:animated]; -} - -- (ASLayout *)transitionLayoutWithSizeRange:(ASSizeRange)constrainedSize animated:(BOOL)animated -{ - BOOL disableImplicitHierarchyManagement = self.usesImplicitHierarchyManagement == NO; - self.usesImplicitHierarchyManagement = YES; // Temporary flag for 1.9.x + ASDisplayNodePerformBlockOnEverySubnode(self, ^(ASDisplayNode * _Nonnull node) { + ASDisplayNodeAssert([node _hasTransitionsInProgress] == NO, @"Can't start a transition when one of the subnodes is performing one."); + node.hierarchyState |= ASHierarchyStateLayoutPending; + node.pendingTransitionID = transitionID; + }); - return [self measureWithSizeRange:constrainedSize completion:^{ - if (disableImplicitHierarchyManagement) { - self.usesImplicitHierarchyManagement = NO; // Temporary flag for 1.9.x - } - - ASPerformBlockOnMainThread(^{ + void (^transitionBlock)() = ^{ + ASLayout *newLayout; + { ASDN::MutexLocker l(_propertyLock); - _transitionContext = [[_ASTransitionContext alloc] initWithAnimation:animated delegate:self]; - [self __implicitlyInsertSubnodes]; + BOOL disableImplicitHierarchyManagement = self.usesImplicitHierarchyManagement == NO; + self.usesImplicitHierarchyManagement = YES; // Temporary flag for 1.9.x + newLayout = [self calculateLayoutThatFits:constrainedSize]; + if (disableImplicitHierarchyManagement) { + self.usesImplicitHierarchyManagement = NO; // Temporary flag for 1.9.x + } + } + + if ([self _shouldAbortTransitionWithID:transitionID]) { + return; + } + + ASPerformBlockOnMainThread(^{ + if ([self _shouldAbortTransitionWithID:transitionID]) { + return; + } + + ASDN::MutexLocker l(_propertyLock); + + ASLayout *previousLayout = _layout; + ASSizeRange previousConstrainedSize = _constrainedSize; + [self applyLayout:newLayout constrainedSize:constrainedSize layoutContext:nil]; + + [self _invalidateTransitionSentinel]; + + ASDisplayNodePerformBlockOnEverySubnode(self, ^(ASDisplayNode * _Nonnull node) { + [node applyPendingLayoutContext]; + [node _completeLayoutCalculation]; + node.hierarchyState &= (~ASHierarchyStateLayoutPending); + }); + + if (completion) { + completion(); + } + + _pendingLayoutContext = [[ASDisplayNodeLayoutContext alloc] initWithNode:self + pendingLayout:newLayout + pendingConstrainedSize:constrainedSize + previousLayout:previousLayout + previousConstrainedSize:previousConstrainedSize]; + [_pendingLayoutContext applySubnodeInsertions]; + + _transitionContext = [[_ASTransitionContext alloc] initWithAnimation:animated + layoutDelegate:_pendingLayoutContext + completionDelegate:self]; [self animateLayoutTransition:_transitionContext]; }); - }]; -} + }; -- (void)__calculateSubnodeOperations -{ - ASDN::MutexLocker l(_propertyLock); - if (_previousLayout) { - NSIndexSet *insertions, *deletions; - [_previousLayout.immediateSublayouts asdk_diffWithArray:_layout.immediateSublayouts - insertions:&insertions - deletions:&deletions - compareBlock:^BOOL(ASLayout *lhs, ASLayout *rhs) { - return ASObjectIsEqual(lhs.layoutableObject, rhs.layoutableObject); - }]; - filterNodesInLayoutAtIndexes(_layout, insertions, &_insertedSubnodes, &_insertedSubnodePositions); - filterNodesInLayoutAtIndexesWithIntersectingNodes(_previousLayout, - deletions, - _insertedSubnodes, - &_removedSubnodes, - &_removedSubnodePositions); + if (shouldMeasureAsync) { + ASPerformBlockOnBackgroundThread(transitionBlock); } else { - NSIndexSet *indexes = [NSIndexSet indexSetWithIndexesInRange:NSMakeRange(0, [_layout.immediateSublayouts count])]; - filterNodesInLayoutAtIndexes(_layout, indexes, &_insertedSubnodes, &_insertedSubnodePositions); - _removedSubnodes = nil; + transitionBlock(); } } -- (void)__completeLayoutCalculation +- (void)_completeLayoutCalculation { ASDN::MutexLocker l(_propertyLock); - _insertedSubnodes = nil; - _removedSubnodes = nil; - _previousLayout = nil; - [self calculatedLayoutDidChange]; // we generate placeholders at measureWithSizeRange: time so that a node is guaranteed @@ -704,53 +756,6 @@ static ASDisplayNodeMethodOverrides GetASDisplayNodeMethodOverrides(Class c) } } -/** - * @abstract Stores the nodes at the given indexes in the `storedNodes` array, storing indexes in a `storedPositions` c++ vector. - */ -static inline void filterNodesInLayoutAtIndexes( - ASLayout *layout, - NSIndexSet *indexes, - NSArray * __strong *storedNodes, - std::vector *storedPositions - ) -{ - filterNodesInLayoutAtIndexesWithIntersectingNodes(layout, indexes, nil, storedNodes, storedPositions); -} - -/** - * @abstract Stores the nodes at the given indexes in the `storedNodes` array, storing indexes in a `storedPositions` c++ vector. - * @discussion If the node exists in the `intersectingNodes` array, the node is not added to `storedNodes`. - */ -static inline void filterNodesInLayoutAtIndexesWithIntersectingNodes( - ASLayout *layout, - NSIndexSet *indexes, - NSArray *intersectingNodes, - NSArray * __strong *storedNodes, - std::vector *storedPositions - ) -{ - NSMutableArray *nodes = [NSMutableArray array]; - std::vector positions = std::vector(); - NSInteger idx = [indexes firstIndex]; - while (idx != NSNotFound) { - BOOL skip = NO; - ASDisplayNode *node = (ASDisplayNode *)layout.immediateSublayouts[idx].layoutableObject; - ASDisplayNodeCAssert(node, @"A flattened layout must consist exclusively of node sublayouts"); - for (ASDisplayNode *i in intersectingNodes) { - if (node == i) { - skip = YES; - break; - } - } - if (!skip) { - [nodes addObject:node]; - positions.push_back(idx); - } - idx = [indexes indexGreaterThanIndex:idx]; - } - *storedNodes = nodes; - *storedPositions = positions; -} - (void)calculatedLayoutDidChange { @@ -779,66 +784,12 @@ static inline void filterNodesInLayoutAtIndexesWithIntersectingNodes( - (void)didCompleteLayoutTransition:(id)context { - [self __implicitlyRemoveSubnodes]; - [self __completeLayoutCalculation]; + [_pendingLayoutContext applySubnodeRemovals]; + [self _completeLayoutCalculation]; + _pendingLayoutContext = nil; } -#pragma mark - Implicit node hierarchy managagment - -- (void)__implicitlyInsertSubnodes -{ - ASDN::MutexLocker l(_propertyLock); - for (NSInteger i = 0; i < [_insertedSubnodes count]; i++) { - NSInteger p = _insertedSubnodePositions[i]; - [self insertSubnode:_insertedSubnodes[i] atIndex:p]; - } -} - -- (void)__implicitlyRemoveSubnodes -{ - ASDN::MutexLocker l(_propertyLock); - for (NSInteger i = 0; i < [_removedSubnodes count]; i++) { - [_removedSubnodes[i] removeFromSupernode]; - } -} - -#pragma mark - _ASTransitionContextDelegate - -- (NSArray *)currentSubnodesWithTransitionContext:(_ASTransitionContext *)context -{ - return _subnodes; -} - -- (NSArray *)insertedSubnodesWithTransitionContext:(_ASTransitionContext *)context -{ - return _insertedSubnodes; -} - -- (NSArray *)removedSubnodesWithTransitionContext:(_ASTransitionContext *)context -{ - return _removedSubnodes; -} - -- (ASLayout *)transitionContext:(_ASTransitionContext *)context layoutForKey:(NSString *)key -{ - if ([key isEqualToString:ASTransitionContextFromLayoutKey]) { - return _previousLayout; - } else if ([key isEqualToString:ASTransitionContextToLayoutKey]) { - return _layout; - } else { - return nil; - } -} -- (ASSizeRange)transitionContext:(_ASTransitionContext *)context constrainedSizeForKey:(NSString *)key -{ - if ([key isEqualToString:ASTransitionContextFromLayoutKey]) { - return _previousConstrainedSize; - } else if ([key isEqualToString:ASTransitionContextToLayoutKey]) { - return _constrainedSize; - } else { - return ASSizeRangeMake(CGSizeZero, CGSizeZero); - } -} +#pragma mark - _ASTransitionContextCompletionDelegate - (void)transitionContext:(_ASTransitionContext *)context didComplete:(BOOL)didComplete { @@ -1896,6 +1847,13 @@ void recursivelyTriggerDisplayForLayer(CALayer *layer, BOOL shouldBlock) return _constrainedSize; } +- (void)setPendingTransitionID:(int32_t)pendingTransitionID +{ + ASDN::MutexLocker l(_propertyLock); + ASDisplayNodeAssertTrue(_pendingTransitionID < pendingTransitionID); + _pendingTransitionID = pendingTransitionID; +} + - (void)setPreferredFrameSize:(CGSize)preferredFrameSize { ASDN::MutexLocker l(_propertyLock); @@ -2211,6 +2169,19 @@ void recursivelyTriggerDisplayForLayer(CALayer *layer, BOOL shouldBlock) } } + if ((newState & ASHierarchyStateLayoutPending) != (oldState & ASHierarchyStateLayoutPending)) { + if (newState & ASHierarchyStateLayoutPending) { + // Entering layout pending state + } else { + // Leaving layout pending state, reset related properties + { + ASDN::MutexLocker l(_propertyLock); + _pendingTransitionID = 0; + _pendingLayoutContext = nil; + } + } + } + if (newState != oldState) { LOG(@"setHierarchyState: oldState = %lu, newState = %lu", (unsigned long)oldState, (unsigned long)newState); } @@ -2236,6 +2207,37 @@ void recursivelyTriggerDisplayForLayer(CALayer *layer, BOOL shouldBlock) }); } +- (void)applyPendingLayoutContext +{ + ASDN::MutexLocker l(_propertyLock); + if (_pendingLayoutContext) { + [self applyLayout:_pendingLayoutContext.pendingLayout + constrainedSize:_pendingLayoutContext.pendingConstrainedSize + layoutContext:_pendingLayoutContext]; + _pendingLayoutContext = nil; + } +} + +- (void)applyLayout:(ASLayout *)layout + constrainedSize:(ASSizeRange)constrainedSize + layoutContext:(ASDisplayNodeLayoutContext *)layoutContext +{ + ASDN::MutexLocker l(_propertyLock); + _layout = layout; + + ASDisplayNodeAssertTrue(layout.layoutableObject == self); + ASDisplayNodeAssertTrue(layout.size.width >= 0.0); + ASDisplayNodeAssertTrue(layout.size.height >= 0.0); + + _constrainedSize = constrainedSize; + _flags.isMeasured = YES; + + if (self.usesImplicitHierarchyManagement && layoutContext != nil) { + [layoutContext applySubnodeInsertions]; + [layoutContext applySubnodeRemovals]; + } +} + - (void)layout { ASDisplayNodeAssertMainThread(); @@ -2552,24 +2554,31 @@ static const char *ASDisplayNodeDrawingPriorityKey = "ASDrawingPriority"; return asyncSizingQueue; } -- (BOOL)_isMarkedForReplacement +- (BOOL)_hasTransitionsInProgress { ASDN::MutexLocker l(_propertyLock); - - return _replaceAsyncSentinel != nil; + return _transitionSentinel != nil; } -// FIXME: This method doesn't appear to be called, and could be removed. -// However, it may be useful for an API similar to what Paper used to create a new node hierarchy, -// trigger asynchronous measurement and display on it, and have it swap out and replace an old hierarchy. -- (ASSentinel *)_asyncReplaceSentinel +- (void)_invalidateTransitionSentinel { ASDN::MutexLocker l(_propertyLock); + _transitionSentinel = nil; +} - if (!_replaceAsyncSentinel) { - _replaceAsyncSentinel = [[ASSentinel alloc] init]; +- (BOOL)_shouldAbortTransitionWithID:(int32_t)transitionID +{ + ASDN::MutexLocker l(_propertyLock); + return _transitionSentinel == nil || transitionID != _transitionSentinel.value; +} + +- (int32_t)_newTransitionID +{ + ASDN::MutexLocker l(_propertyLock); + if (!_transitionSentinel) { + _transitionSentinel = [[ASSentinel alloc] init]; } - return _replaceAsyncSentinel; + return [_transitionSentinel increment]; } // Calls completion with nil to indicated cancellation diff --git a/AsyncDisplayKit/Layout/ASDimension.h b/AsyncDisplayKit/Layout/ASDimension.h index c96b2155ae..462bdfc7f9 100644 --- a/AsyncDisplayKit/Layout/ASDimension.h +++ b/AsyncDisplayKit/Layout/ASDimension.h @@ -30,6 +30,7 @@ typedef struct { typedef struct { CGSize min; CGSize max; + int32_t transitionID; } ASSizeRange; extern ASRelativeDimension const ASRelativeDimensionUnconstrained; diff --git a/AsyncDisplayKit/Private/ASDisplayNode+AsyncDisplay.mm b/AsyncDisplayKit/Private/ASDisplayNode+AsyncDisplay.mm index aa0982b77a..98e8a2bc68 100644 --- a/AsyncDisplayKit/Private/ASDisplayNode+AsyncDisplay.mm +++ b/AsyncDisplayKit/Private/ASDisplayNode+AsyncDisplay.mm @@ -331,7 +331,7 @@ static void __ASDisplayLayerDecrementConcurrentDisplayCount(BOOL displayIsAsync, // FIXME: what about the degenerate case where we are calling setNeedsDisplay faster than the jobs are dequeuing // from the displayQueue? Need to not cancel early fails from displaySentinel changes. ASSentinel *displaySentinel = (asynchronously ? _displaySentinel : nil); - int64_t displaySentinelValue = [displaySentinel increment]; + int32_t displaySentinelValue = [displaySentinel increment]; asdisplaynode_iscancelled_block_t isCancelledBlock = ^{ return BOOL(displaySentinelValue != displaySentinel.value); diff --git a/AsyncDisplayKit/Private/ASDisplayNode+FrameworkPrivate.h b/AsyncDisplayKit/Private/ASDisplayNode+FrameworkPrivate.h index 4fdaa8632a..79f2b2e273 100644 --- a/AsyncDisplayKit/Private/ASDisplayNode+FrameworkPrivate.h +++ b/AsyncDisplayKit/Private/ASDisplayNode+FrameworkPrivate.h @@ -45,9 +45,17 @@ typedef NS_OPTIONS(NSUInteger, ASHierarchyState) ASHierarchyStateRangeManaged = 1 << 1, /** Down-propogated version of _flags.visibilityNotificationsDisabled. This flag is very rarely set, but by having it locally available to nodes, they do not have to walk up supernodes at the critical points it is checked. */ - ASHierarchyStateTransitioningSupernodes = 1 << 2 + ASHierarchyStateTransitioningSupernodes = 1 << 2, + /** One of the supernodes of this node is performing a transition. + Any layout calculated during this state should not be applied immediately, but pending until later. */ + ASHierarchyStateLayoutPending = 1 << 3 }; +inline BOOL ASHierarchyStateIncludesLayoutPending(ASHierarchyState hierarchyState) +{ + return ((hierarchyState & ASHierarchyStateLayoutPending) == ASHierarchyStateLayoutPending); +} + @interface ASDisplayNode () { @protected diff --git a/AsyncDisplayKit/Private/ASDisplayNodeInternal.h b/AsyncDisplayKit/Private/ASDisplayNodeInternal.h index c63b89582b..e491aac0c6 100644 --- a/AsyncDisplayKit/Private/ASDisplayNodeInternal.h +++ b/AsyncDisplayKit/Private/ASDisplayNodeInternal.h @@ -18,12 +18,14 @@ #import "ASThread.h" #import "ASLayoutOptions.h" #import "_ASTransitionContext.h" +#import "ASDisplayNodeLayoutContext.h" #include @protocol _ASDisplayLayerDelegate; @class _ASDisplayLayer; @class _ASPendingState; +@class ASSentinel; BOOL ASDisplayNodeSubclassOverridesSelector(Class subclass, SEL selector); @@ -89,15 +91,13 @@ FOUNDATION_EXPORT NSString * const ASRenderingEngineDidDisplayNodesScheduledBefo ASDisplayNode * __weak _supernode; ASSentinel *_displaySentinel; - ASSentinel *_replaceAsyncSentinel; + ASSentinel *_transitionSentinel; // This is the desired contentsScale, not the scale at which the layer's contents should be displayed CGFloat _contentsScaleForDisplay; - ASLayout *_previousLayout; ASLayout *_layout; - ASSizeRange _previousConstrainedSize; ASSizeRange _constrainedSize; UIEdgeInsets _hitTestSlop; @@ -107,11 +107,9 @@ FOUNDATION_EXPORT NSString * const ASRenderingEngineDidDisplayNodesScheduledBefo _ASTransitionContext *_transitionContext; BOOL _usesImplicitHierarchyManagement; - NSArray *_insertedSubnodes; - NSArray *_removedSubnodes; - std::vector _insertedSubnodePositions; - std::vector _removedSubnodePositions; - + int32_t _pendingTransitionID; + ASDisplayNodeLayoutContext *_pendingLayoutContext; + ASDisplayNodeViewBlock _viewBlock; ASDisplayNodeLayerBlock _layerBlock; ASDisplayNodeDidLoadBlock _nodeLoadedBlock; diff --git a/AsyncDisplayKit/Private/ASDisplayNodeLayoutContext.h b/AsyncDisplayKit/Private/ASDisplayNodeLayoutContext.h new file mode 100644 index 0000000000..2c5530cab2 --- /dev/null +++ b/AsyncDisplayKit/Private/ASDisplayNodeLayoutContext.h @@ -0,0 +1,33 @@ +// +// ASDisplayNodeLayoutContext.h +// AsyncDisplayKit +// +// Created by Huy Nguyen on 3/8/16. +// Copyright © 2016 Facebook. All rights reserved. +// + +#import "ASDimension.h" +#import "_ASTransitionContext.h" + +@class ASDisplayNode; +@class ASLayout; + +@interface ASDisplayNodeLayoutContext : NSObject <_ASTransitionContextLayoutDelegate> + +@property (nonatomic, readonly, weak) ASDisplayNode *node; +@property (nonatomic, readonly, strong) ASLayout *pendingLayout; +@property (nonatomic, readonly, assign) ASSizeRange pendingConstrainedSize; +@property (nonatomic, readonly, strong) ASLayout *previousLayout; +@property (nonatomic, readonly, assign) ASSizeRange previousConstrainedSize; + +- (instancetype)initWithNode:(ASDisplayNode *)node + pendingLayout:(ASLayout *)pendingLayout + pendingConstrainedSize:(ASSizeRange)pendingConstrainedSize + previousLayout:(ASLayout *)previousLayout + previousConstrainedSize:(ASSizeRange)previousConstrainedSize; + +- (void)applySubnodeInsertions; + +- (void)applySubnodeRemovals; + +@end diff --git a/AsyncDisplayKit/Private/ASDisplayNodeLayoutContext.mm b/AsyncDisplayKit/Private/ASDisplayNodeLayoutContext.mm new file mode 100644 index 0000000000..446bdb2439 --- /dev/null +++ b/AsyncDisplayKit/Private/ASDisplayNodeLayoutContext.mm @@ -0,0 +1,190 @@ +// +// ASDisplayNodeLayoutContext.mm +// AsyncDisplayKit +// +// Created by Huy Nguyen on 3/8/16. +// Copyright © 2016 Facebook. All rights reserved. +// + +#import "ASDisplayNodeLayoutContext.h" + +#import "ASDisplayNode.h" +#import "ASDisplayNodeInternal.h" +#import "ASDisplayNode+Subclasses.h" +#import "ASLayout.h" + +#import + +#import "NSArray+Diffing.h" +#import "ASEqualityHelpers.h" + +@implementation ASDisplayNodeLayoutContext { + ASDN::RecursiveMutex _propertyLock; + BOOL _calculatedSubnodeOperations; + NSArray *_insertedSubnodes; + NSArray *_removedSubnodes; + std::vector _insertedSubnodePositions; + std::vector _removedSubnodePositions; +} + +- (instancetype)initWithNode:(ASDisplayNode *)node + pendingLayout:(ASLayout *)pendingLayout + pendingConstrainedSize:(ASSizeRange)pendingConstrainedSize + previousLayout:(ASLayout *)previousLayout + previousConstrainedSize:(ASSizeRange)previousConstrainedSize +{ + self = [super init]; + if (self) { + _node = node; + _pendingLayout = pendingLayout; + _pendingConstrainedSize = pendingConstrainedSize; + _previousLayout = previousLayout; + _previousConstrainedSize = previousConstrainedSize; + } + return self; +} + +- (void)applySubnodeInsertions +{ + ASDN::MutexLocker l(_propertyLock); + [self calculateSubnodeOperationsIfNeeded]; + for (NSInteger i = 0; i < [_insertedSubnodes count]; i++) { + NSInteger p = _insertedSubnodePositions[i]; + [_node insertSubnode:_insertedSubnodes[i] atIndex:p]; + } +} + +- (void)applySubnodeRemovals +{ + ASDN::MutexLocker l(_propertyLock); + [self calculateSubnodeOperationsIfNeeded]; + for (NSInteger i = 0; i < [_removedSubnodes count]; i++) { + [_removedSubnodes[i] removeFromSupernode]; + } +} + +- (void)calculateSubnodeOperationsIfNeeded +{ + ASDN::MutexLocker l(_propertyLock); + if (_calculatedSubnodeOperations) { + return; + } + if (_previousLayout) { + NSIndexSet *insertions, *deletions; + [_previousLayout.immediateSublayouts asdk_diffWithArray:_pendingLayout.immediateSublayouts + insertions:&insertions + deletions:&deletions + compareBlock:^BOOL(ASLayout *lhs, ASLayout *rhs) { + return ASObjectIsEqual(lhs.layoutableObject, rhs.layoutableObject); + }]; + filterNodesInLayoutAtIndexes(_pendingLayout, insertions, &_insertedSubnodes, &_insertedSubnodePositions); + filterNodesInLayoutAtIndexesWithIntersectingNodes(_previousLayout, + deletions, + _insertedSubnodes, + &_removedSubnodes, + &_removedSubnodePositions); + } else { + NSIndexSet *indexes = [NSIndexSet indexSetWithIndexesInRange:NSMakeRange(0, [_pendingLayout.immediateSublayouts count])]; + filterNodesInLayoutAtIndexes(_pendingLayout, indexes, &_insertedSubnodes, &_insertedSubnodePositions); + _removedSubnodes = nil; + } + _calculatedSubnodeOperations = YES; +} + +#pragma mark - _ASTransitionContextDelegate + +- (NSArray *)currentSubnodesWithTransitionContext:(_ASTransitionContext *)context +{ + ASDN::MutexLocker l(_propertyLock); + return _node.subnodes; +} + +- (NSArray *)insertedSubnodesWithTransitionContext:(_ASTransitionContext *)context +{ + ASDN::MutexLocker l(_propertyLock); + [self calculateSubnodeOperationsIfNeeded]; + return _insertedSubnodes; +} + +- (NSArray *)removedSubnodesWithTransitionContext:(_ASTransitionContext *)context +{ + ASDN::MutexLocker l(_propertyLock); + [self calculateSubnodeOperationsIfNeeded]; + return _removedSubnodes; +} + +- (ASLayout *)transitionContext:(_ASTransitionContext *)context layoutForKey:(NSString *)key +{ + ASDN::MutexLocker l(_propertyLock); + if ([key isEqualToString:ASTransitionContextFromLayoutKey]) { + return _previousLayout; + } else if ([key isEqualToString:ASTransitionContextToLayoutKey]) { + return _pendingLayout; + } else { + return nil; + } +} + +- (ASSizeRange)transitionContext:(_ASTransitionContext *)context constrainedSizeForKey:(NSString *)key +{ + ASDN::MutexLocker l(_propertyLock); + if ([key isEqualToString:ASTransitionContextFromLayoutKey]) { + return _previousConstrainedSize; + } else if ([key isEqualToString:ASTransitionContextToLayoutKey]) { + return _pendingConstrainedSize; + } else { + return ASSizeRangeMake(CGSizeZero, CGSizeZero); + } +} + +#pragma mark - Filter helpers + +/** + * @abstract Stores the nodes at the given indexes in the `storedNodes` array, storing indexes in a `storedPositions` c++ vector. + */ +static inline void filterNodesInLayoutAtIndexes( + ASLayout *layout, + NSIndexSet *indexes, + NSArray * __strong *storedNodes, + std::vector *storedPositions + ) +{ + filterNodesInLayoutAtIndexesWithIntersectingNodes(layout, indexes, nil, storedNodes, storedPositions); +} + +/** + * @abstract Stores the nodes at the given indexes in the `storedNodes` array, storing indexes in a `storedPositions` c++ vector. + * @discussion If the node exists in the `intersectingNodes` array, the node is not added to `storedNodes`. + */ +static inline void filterNodesInLayoutAtIndexesWithIntersectingNodes( + ASLayout *layout, + NSIndexSet *indexes, + NSArray *intersectingNodes, + NSArray * __strong *storedNodes, + std::vector *storedPositions + ) +{ + NSMutableArray *nodes = [NSMutableArray array]; + std::vector positions = std::vector(); + NSInteger idx = [indexes firstIndex]; + while (idx != NSNotFound) { + BOOL skip = NO; + ASDisplayNode *node = (ASDisplayNode *)layout.immediateSublayouts[idx].layoutableObject; + ASDisplayNodeCAssert(node, @"A flattened layout must consist exclusively of node sublayouts"); + for (ASDisplayNode *i in intersectingNodes) { + if (node == i) { + skip = YES; + break; + } + } + if (!skip) { + [nodes addObject:node]; + positions.push_back(idx); + } + idx = [indexes indexGreaterThanIndex:idx]; + } + *storedNodes = nodes; + *storedPositions = positions; +} + +@end diff --git a/AsyncDisplayKit/_ASTransitionContext.h b/AsyncDisplayKit/_ASTransitionContext.h index 9411d76ac1..c9d4cd7ff9 100644 --- a/AsyncDisplayKit/_ASTransitionContext.h +++ b/AsyncDisplayKit/_ASTransitionContext.h @@ -13,7 +13,7 @@ @class ASLayout; @class _ASTransitionContext; -@protocol _ASTransitionContextDelegate +@protocol _ASTransitionContextLayoutDelegate - (NSArray *)currentSubnodesWithTransitionContext:(_ASTransitionContext *)context; @@ -23,6 +23,10 @@ - (ASLayout *)transitionContext:(_ASTransitionContext *)context layoutForKey:(NSString *)key; - (ASSizeRange)transitionContext:(_ASTransitionContext *)context constrainedSizeForKey:(NSString *)key; +@end + +@protocol _ASTransitionContextCompletionDelegate + - (void)transitionContext:(_ASTransitionContext *)context didComplete:(BOOL)didComplete; @end @@ -31,6 +35,8 @@ @property (assign, readonly, nonatomic, getter=isAnimated) BOOL animated; -- (instancetype)initWithAnimation:(BOOL)animated delegate:(id<_ASTransitionContextDelegate>)delegate; +- (instancetype)initWithAnimation:(BOOL)animated + layoutDelegate:(id<_ASTransitionContextLayoutDelegate>)layoutDelegate + completionDelegate:(id<_ASTransitionContextCompletionDelegate>)completionDelegate; @end diff --git a/AsyncDisplayKit/_ASTransitionContext.m b/AsyncDisplayKit/_ASTransitionContext.m index 8c69f194fa..2474f3f395 100644 --- a/AsyncDisplayKit/_ASTransitionContext.m +++ b/AsyncDisplayKit/_ASTransitionContext.m @@ -16,18 +16,22 @@ NSString * const ASTransitionContextToLayoutKey = @"org.asyncdisplaykit.ASTransi @interface _ASTransitionContext () -@property (weak, nonatomic) id<_ASTransitionContextDelegate> delegate; +@property (weak, nonatomic) id<_ASTransitionContextLayoutDelegate> layoutDelegate; +@property (weak, nonatomic) id<_ASTransitionContextCompletionDelegate> completionDelegate; @end @implementation _ASTransitionContext -- (instancetype)initWithAnimation:(BOOL)animated delegate:(id<_ASTransitionContextDelegate>)delegate +- (instancetype)initWithAnimation:(BOOL)animated + layoutDelegate:(id<_ASTransitionContextLayoutDelegate>)layoutDelegate + completionDelegate:(id<_ASTransitionContextCompletionDelegate>)completionDelegate { self = [super init]; if (self) { _animated = animated; - _delegate = delegate; + _layoutDelegate = layoutDelegate; + _completionDelegate = completionDelegate; } return self; } @@ -36,17 +40,17 @@ NSString * const ASTransitionContextToLayoutKey = @"org.asyncdisplaykit.ASTransi - (ASLayout *)layoutForKey:(NSString *)key { - return [_delegate transitionContext:self layoutForKey:key]; + return [_layoutDelegate transitionContext:self layoutForKey:key]; } - (ASSizeRange)constrainedSizeForKey:(NSString *)key { - return [_delegate transitionContext:self constrainedSizeForKey:key]; + return [_layoutDelegate transitionContext:self constrainedSizeForKey:key]; } - (CGRect)initialFrameForNode:(ASDisplayNode *)node { - for (ASDisplayNode *subnode in [_delegate currentSubnodesWithTransitionContext:self]) { + for (ASDisplayNode *subnode in [_layoutDelegate currentSubnodesWithTransitionContext:self]) { if (node == subnode) { return node.frame; } @@ -75,17 +79,17 @@ NSString * const ASTransitionContextToLayoutKey = @"org.asyncdisplaykit.ASTransi - (NSArray *)insertedSubnodes { - return [_delegate insertedSubnodesWithTransitionContext:self]; + return [_layoutDelegate insertedSubnodesWithTransitionContext:self]; } - (NSArray *)removedSubnodes { - return [_delegate removedSubnodesWithTransitionContext:self]; + return [_layoutDelegate removedSubnodesWithTransitionContext:self]; } - (void)completeTransition:(BOOL)didComplete { - [_delegate transitionContext:self didComplete:didComplete]; + [_completionDelegate transitionContext:self didComplete:didComplete]; } @end From af61645fafc4e0e0ae2df90d3edca4f4795a1812 Mon Sep 17 00:00:00 2001 From: Garrett Moon Date: Fri, 11 Mar 2016 15:51:17 -0800 Subject: [PATCH 15/26] Add support for attempting to get image synchronously --- AsyncDisplayKit/ASNetworkImageNode.mm | 15 +++++++++++++++ AsyncDisplayKit/Details/ASImageProtocols.h | 13 +++++++++++++ .../Details/ASPINRemoteImageDownloader.m | 7 +++++++ 3 files changed, 35 insertions(+) diff --git a/AsyncDisplayKit/ASNetworkImageNode.mm b/AsyncDisplayKit/ASNetworkImageNode.mm index e1b2210760..c7cadff233 100755 --- a/AsyncDisplayKit/ASNetworkImageNode.mm +++ b/AsyncDisplayKit/ASNetworkImageNode.mm @@ -49,6 +49,7 @@ static const CGSize kMinReleaseImageOnBackgroundSize = {20.0, 20.0}; BOOL _cacheSupportsNewProtocol; BOOL _cacheSupportsClearing; + BOOL _cacheSupportsSynchronousFetch; } @end @@ -73,6 +74,7 @@ static const CGSize kMinReleaseImageOnBackgroundSize = {20.0, 20.0}; _cacheSupportsNewProtocol = [cache respondsToSelector:@selector(cachedImageWithURL:callbackQueue:completion:)]; _cacheSupportsClearing = [cache respondsToSelector:@selector(clearFetchedImageFromCacheWithURL:)]; + _cacheSupportsSynchronousFetch = [cache respondsToSelector:@selector(synchronouslyFetchedCachedImageWithURL:)]; _shouldCacheImage = YES; self.shouldBypassEnsureDisplay = YES; @@ -169,6 +171,19 @@ static const CGSize kMinReleaseImageOnBackgroundSize = {20.0, 20.0}; - (void)displayWillStart { [super displayWillStart]; + + if (_cacheSupportsSynchronousFetch) { + { + ASDN::MutexLocker l(_lock); + if (_URL && _downloadIdentifier == nil) { + UIImage *result = [_cache synchronouslyFetchedCachedImageWithURL:_URL]; + if (result) { + self.image = result; + _imageLoaded = YES; + } + } + } + } [self fetchData]; diff --git a/AsyncDisplayKit/Details/ASImageProtocols.h b/AsyncDisplayKit/Details/ASImageProtocols.h index ddbccb0eb1..365c374a00 100644 --- a/AsyncDisplayKit/Details/ASImageProtocols.h +++ b/AsyncDisplayKit/Details/ASImageProtocols.h @@ -17,6 +17,19 @@ typedef void(^ASImageCacherCompletion)(UIImage * _Nullable imageFromCache); @optional +/** + @abstract Attempts to fetch an image with the given URL from a memory cache. + @param URL The URL of the image to retrieve from the cache. + @discussion This method exists to support synchronous rendering of nodes. Before the layer is drawn, this method + is called to attempt to get the image out of the cache synchronously. This allows drawing to occur on the main thread + if displaysAsynchronously is set to NO or recursivelyEnsureDisplaySynchronously: has been called. + + If `URL` is nil, `completion` will be invoked immediately with a nil image. This method *should* block + the calling thread to fetch the image from a fast memory cache. It is OK to return nil from this method and instead + support only cachedImageWithURL:callbackQueue:completion: however, synchronous rendering will not be possible. + */ +- (_Nullable UIImage *)synchronouslyFetchedCachedImageWithURL:(NSURL *)URL; + /** @abstract Attempts to fetch an image with the given URL from the cache. @param URL The URL of the image to retrieve from the cache. diff --git a/AsyncDisplayKit/Details/ASPINRemoteImageDownloader.m b/AsyncDisplayKit/Details/ASPINRemoteImageDownloader.m index dc15fb98ec..13f304f76e 100644 --- a/AsyncDisplayKit/Details/ASPINRemoteImageDownloader.m +++ b/AsyncDisplayKit/Details/ASPINRemoteImageDownloader.m @@ -29,6 +29,13 @@ #pragma mark ASImageProtocols +- (UIImage *)synchronouslyFetchedCachedImageWithURL:(NSURL *)URL +{ + NSString *key = [[PINRemoteImageManager sharedImageManager] cacheKeyForURL:URL processorKey:nil]; + PINRemoteImageManagerResult *result = [[PINRemoteImageManager sharedImageManager] synchronousImageFromCacheWithCacheKey:key options:PINRemoteImageManagerDownloadOptionsSkipDecode]; + return result.image; +} + - (void)fetchCachedImageWithURL:(NSURL *)URL callbackQueue:(dispatch_queue_t)callbackQueue completion:(void (^)(CGImageRef imageFromCache))completion From 53f6dadbbb3e6768c837cf68fee3e1afd5dd5423 Mon Sep 17 00:00:00 2001 From: Garrett Moon Date: Fri, 11 Mar 2016 16:34:49 -0800 Subject: [PATCH 16/26] Should be nullable not _Nullable --- AsyncDisplayKit/Details/ASImageProtocols.h | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/AsyncDisplayKit/Details/ASImageProtocols.h b/AsyncDisplayKit/Details/ASImageProtocols.h index 365c374a00..25ee762862 100644 --- a/AsyncDisplayKit/Details/ASImageProtocols.h +++ b/AsyncDisplayKit/Details/ASImageProtocols.h @@ -28,7 +28,7 @@ typedef void(^ASImageCacherCompletion)(UIImage * _Nullable imageFromCache); the calling thread to fetch the image from a fast memory cache. It is OK to return nil from this method and instead support only cachedImageWithURL:callbackQueue:completion: however, synchronous rendering will not be possible. */ -- (_Nullable UIImage *)synchronouslyFetchedCachedImageWithURL:(NSURL *)URL; +- (nullable UIImage *)synchronouslyFetchedCachedImageWithURL:(NSURL *)URL; /** @abstract Attempts to fetch an image with the given URL from the cache. From 3a7d150680ddb389c5fa7bd5b6983515b769406e Mon Sep 17 00:00:00 2001 From: Garrett Moon Date: Fri, 11 Mar 2016 21:23:52 -0800 Subject: [PATCH 17/26] Remove extraneous brackets, check _imageLoaded before doing work --- AsyncDisplayKit/ASNetworkImageNode.mm | 14 ++++++-------- 1 file changed, 6 insertions(+), 8 deletions(-) diff --git a/AsyncDisplayKit/ASNetworkImageNode.mm b/AsyncDisplayKit/ASNetworkImageNode.mm index c7cadff233..5b5f97e793 100755 --- a/AsyncDisplayKit/ASNetworkImageNode.mm +++ b/AsyncDisplayKit/ASNetworkImageNode.mm @@ -173,14 +173,12 @@ static const CGSize kMinReleaseImageOnBackgroundSize = {20.0, 20.0}; [super displayWillStart]; if (_cacheSupportsSynchronousFetch) { - { - ASDN::MutexLocker l(_lock); - if (_URL && _downloadIdentifier == nil) { - UIImage *result = [_cache synchronouslyFetchedCachedImageWithURL:_URL]; - if (result) { - self.image = result; - _imageLoaded = YES; - } + ASDN::MutexLocker l(_lock); + if (_imageLoaded == NO && _URL && _downloadIdentifier == nil) { + UIImage *result = [_cache synchronouslyFetchedCachedImageWithURL:_URL]; + if (result) { + self.image = result; + _imageLoaded = YES; } } } From 72ecdb5e325744f72cdeaa737b1d29978ee9440d Mon Sep 17 00:00:00 2001 From: Garrett Moon Date: Fri, 11 Mar 2016 21:32:21 -0800 Subject: [PATCH 18/26] Requires PINRemoteImage 2.1 --- AsyncDisplayKit.podspec | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/AsyncDisplayKit.podspec b/AsyncDisplayKit.podspec index 8a71f9356f..1516962121 100644 --- a/AsyncDisplayKit.podspec +++ b/AsyncDisplayKit.podspec @@ -51,7 +51,7 @@ Pod::Spec.new do |spec| spec.subspec 'PINRemoteImage' do |pin| pin.xcconfig = { 'GCC_PREPROCESSOR_DEFINITIONS' => '$(inherited) PIN_REMOTE_IMAGE=1' } - pin.dependency 'PINRemoteImage/iOS', '>= 2' + pin.dependency 'PINRemoteImage/iOS', '>= 2.1' pin.dependency 'AsyncDisplayKit/ASDealloc2MainObject' end From d4b724d462793800bbf7b0327307e6ed83f7722c Mon Sep 17 00:00:00 2001 From: Scott Goodson Date: Fri, 11 Mar 2016 22:12:28 -0800 Subject: [PATCH 19/26] [Xcode Project] Update Framework target with new internal file to support asynchronously-measured transition animations. --- AsyncDisplayKit.xcodeproj/project.pbxproj | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/AsyncDisplayKit.xcodeproj/project.pbxproj b/AsyncDisplayKit.xcodeproj/project.pbxproj index ce09ec2e70..91db74aa94 100644 --- a/AsyncDisplayKit.xcodeproj/project.pbxproj +++ b/AsyncDisplayKit.xcodeproj/project.pbxproj @@ -498,6 +498,8 @@ DBDB83971C6E879900D0098C /* ASPagerFlowLayout.m in Sources */ = {isa = PBXBuildFile; fileRef = DBDB83931C6E879900D0098C /* ASPagerFlowLayout.m */; }; DE040EF91C2B40AC004692FF /* ASCollectionViewFlowLayoutInspector.h in Headers */ = {isa = PBXBuildFile; fileRef = 251B8EF41BBB3D690087C538 /* ASCollectionViewFlowLayoutInspector.h */; settings = {ATTRIBUTES = (Public, ); }; }; DE0702FC1C3671E900D7DE62 /* libAsyncDisplayKit.a in Frameworks */ = {isa = PBXBuildFile; fileRef = 058D09AC195D04C000B7D73C /* libAsyncDisplayKit.a */; }; + DE4843DB1C93EAB100A1F33B /* ASDisplayNodeLayoutContext.mm in Sources */ = {isa = PBXBuildFile; fileRef = E52405B21C8FEF03004DC8E7 /* ASDisplayNodeLayoutContext.mm */; }; + DE4843DC1C93EAC100A1F33B /* ASDisplayNodeLayoutContext.h in Headers */ = {isa = PBXBuildFile; fileRef = E52405B41C8FEF16004DC8E7 /* ASDisplayNodeLayoutContext.h */; }; DE6EA3221C14000600183B10 /* ASDisplayNode+FrameworkPrivate.h in Headers */ = {isa = PBXBuildFile; fileRef = DE6EA3211C14000600183B10 /* ASDisplayNode+FrameworkPrivate.h */; }; DE6EA3231C14000600183B10 /* ASDisplayNode+FrameworkPrivate.h in Headers */ = {isa = PBXBuildFile; fileRef = DE6EA3211C14000600183B10 /* ASDisplayNode+FrameworkPrivate.h */; }; DE84918D1C8FFF2B003D89E9 /* ASRunLoopQueue.h in Headers */ = {isa = PBXBuildFile; fileRef = 81EE384D1C8E94F000456208 /* ASRunLoopQueue.h */; settings = {ATTRIBUTES = (Public, ); }; }; @@ -1612,6 +1614,7 @@ E5711A2C1C840C81009619D4 /* ASIndexedNodeContext.h in Headers */, 254C6B7B1BF94DF4003EC431 /* ASTextKitRenderer+Positioning.h in Headers */, CC7FD9E21BB603FF005CCB2B /* ASPhotosFrameworkImageRequest.h in Headers */, + DE4843DC1C93EAC100A1F33B /* ASDisplayNodeLayoutContext.h in Headers */, 254C6B761BF94DF4003EC431 /* ASTextNodeTypes.h in Headers */, 34EFC7711B701CFF00AD841F /* ASStackLayoutSpec.h in Headers */, 2767E9411BB19BD600EA9B77 /* ASViewController.h in Headers */, @@ -1989,6 +1992,7 @@ isa = PBXSourcesBuildPhase; buildActionMask = 2147483647; files = ( + DE4843DB1C93EAB100A1F33B /* ASDisplayNodeLayoutContext.mm in Sources */, B30BF6541C59D889004FCD53 /* ASLayoutManager.m in Sources */, 92DD2FE71BF4D0850074C9DD /* ASMapNode.mm in Sources */, 636EA1A51C7FF4EF00EE152F /* ASDefaultPlayButton.m in Sources */, From f97a509541e99df196bba1d43ede00914ac5ff65 Mon Sep 17 00:00:00 2001 From: Scott Goodson Date: Sat, 12 Mar 2016 16:26:33 -0800 Subject: [PATCH 20/26] [ASRangeController] Ensure that even if the collection view layout is inconsistent, it is impossible for a visible item to not be in the display range. --- AsyncDisplayKit/ASControlNode.h | 5 ----- AsyncDisplayKit/ASDisplayNode.mm | 2 ++ AsyncDisplayKit/AsyncDisplayKit+Debug.h | 14 ++++++++++++-- AsyncDisplayKit/Details/ASRangeController.mm | 16 +++++++++------- 4 files changed, 23 insertions(+), 14 deletions(-) diff --git a/AsyncDisplayKit/ASControlNode.h b/AsyncDisplayKit/ASControlNode.h index 886855f008..9f5c37e343 100644 --- a/AsyncDisplayKit/ASControlNode.h +++ b/AsyncDisplayKit/ASControlNode.h @@ -120,11 +120,6 @@ typedef NS_OPTIONS(NSUInteger, ASControlState) { */ - (void)sendActionsForControlEvents:(ASControlNodeEvent)controlEvents withEvent:(nullable UIEvent *)event; -/** - Class method to enable a visualization overlay of the tapable area on the ASControlNode. For app debugging purposes only. - @param enable Specify YES to make this debug feature enabled when messaging the ASControlNode class. - */ -+ (void)setEnableHitTestDebug:(BOOL)enable; @end NS_ASSUME_NONNULL_END diff --git a/AsyncDisplayKit/ASDisplayNode.mm b/AsyncDisplayKit/ASDisplayNode.mm index c9615282ea..d16479a5d5 100644 --- a/AsyncDisplayKit/ASDisplayNode.mm +++ b/AsyncDisplayKit/ASDisplayNode.mm @@ -2003,6 +2003,8 @@ void recursivelyTriggerDisplayForLayer(CALayer *layer, BOOL shouldBlock) - (void)setInterfaceState:(ASInterfaceState)newState { + // It should never be possible for a node to be visible but not be allowed / expected to display. + ASDisplayNodeAssertFalse(ASInterfaceStateIncludesVisible(newState) && !ASInterfaceStateIncludesDisplay(newState)); ASInterfaceState oldState = ASInterfaceStateNone; { ASDN::MutexLocker l(_propertyLock); diff --git a/AsyncDisplayKit/AsyncDisplayKit+Debug.h b/AsyncDisplayKit/AsyncDisplayKit+Debug.h index c9be26f11a..4192d67398 100644 --- a/AsyncDisplayKit/AsyncDisplayKit+Debug.h +++ b/AsyncDisplayKit/AsyncDisplayKit+Debug.h @@ -6,9 +6,19 @@ // Copyright © 2016 Facebook. All rights reserved. // -#import +#import "ASControlNode.h" #import "ASImageNode.h" +@interface ASControlNode (Debugging) + +/** + Class method to enable a visualization overlay of the tapable area on the ASControlNode. For app debugging purposes only. + @param enable Specify YES to make this debug feature enabled when messaging the ASControlNode class. + */ ++ (void)setEnableHitTestDebug:(BOOL)enable; + +@end + @interface ASImageNode (Debugging) /** @@ -20,4 +30,4 @@ + (void)setShouldShowImageScalingOverlay:(BOOL)show; + (BOOL)shouldShowImageScalingOverlay; -@end \ No newline at end of file +@end diff --git a/AsyncDisplayKit/Details/ASRangeController.mm b/AsyncDisplayKit/Details/ASRangeController.mm index 22ad82e426..d89d117af4 100644 --- a/AsyncDisplayKit/Details/ASRangeController.mm +++ b/AsyncDisplayKit/Details/ASRangeController.mm @@ -238,6 +238,7 @@ static UIApplicationState __ApplicationState = UIApplicationStateActive; [self registerForNotificationsForInterfaceStateIfNeeded:selfInterfaceState]; #if ASRangeControllerLoggingEnabled + ASDisplayNodeAssertTrue([visibleIndexPaths isSubsetOfSet:displayIndexPaths]); NSMutableArray *modifiedIndexPaths = (ASRangeControllerLoggingEnabled ? [NSMutableArray array] : nil); #endif @@ -247,14 +248,15 @@ static UIApplicationState __ApplicationState = UIApplicationStateActive; ASInterfaceState interfaceState = ASInterfaceStateMeasureLayout; if (ASInterfaceStateIncludesVisible(selfInterfaceState)) { - if ([fetchDataIndexPaths containsObject:indexPath]) { - interfaceState |= ASInterfaceStateFetchData; - } - if ([displayIndexPaths containsObject:indexPath]) { - interfaceState |= ASInterfaceStateDisplay; - } if ([visibleIndexPaths containsObject:indexPath]) { - interfaceState |= ASInterfaceStateVisible; + interfaceState |= (ASInterfaceStateVisible | ASInterfaceStateDisplay | ASInterfaceStateFetchData); + } else { + if ([fetchDataIndexPaths containsObject:indexPath]) { + interfaceState |= ASInterfaceStateFetchData; + } + if ([displayIndexPaths containsObject:indexPath]) { + interfaceState |= ASInterfaceStateDisplay; + } } } else { // If selfInterfaceState isn't visible, then visibleIndexPaths represents what /will/ be immediately visible at the From f39eacf78995b6de4fb35b120d86a450cf9565b7 Mon Sep 17 00:00:00 2001 From: Eric Horacek Date: Mon, 14 Mar 2016 11:27:20 -0700 Subject: [PATCH 21/26] Fix umbrella header `ASRunLoopQueue` was added in #1341 and declared as a public header. However, it was not added to the umbrella header. As-is, when consumers integrate `1.9.7`, the framework will not compile with the error: ``` Umbrella header for module 'AsyncDisplayKit' does not include header 'ASDisplayNode+Beta.h' ``` --- AsyncDisplayKit/AsyncDisplayKit.h | 1 + 1 file changed, 1 insertion(+) diff --git a/AsyncDisplayKit/AsyncDisplayKit.h b/AsyncDisplayKit/AsyncDisplayKit.h index fc345706c3..8fbeec4771 100644 --- a/AsyncDisplayKit/AsyncDisplayKit.h +++ b/AsyncDisplayKit/AsyncDisplayKit.h @@ -74,5 +74,6 @@ #import #import #import +#import #import From 6ebb376a4d1ca5acd3ad98c18aa4c8e8a392baa3 Mon Sep 17 00:00:00 2001 From: Eric Jensen Date: Tue, 15 Mar 2016 12:58:23 -0700 Subject: [PATCH 22/26] Annotate ASImageNode's imageModificationBlock as nullable --- AsyncDisplayKit/ASImageNode.h | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/AsyncDisplayKit/ASImageNode.h b/AsyncDisplayKit/ASImageNode.h index b2298752e5..ee8f1f1cc2 100644 --- a/AsyncDisplayKit/ASImageNode.h +++ b/AsyncDisplayKit/ASImageNode.h @@ -94,7 +94,7 @@ typedef UIImage * _Nullable (^asimagenode_modification_block_t)(UIImage *image); * @discussion Can be used to add image effects (such as rounding, adding * borders, or other pattern overlays) without extraneous display calls. */ -@property (nonatomic, readwrite, copy) asimagenode_modification_block_t imageModificationBlock; +@property (nullable, nonatomic, readwrite, copy) asimagenode_modification_block_t imageModificationBlock; /** * @abstract Marks the receiver as needing display and performs a block after From 22b105bfdc45fbf5f197e791cd1149337cb0c78e Mon Sep 17 00:00:00 2001 From: Huy Nguyen Date: Mon, 14 Mar 2016 22:30:31 -0700 Subject: [PATCH 23/26] ASCollectionView doesn't animate size changes if some of the updated cell nodes don't want to --- AsyncDisplayKit/ASCollectionView.mm | 88 ++++++++++++++++--- AsyncDisplayKit/ASDisplayNode.h | 5 ++ AsyncDisplayKit/ASDisplayNode.mm | 15 ++++ AsyncDisplayKit/ASDisplayNodeExtras.h | 11 ++- AsyncDisplayKit/ASDisplayNodeExtras.mm | 17 ++-- .../Private/ASDisplayNodeInternal.h | 1 + 6 files changed, 118 insertions(+), 19 deletions(-) diff --git a/AsyncDisplayKit/ASCollectionView.mm b/AsyncDisplayKit/ASCollectionView.mm index a9c79acf1c..f8929f170d 100644 --- a/AsyncDisplayKit/ASCollectionView.mm +++ b/AsyncDisplayKit/ASCollectionView.mm @@ -15,6 +15,7 @@ #import "ASCollectionViewLayoutController.h" #import "ASCollectionViewFlowLayoutInspector.h" #import "ASCollectionViewLayoutFacilitatorProtocol.h" +#import "ASDisplayNodeExtras.h" #import "ASDisplayNode+FrameworkPrivate.h" #import "ASDisplayNode+Beta.h" #import "ASInternalHelpers.h" @@ -57,6 +58,36 @@ static NSString * const kCellReuseIdentifier = @"_ASCollectionViewCell"; @end +#pragma mark - +#pragma mark _ASCollectionViewNodeSizeUpdateContext + +/** + * This class contains all the nodes that have a new size and UICollectionView should requery them all at once. + * It is intended to be used strictly on main thread and is not thread safe. + */ +@interface _ASCollectionViewNodeSizeInvalidationContext : NSObject +/** + * It's possible that a node triggered multiple size changes before main thread has a chance to execute `requeryNodeSizes`. + * Therefore, a set is preferred here, to avoid asking ASDataController to search for index path of the same node multiple times. + */ +@property (nonatomic, strong) NSMutableSet *invalidatedNodes; +@property (nonatomic, assign) BOOL shouldAnimate; +@end + +@implementation _ASCollectionViewNodeSizeInvalidationContext + +- (instancetype)init +{ + self = [super init]; + if (self) { + _invalidatedNodes = [NSMutableSet set]; + _shouldAnimate = YES; + } + return self; +} + +@end + #pragma mark - #pragma mark ASCollectionView. @@ -78,7 +109,7 @@ static NSString * const kCellReuseIdentifier = @"_ASCollectionViewCell"; BOOL _asyncDelegateImplementsScrollviewDidScroll; BOOL _asyncDataSourceImplementsConstrainedSizeForNode; BOOL _asyncDataSourceImplementsNodeBlockForItemAtIndexPath; - BOOL _queuedNodeSizeUpdate; + _ASCollectionViewNodeSizeInvalidationContext *_queuedNodeSizeInvalidationContext; // Main thread only BOOL _isDeallocating; ASBatchContext *_batchContext; @@ -1018,24 +1049,61 @@ static NSString * const kCellReuseIdentifier = @"_ASCollectionViewCell"; { ASDisplayNodeAssertMainThread(); - if (!sizeChanged || _queuedNodeSizeUpdate) { + if (!sizeChanged) { return; } - _queuedNodeSizeUpdate = YES; - [_layoutFacilitator collectionViewWillEditCellsAtIndexPaths:@[[self indexPathForNode:node]] batched:NO]; - [self performSelector:@selector(requeryNodeSizes) - withObject:nil - afterDelay:0 - inModes:@[ NSRunLoopCommonModes ]]; + BOOL queued = (_queuedNodeSizeInvalidationContext != nil); + if (!queued) { + _queuedNodeSizeInvalidationContext = [[_ASCollectionViewNodeSizeInvalidationContext alloc] init]; + + __weak __typeof__(self) weakSelf = self; + dispatch_async(dispatch_get_main_queue(), ^{ + __typeof__(self) strongSelf = weakSelf; + if (strongSelf) { + [strongSelf requeryNodeSizes]; + } + }); + } + + [_queuedNodeSizeInvalidationContext.invalidatedNodes addObject:node]; + + // Check if this node or one of its subnodes can be animated. + // If the context is already non-animated, don't bother checking this node. + if (_queuedNodeSizeInvalidationContext.shouldAnimate) { + BOOL (^shouldNotAnimateBlock)(ASDisplayNode *) = ^BOOL(ASDisplayNode * _Nonnull node) { + return node.shouldAnimateSizeChanges == NO; + }; + if (ASDisplayNodeFindFirstNode(node, shouldNotAnimateBlock) != nil) { + // One single non-animated cell node causes the whole context to be non-animated + _queuedNodeSizeInvalidationContext.shouldAnimate = NO; + } + } } // Cause UICollectionView to requery for the new size of all nodes - (void)requeryNodeSizes { - _queuedNodeSizeUpdate = NO; + ASDisplayNodeAssertMainThread(); + NSSet *nodes = _queuedNodeSizeInvalidationContext.invalidatedNodes; + NSMutableArray *indexPaths = [NSMutableArray arrayWithCapacity:nodes.count]; + for (ASCellNode *node in nodes) { + NSIndexPath *indexPath = [self indexPathForNode:node]; + if (indexPath != nil) { + [indexPaths addObject:indexPath]; + } + } - [super performBatchUpdates:^{} completion:nil]; + if (indexPaths.count > 0) { + [_layoutFacilitator collectionViewWillEditCellsAtIndexPaths:indexPaths batched:NO]; + + ASPerformBlockWithoutAnimation(!_queuedNodeSizeInvalidationContext.shouldAnimate, ^{ + // Perform an empty update transaction here to trigger UICollectionView to requery row sizes and layout its subviews again + [super performBatchUpdates:^{} completion:nil]; + }); + } + + _queuedNodeSizeInvalidationContext = nil; } #pragma mark - Memory Management diff --git a/AsyncDisplayKit/ASDisplayNode.h b/AsyncDisplayKit/ASDisplayNode.h index 2f67dc170c..7a8d6ddffe 100644 --- a/AsyncDisplayKit/ASDisplayNode.h +++ b/AsyncDisplayKit/ASDisplayNode.h @@ -428,6 +428,11 @@ NS_ASSUME_NONNULL_BEGIN */ @property (nonatomic, assign) BOOL displaySuspended; +/** + * @abstract Whether size changes should be animated. Default to YES. + */ +@property (nonatomic, assign) BOOL shouldAnimateSizeChanges; + /** * @abstract Prevent the node and its descendants' layer from displaying. * diff --git a/AsyncDisplayKit/ASDisplayNode.mm b/AsyncDisplayKit/ASDisplayNode.mm index d16479a5d5..cacd44e51b 100644 --- a/AsyncDisplayKit/ASDisplayNode.mm +++ b/AsyncDisplayKit/ASDisplayNode.mm @@ -114,6 +114,7 @@ static struct ASDisplayNodeFlags GetASDisplayNodeFlags(Class c, ASDisplayNode *i flags.isInHierarchy = NO; flags.displaysAsynchronously = YES; + flags.shouldAnimateSizeChanges = YES; flags.implementsDrawRect = ([c respondsToSelector:@selector(drawRect:withParameters:isCancelled:isRasterizing:)] ? 1 : 0); flags.implementsImageDisplay = ([c respondsToSelector:@selector(displayWithParameters:isCancelled:)] ? 1 : 0); if (instance) { @@ -2502,6 +2503,20 @@ static void _recursivelySetDisplaySuspended(ASDisplayNode *node, CALayer *layer, } } +- (BOOL)shouldAnimateSizeChanges +{ + ASDisplayNodeAssertThreadAffinity(self); + ASDN::MutexLocker l(_propertyLock); + return _flags.shouldAnimateSizeChanges; +} + +-(void)setShouldAnimateSizeChanges:(BOOL)shouldAnimateSizeChanges +{ + ASDisplayNodeAssertThreadAffinity(self); + ASDN::MutexLocker l(_propertyLock); + _flags.shouldAnimateSizeChanges = shouldAnimateSizeChanges; +} + static const char *ASDisplayNodeDrawingPriorityKey = "ASDrawingPriority"; - (void)setDrawingPriority:(NSInteger)drawingPriority diff --git a/AsyncDisplayKit/ASDisplayNodeExtras.h b/AsyncDisplayKit/ASDisplayNodeExtras.h index 319b5ff62d..6b5bf3f84d 100644 --- a/AsyncDisplayKit/ASDisplayNodeExtras.h +++ b/AsyncDisplayKit/ASDisplayNodeExtras.h @@ -91,12 +91,12 @@ extern void ASDisplayNodePerformBlockOnEverySubnode(ASDisplayNode *node, void(^b /** Given a display node, traverses up the layer tree hierarchy, returning the first display node that passes block. */ -extern id _Nullable ASDisplayNodeFind(ASDisplayNode * _Nullable node, BOOL (^block)(ASDisplayNode *node)); +extern id _Nullable ASDisplayNodeFindFirstSupernode(ASDisplayNode * _Nullable node, BOOL (^block)(ASDisplayNode *node)); /** Given a display node, traverses up the layer tree hierarchy, returning the first display node of kind class. */ -extern id _Nullable ASDisplayNodeFindClass(ASDisplayNode *start, Class c); +extern id _Nullable ASDisplayNodeFindFirstSupernodeOfClass(ASDisplayNode *start, Class c); /** * Given two nodes, finds their most immediate common parent. Used for geometry conversion methods. @@ -124,7 +124,12 @@ extern NSArray *ASDisplayNodeFindAllSubnodes(ASDisplayNode *sta extern NSArray *ASDisplayNodeFindAllSubnodesOfClass(ASDisplayNode *start, Class c); /** - Given a display node, traverses down the node hierarchy, returning the depth-first display node that pass the block. + Given a display node, traverses down the node hierarchy, returning the depth-first display node, including the start node that pass the block. + */ +extern __kindof ASDisplayNode * ASDisplayNodeFindFirstNode(ASDisplayNode *start, BOOL (^block)(ASDisplayNode *node)); + +/** + Given a display node, traverses down the node hierarchy, returning the depth-first display node, excluding the start node, that pass the block */ extern __kindof ASDisplayNode * ASDisplayNodeFindFirstSubnode(ASDisplayNode *start, BOOL (^block)(ASDisplayNode *node)); diff --git a/AsyncDisplayKit/ASDisplayNodeExtras.mm b/AsyncDisplayKit/ASDisplayNodeExtras.mm index 67e9185d4e..75b9ddd575 100644 --- a/AsyncDisplayKit/ASDisplayNodeExtras.mm +++ b/AsyncDisplayKit/ASDisplayNodeExtras.mm @@ -53,7 +53,7 @@ extern void ASDisplayNodePerformBlockOnEverySubnode(ASDisplayNode *node, void(^b } } -id ASDisplayNodeFind(ASDisplayNode *node, BOOL (^block)(ASDisplayNode *node)) +id ASDisplayNodeFindFirstSupernode(ASDisplayNode *node, BOOL (^block)(ASDisplayNode *node)) { CALayer *layer = node.layer; @@ -68,9 +68,9 @@ id ASDisplayNodeFind(ASDisplayNode *node, BOOL (^block)(ASDisplayNode *node)) return nil; } -id ASDisplayNodeFindClass(ASDisplayNode *start, Class c) +id ASDisplayNodeFindFirstSupernodeOfClass(ASDisplayNode *start, Class c) { - return ASDisplayNodeFind(start, ^(ASDisplayNode *n) { + return ASDisplayNodeFindFirstSupernode(start, ^(ASDisplayNode *n) { return [n isKindOfClass:c]; }); } @@ -128,10 +128,10 @@ extern NSArray *ASDisplayNodeFindAllSubnodesOfClass(ASDisplayNo #pragma mark - Find first subnode -static ASDisplayNode *_ASDisplayNodeFindFirstSubnode(ASDisplayNode *startNode, BOOL includeStartNode, BOOL (^block)(ASDisplayNode *node)) +static ASDisplayNode *_ASDisplayNodeFindFirstNode(ASDisplayNode *startNode, BOOL includeStartNode, BOOL (^block)(ASDisplayNode *node)) { for (ASDisplayNode *subnode in startNode.subnodes) { - ASDisplayNode *foundNode = _ASDisplayNodeFindFirstSubnode(subnode, YES, block); + ASDisplayNode *foundNode = _ASDisplayNodeFindFirstNode(subnode, YES, block); if (foundNode) { return foundNode; } @@ -143,9 +143,14 @@ static ASDisplayNode *_ASDisplayNodeFindFirstSubnode(ASDisplayNode *startNode, B return nil; } +extern __kindof ASDisplayNode * ASDisplayNodeFindFirstNode(ASDisplayNode *startNode, BOOL (^block)(ASDisplayNode *node)) +{ + return _ASDisplayNodeFindFirstNode(startNode, YES, block); +} + extern __kindof ASDisplayNode * ASDisplayNodeFindFirstSubnode(ASDisplayNode *startNode, BOOL (^block)(ASDisplayNode *node)) { - return _ASDisplayNodeFindFirstSubnode(startNode, NO, block); + return _ASDisplayNodeFindFirstNode(startNode, NO, block); } extern __kindof ASDisplayNode * ASDisplayNodeFindFirstSubnodeOfClass(ASDisplayNode *start, Class c) diff --git a/AsyncDisplayKit/Private/ASDisplayNodeInternal.h b/AsyncDisplayKit/Private/ASDisplayNodeInternal.h index e491aac0c6..89c5678a96 100644 --- a/AsyncDisplayKit/Private/ASDisplayNodeInternal.h +++ b/AsyncDisplayKit/Private/ASDisplayNodeInternal.h @@ -70,6 +70,7 @@ FOUNDATION_EXPORT NSString * const ASRenderingEngineDidDisplayNodesScheduledBefo unsigned shouldRasterizeDescendants:1; unsigned shouldBypassEnsureDisplay:1; unsigned displaySuspended:1; + unsigned shouldAnimateSizeChanges:1; unsigned hasCustomDrawingPriority:1; // whether custom drawing is enabled From 863b0ca956a48ecdf802c12102dd2dfac4ebb7e6 Mon Sep 17 00:00:00 2001 From: Michael Schneider Date: Tue, 15 Mar 2016 16:30:08 -0700 Subject: [PATCH 24/26] Remove the lock for accessing _pendingDisplayNodes and force methods regarding display to the main thread This should fix a deadlock in ASDisplayNode and it's caused by displayWillStart: and where one thread is recursing down the tree and another thread is recursing up the tree. We remove the lock in _pendingDisplayNodes for the property, but need to guarantee that we only modify the _pendingDisplayNodes state on the main thread. Furthermore add documentation on what thread displayWillStart and displayDidFinish will be called --- AsyncDisplayKit/ASDisplayNode+Subclasses.h | 4 ++ AsyncDisplayKit/ASDisplayNode.mm | 47 ++++++++++++---------- 2 files changed, 30 insertions(+), 21 deletions(-) diff --git a/AsyncDisplayKit/ASDisplayNode+Subclasses.h b/AsyncDisplayKit/ASDisplayNode+Subclasses.h index e369b5c18a..023567d9a2 100644 --- a/AsyncDisplayKit/ASDisplayNode+Subclasses.h +++ b/AsyncDisplayKit/ASDisplayNode+Subclasses.h @@ -206,6 +206,8 @@ NS_ASSUME_NONNULL_BEGIN * * @discussion Subclasses may override this method to be notified when display (asynchronous or synchronous) is * about to begin. + * + * @note Called on the main thread only */ - (void)displayWillStart ASDISPLAYNODE_REQUIRES_SUPER; @@ -214,6 +216,8 @@ NS_ASSUME_NONNULL_BEGIN * * @discussion Subclasses may override this method to be notified when display (asynchronous or synchronous) has * completed. + * + * @note Called on the main thread only */ - (void)displayDidFinish ASDISPLAYNODE_REQUIRES_SUPER; diff --git a/AsyncDisplayKit/ASDisplayNode.mm b/AsyncDisplayKit/ASDisplayNode.mm index d16479a5d5..256e9e7d03 100644 --- a/AsyncDisplayKit/ASDisplayNode.mm +++ b/AsyncDisplayKit/ASDisplayNode.mm @@ -1657,7 +1657,7 @@ static NSInteger incrementIfFound(NSInteger i) { // The node sending the message should usually be passed as the parameter, similar to the delegation pattern. - (void)_pendingNodeWillDisplay:(ASDisplayNode *)node { - ASDN::MutexLocker l(_propertyLock); + ASDisplayNodeAssertMainThread(); if (!_pendingDisplayNodes) { _pendingDisplayNodes = [[NSMutableSet alloc] init]; @@ -1670,27 +1670,25 @@ static NSInteger incrementIfFound(NSInteger i) { // The node sending the message should usually be passed as the parameter, similar to the delegation pattern. - (void)_pendingNodeDidDisplay:(ASDisplayNode *)node { - ASDN::MutexLocker l(_propertyLock); + ASDisplayNodeAssertMainThread(); [_pendingDisplayNodes removeObject:node]; // only trampoline if there is a placeholder and nodes are done displaying if ([self _pendingDisplayNodesHaveFinished] && _placeholderLayer.superlayer) { - dispatch_async(dispatch_get_main_queue(), ^{ - void (^cleanupBlock)() = ^{ - [self _tearDownPlaceholderLayer]; - }; + void (^cleanupBlock)() = ^{ + [self _tearDownPlaceholderLayer]; + }; - if (_placeholderFadeDuration > 0.0 && ASInterfaceStateIncludesVisible(self.interfaceState)) { - [CATransaction begin]; - [CATransaction setCompletionBlock:cleanupBlock]; - [CATransaction setAnimationDuration:_placeholderFadeDuration]; - _placeholderLayer.opacity = 0.0; - [CATransaction commit]; - } else { - cleanupBlock(); - } - }); + if (_placeholderFadeDuration > 0.0 && ASInterfaceStateIncludesVisible(self.interfaceState)) { + [CATransaction begin]; + [CATransaction setCompletionBlock:cleanupBlock]; + [CATransaction setAnimationDuration:_placeholderFadeDuration]; + _placeholderLayer.opacity = 0.0; + [CATransaction commit]; + } else { + cleanupBlock(); + } } } @@ -2260,6 +2258,8 @@ void recursivelyTriggerDisplayForLayer(CALayer *layer, BOOL shouldBlock) - (void)displayWillStart { + ASDisplayNodeAssertMainThread(); + // in case current node takes longer to display than it's subnodes, treat it as a dependent node [self _pendingNodeWillDisplay:self]; @@ -2288,6 +2288,8 @@ void recursivelyTriggerDisplayForLayer(CALayer *layer, BOOL shouldBlock) - (void)displayDidFinish { + ASDisplayNodeAssertMainThread(); + [self _pendingNodeDidDisplay:self]; [_supernode subnodeDisplayDidFinish:self]; @@ -2494,11 +2496,14 @@ static void _recursivelySetDisplaySuspended(ASDisplayNode *node, CALayer *layer, self.asyncLayer.displaySuspended = flag; if ([self __implementsDisplay]) { - if (flag) { - [_supernode subnodeDisplayDidFinish:self]; - } else { - [_supernode subnodeDisplayWillStart:self]; - } + // Display start and finish methods needs to happen on the main thread + ASPerformBlockOnMainThread(^{ + if (flag) { + [_supernode subnodeDisplayDidFinish:self]; + } else { + [_supernode subnodeDisplayWillStart:self]; + } + }); } } From b7a92b2947aaeccfdf49cc759aa31d5949cb281f Mon Sep 17 00:00:00 2001 From: Michael Schneider Date: Tue, 15 Mar 2016 16:30:23 -0700 Subject: [PATCH 25/26] Add documentation for visibilityDidChange: --- AsyncDisplayKit/ASDisplayNode+Subclasses.h | 5 +++++ AsyncDisplayKit/ASDisplayNode.mm | 1 + 2 files changed, 6 insertions(+) diff --git a/AsyncDisplayKit/ASDisplayNode+Subclasses.h b/AsyncDisplayKit/ASDisplayNode+Subclasses.h index 023567d9a2..d7dad4cbc8 100644 --- a/AsyncDisplayKit/ASDisplayNode+Subclasses.h +++ b/AsyncDisplayKit/ASDisplayNode+Subclasses.h @@ -231,6 +231,11 @@ NS_ASSUME_NONNULL_BEGIN */ - (void)interfaceStateDidChange:(ASInterfaceState)newState fromState:(ASInterfaceState)oldState ASDISPLAYNODE_REQUIRES_SUPER; +/** + * @abstract Called whenever the visiblity of the node changed. + * + * @discussion Subclasses may use this to monitor when they become visible. + */ - (void)visibilityDidChange:(BOOL)isVisible ASDISPLAYNODE_REQUIRES_SUPER; /** diff --git a/AsyncDisplayKit/ASDisplayNode.mm b/AsyncDisplayKit/ASDisplayNode.mm index 256e9e7d03..1421d8fa27 100644 --- a/AsyncDisplayKit/ASDisplayNode.mm +++ b/AsyncDisplayKit/ASDisplayNode.mm @@ -1982,6 +1982,7 @@ void recursivelyTriggerDisplayForLayer(CALayer *layer, BOOL shouldBlock) - (void)visibilityDidChange:(BOOL)isVisible { + // subclass override } /** From abd148ae2ec2aab716ec1b805d85e4aecddd6595 Mon Sep 17 00:00:00 2001 From: Scott Goodson Date: Tue, 15 Mar 2016 17:24:05 -0700 Subject: [PATCH 26/26] [Testing] ASVideoNodeTests should not pass in an invalid combination of ASInterfaceState flags. --- AsyncDisplayKitTests/ASVideoNodeTests.m | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/AsyncDisplayKitTests/ASVideoNodeTests.m b/AsyncDisplayKitTests/ASVideoNodeTests.m index d14329d936..4338323705 100644 --- a/AsyncDisplayKitTests/ASVideoNodeTests.m +++ b/AsyncDisplayKitTests/ASVideoNodeTests.m @@ -140,7 +140,7 @@ _videoNode.asset = _firstAsset; [_videoNode pause]; - [_videoNode setInterfaceState:ASInterfaceStateVisible]; + [_videoNode setInterfaceState:ASInterfaceStateVisible | ASInterfaceStateDisplay]; [_videoNode didLoad]; XCTAssert(![_videoNode.subnodes containsObject:_videoNode.playerNode]);