diff --git a/.github/ISSUE_TEMPLATE.md b/.github/ISSUE_TEMPLATE.md index 03194fca3b..c6e5d8efcc 100644 --- a/.github/ISSUE_TEMPLATE.md +++ b/.github/ISSUE_TEMPLATE.md @@ -1,3 +1,6 @@ +// If you're looking for help, please consider joining our slack channel: +// http://asyncdisplaykit.org/slack + // The more information you include, the faster we can help you out! // Please include: a sample project or screenshots, code snippets // AsyncDisplayKit version, and/or backtraces for any crashes (> bt all). diff --git a/AsyncDisplayKit.podspec b/AsyncDisplayKit.podspec index e5101b4a85..a3b062d43d 100644 --- a/AsyncDisplayKit.podspec +++ b/AsyncDisplayKit.podspec @@ -1,6 +1,6 @@ Pod::Spec.new do |spec| spec.name = 'AsyncDisplayKit' - spec.version = '2.0-beta.1' + spec.version = '2.0-rc.1' spec.license = { :type => 'BSD' } spec.homepage = 'http://asyncdisplaykit.org' spec.authors = { 'Scott Goodson' => 'scottgoodson@gmail.com' } diff --git a/AsyncDisplayKit.xcodeproj/project.pbxproj b/AsyncDisplayKit.xcodeproj/project.pbxproj index 660e89e877..9cce88a31f 100644 --- a/AsyncDisplayKit.xcodeproj/project.pbxproj +++ b/AsyncDisplayKit.xcodeproj/project.pbxproj @@ -154,7 +154,7 @@ 34EFC7771B701D2D00AD841F /* ASStackUnpositionedLayout.h in Headers */ = {isa = PBXBuildFile; fileRef = ACF6ED491B17847A00DA7C62 /* ASStackUnpositionedLayout.h */; }; 34EFC7781B701D3100AD841F /* ASStackUnpositionedLayout.mm in Sources */ = {isa = PBXBuildFile; fileRef = ACF6ED4A1B17847A00DA7C62 /* ASStackUnpositionedLayout.mm */; }; 34EFC7791B701D3600AD841F /* ASLayoutSpecUtilities.h in Headers */ = {isa = PBXBuildFile; fileRef = ACF6ED451B17847A00DA7C62 /* ASLayoutSpecUtilities.h */; }; - 3C9C128519E616EF00E942A0 /* ASTableViewTests.m in Sources */ = {isa = PBXBuildFile; fileRef = 3C9C128419E616EF00E942A0 /* ASTableViewTests.m */; }; + 3C9C128519E616EF00E942A0 /* ASTableViewTests.mm in Sources */ = {isa = PBXBuildFile; fileRef = 3C9C128419E616EF00E942A0 /* ASTableViewTests.mm */; }; 430E7C901B4C23F100697A4C /* ASIndexPath.h in Headers */ = {isa = PBXBuildFile; fileRef = 430E7C8D1B4C23F100697A4C /* ASIndexPath.h */; settings = {ATTRIBUTES = (Public, ); }; }; 430E7C911B4C23F100697A4C /* ASIndexPath.m in Sources */ = {isa = PBXBuildFile; fileRef = 430E7C8E1B4C23F100697A4C /* ASIndexPath.m */; }; 430E7C921B4C23F100697A4C /* ASIndexPath.m in Sources */ = {isa = PBXBuildFile; fileRef = 430E7C8E1B4C23F100697A4C /* ASIndexPath.m */; }; @@ -972,7 +972,7 @@ 299DA1A71A828D2900162D41 /* ASBatchContext.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = ASBatchContext.h; sourceTree = ""; }; 299DA1A81A828D2900162D41 /* ASBatchContext.mm */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.cpp.objcpp; path = ASBatchContext.mm; sourceTree = ""; }; 29CDC2E11AAE70D000833CA4 /* ASBasicImageDownloaderContextTests.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = ASBasicImageDownloaderContextTests.m; sourceTree = ""; }; - 3C9C128419E616EF00E942A0 /* ASTableViewTests.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; lineEnding = 0; path = ASTableViewTests.m; sourceTree = ""; xcLanguageSpecificationIdentifier = xcode.lang.objc; }; + 3C9C128419E616EF00E942A0 /* ASTableViewTests.mm */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.cpp.objcpp; lineEnding = 0; path = ASTableViewTests.mm; sourceTree = ""; xcLanguageSpecificationIdentifier = xcode.lang.objc; }; 430E7C8D1B4C23F100697A4C /* ASIndexPath.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = ASIndexPath.h; sourceTree = ""; }; 430E7C8E1B4C23F100697A4C /* ASIndexPath.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = ASIndexPath.m; sourceTree = ""; }; 464052191A3F83C40061C0BA /* ASDataController.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; lineEnding = 0; path = ASDataController.h; sourceTree = ""; }; @@ -1451,7 +1451,7 @@ 697B31591CFE4B410049936F /* ASEditableTextNodeTests.m */, 052EE0651A159FEF002C6279 /* ASMultiplexImageNodeTests.m */, 058D0A32195D057000B7D73C /* ASMutableAttributedStringBuilderTests.m */, - 3C9C128419E616EF00E942A0 /* ASTableViewTests.m */, + 3C9C128419E616EF00E942A0 /* ASTableViewTests.mm */, CC4981B21D1A02BE004E13CC /* ASTableViewThrashTests.m */, 058D0A33195D057000B7D73C /* ASTextKitCoreTextAdditionsTests.m */, 254C6B511BF8FE6D003EC431 /* ASTextKitTruncationTests.mm */, @@ -2360,7 +2360,7 @@ 05EA6FE71AC0966E00E35788 /* ASSnapshotTestCase.m in Sources */, ACF6ED631B178DC700DA7C62 /* ASStackLayoutSpecSnapshotTests.mm in Sources */, 81E95C141D62639600336598 /* ASTextNodeSnapshotTests.m in Sources */, - 3C9C128519E616EF00E942A0 /* ASTableViewTests.m in Sources */, + 3C9C128519E616EF00E942A0 /* ASTableViewTests.mm in Sources */, AEEC47E41C21D3D200EC1693 /* ASVideoNodeTests.m in Sources */, 254C6B521BF8FE6D003EC431 /* ASTextKitTruncationTests.mm in Sources */, 058D0A3D195D057000B7D73C /* ASTextKitCoreTextAdditionsTests.m in Sources */, diff --git a/AsyncDisplayKit/ASButtonNode.mm b/AsyncDisplayKit/ASButtonNode.mm index fda284ad70..f260fd6a45 100644 --- a/AsyncDisplayKit/ASButtonNode.mm +++ b/AsyncDisplayKit/ASButtonNode.mm @@ -527,6 +527,7 @@ - (void)layout { [super layout]; + _backgroundImageNode.hidden = (_backgroundImageNode.image == nil); _imageNode.hidden = (_imageNode.image == nil); _titleNode.hidden = (_titleNode.attributedText.length == 0); diff --git a/AsyncDisplayKit/ASCellNode.h b/AsyncDisplayKit/ASCellNode.h index ed9d1a4ea5..255db70ff5 100644 --- a/AsyncDisplayKit/ASCellNode.h +++ b/AsyncDisplayKit/ASCellNode.h @@ -118,6 +118,13 @@ typedef NS_ENUM(NSUInteger, ASCellNodeVisibilityEvent) { */ @property (nonatomic, readonly, nullable) NSIndexPath *indexPath; +/** + * The backing view controller, or @c nil if the node wasn't initialized with backing view controller + * @note This property must be accessed on the main thread. + */ +@property (nonatomic, readonly, nullable) UIViewController *viewController; + + /** * The owning node (ASCollectionNode/ASTableNode) of this cell node, or @c nil if this node is * not a valid item inside a table node or collection node or if those nodes are nil. diff --git a/AsyncDisplayKit/ASCellNode.mm b/AsyncDisplayKit/ASCellNode.mm index 747e73d675..ed4be865f8 100644 --- a/AsyncDisplayKit/ASCellNode.mm +++ b/AsyncDisplayKit/ASCellNode.mm @@ -60,6 +60,7 @@ static NSMutableSet *__cellClassesForVisibilityNotifications = nil; // See +init // Use UITableViewCell defaults _selectionStyle = UITableViewCellSelectionStyleDefault; self.clipsToBounds = YES; + return self; } @@ -119,15 +120,13 @@ static NSMutableSet *__cellClassesForVisibilityNotifications = nil; // See +init _viewControllerNode.frame = self.bounds; } -- (void)__setNeedsLayout +- (void)_locked_displayNodeDidInvalidateSizeNewSize:(CGSize)newSize { - CGSize oldSize = self.calculatedSize; - [super __setNeedsLayout]; - - //Adding this lock because lock used to be held when this method was called. Not sure if it's necessary for - //didRelayoutFromOldSize:toNewSize: - ASDN::MutexLocker l(__instanceLock__); - [self didRelayoutFromOldSize:oldSize toNewSize:self.calculatedSize]; + CGSize oldSize = self.bounds.size; + if (CGSizeEqualToSize(oldSize, newSize) == NO) { + self.frame = {self.frame.origin, newSize}; + [self didRelayoutFromOldSize:oldSize toNewSize:newSize]; + } } - (void)transitionLayoutWithAnimation:(BOOL)animated @@ -238,6 +237,17 @@ static NSMutableSet *__cellClassesForVisibilityNotifications = nil; // See +init return nil; } +- (UIViewController *)viewController +{ + ASDisplayNodeAssertMainThread(); + // Force the view to load so that we will create the + // view controller if we haven't already. + if (self.isNodeLoaded == NO) { + [self view]; + } + return _viewController; +} + #pragma clang diagnostic push #pragma clang diagnostic ignored "-Wobjc-missing-super-calls" diff --git a/AsyncDisplayKit/ASCollectionNode.mm b/AsyncDisplayKit/ASCollectionNode.mm index 8a077be8e7..7f4da85d04 100644 --- a/AsyncDisplayKit/ASCollectionNode.mm +++ b/AsyncDisplayKit/ASCollectionNode.mm @@ -135,12 +135,6 @@ return nil; } -- (void)dealloc -{ - self.delegate = nil; - self.dataSource = nil; -} - #pragma mark ASDisplayNode - (void)didLoad @@ -175,10 +169,10 @@ [self.rangeController clearContents]; } -- (void)clearFetchedData +- (void)didExitPreloadState { - [super clearFetchedData]; - [self.rangeController clearFetchedData]; + [super didExitPreloadState]; + [self.rangeController clearPreloadedData]; } - (void)interfaceStateDidChange:(ASInterfaceState)newState fromState:(ASInterfaceState)oldState @@ -234,7 +228,8 @@ // Manually trampoline to the main thread. The view requires this be called on main // and asserting here isn't an option – it is a common pattern for users to clear // the delegate/dataSource in dealloc, which may be running on a background thread. - ASCollectionView *view = self.view; + // It is important that we avoid retaining self in this block, so that this method is dealloc-safe. + ASCollectionView *view = (ASCollectionView *)_view; ASPerformBlockOnMainThread(^{ view.asyncDelegate = delegate; }); @@ -259,7 +254,8 @@ // Manually trampoline to the main thread. The view requires this be called on main // and asserting here isn't an option – it is a common pattern for users to clear // the delegate/dataSource in dealloc, which may be running on a background thread. - ASCollectionView *view = self.view; + // It is important that we avoid retaining self in this block, so that this method is dealloc-safe. + ASCollectionView *view = (ASCollectionView *)_view; ASPerformBlockOnMainThread(^{ view.asyncDataSource = dataSource; }); diff --git a/AsyncDisplayKit/ASCollectionView.mm b/AsyncDisplayKit/ASCollectionView.mm index 26aa101d39..2cf78a6f58 100644 --- a/AsyncDisplayKit/ASCollectionView.mm +++ b/AsyncDisplayKit/ASCollectionView.mm @@ -29,6 +29,25 @@ #import "ASSectionContext.h" #import "ASCollectionView+Undeprecated.h" +/** + * A macro to get self.collectionNode and assign it to a local variable, or return + * the given value if nil. + * + * Previously we would set ASCollectionNode's dataSource & delegate to nil + * during dealloc. However, our asyncDelegate & asyncDataSource must be set on the + * main thread, so if the node is deallocated off-main, we won't learn about the change + * until later on. Since our @c collectionNode parameter to delegate methods (e.g. + * collectionNode:didEndDisplayingItemWithNode:) is nonnull, it's important that we never + * unintentionally pass nil (this will crash in Swift, in production). So we can use + * this macro to ensure that our node is still alive before calling out to the user + * on its behalf. + */ +#define GET_COLLECTIONNODE_OR_RETURN(__var, __val) \ + ASCollectionNode *__var = self.collectionNode; \ + if (__var == nil) { \ + return __val; \ + } + /// What, if any, invalidation should we perform during the next -layoutSubviews. typedef NS_ENUM(NSUInteger, ASCollectionViewInvalidationStyle) { /// Perform no invalidation. @@ -157,6 +176,15 @@ static NSString * const kCellReuseIdentifier = @"_ASCollectionViewCell"; * The collection view never queried your data source before the update to see that it actually had 0 items. */ BOOL _superIsPendingDataLoad; + + /** + * It's important that we always check for batch fetching at least once, but also + * that we do not check for batch fetching for empty updates (as that may cause an infinite + * loop of batch fetching, where the batch completes and performBatchUpdates: is called without + * actually making any changes.) So to handle the case where a collection is completely empty + * (0 sections) we always check at least once after each update (initial reload is the first update.) + */ + BOOL _hasEverCheckedForBatchFetchingDueToUpdate; struct { unsigned int scrollViewDidScroll:1; @@ -418,6 +446,7 @@ static NSString * const kCellReuseIdentifier = @"_ASCollectionViewCell"; || _asyncDataSourceFlags.collectionViewNodeForItem, @"Data source must implement collectionNode:nodeBlockForItemAtIndexPath: or collectionNode:nodeForItemAtIndexPath:"); } + _dataController.validationErrorSource = asyncDataSource; super.dataSource = (id)_proxyDataSource; //Cache results of layoutInspector to ensure flags are up to date if getter lazily loads a new one. @@ -827,7 +856,9 @@ static NSString * const kCellReuseIdentifier = @"_ASCollectionViewCell"; ASDisplayNodeAssertNotNil(cellNode, @"Expected node associated with cell that will be displayed not to be nil. indexPath: %@", indexPath); if (_asyncDelegateFlags.collectionNodeWillDisplayItem) { - [_asyncDelegate collectionNode:self.collectionNode willDisplayItemWithNode:cellNode]; + if (ASCollectionNode *collectionNode = self.collectionNode) { + [_asyncDelegate collectionNode:collectionNode willDisplayItemWithNode:cellNode]; + } } else if (_asyncDelegateFlags.collectionViewWillDisplayNodeForItem) { #pragma clang diagnostic push #pragma clang diagnostic ignored "-Wdeprecated-declarations" @@ -850,7 +881,9 @@ static NSString * const kCellReuseIdentifier = @"_ASCollectionViewCell"; ASDisplayNodeAssertNotNil(cellNode, @"Expected node associated with removed cell not to be nil."); if (_asyncDelegateFlags.collectionNodeDidEndDisplayingItem) { - [_asyncDelegate collectionNode:self.collectionNode didEndDisplayingItemWithNode:cellNode]; + if (ASCollectionNode *collectionNode = self.collectionNode) { + [_asyncDelegate collectionNode:collectionNode didEndDisplayingItemWithNode:cellNode]; + } } else if (_asyncDelegateFlags.collectionViewDidEndDisplayingNodeForItem) { #pragma clang diagnostic push #pragma clang diagnostic ignored "-Wdeprecated-declarations" @@ -869,27 +902,30 @@ static NSString * const kCellReuseIdentifier = @"_ASCollectionViewCell"; - (void)collectionView:(UICollectionView *)collectionView willDisplaySupplementaryView:(UICollectionReusableView *)view forElementKind:(NSString *)elementKind atIndexPath:(NSIndexPath *)indexPath { if (_asyncDelegateFlags.collectionNodeWillDisplaySupplementaryElement) { + GET_COLLECTIONNODE_OR_RETURN(collectionNode, (void)0); ASCellNode *node = [self supplementaryNodeForElementKind:elementKind atIndexPath:indexPath]; ASDisplayNodeAssert([node.supplementaryElementKind isEqualToString:elementKind], @"Expected node for supplementary element to have kind '%@', got '%@'.", elementKind, node.supplementaryElementKind); - [_asyncDelegate collectionNode:self.collectionNode willDisplaySupplementaryElementWithNode:node]; + [_asyncDelegate collectionNode:collectionNode willDisplaySupplementaryElementWithNode:node]; } } - (void)collectionView:(UICollectionView *)collectionView didEndDisplayingSupplementaryView:(UICollectionReusableView *)view forElementOfKind:(NSString *)elementKind atIndexPath:(NSIndexPath *)indexPath { if (_asyncDelegateFlags.collectionNodeDidEndDisplayingSupplementaryElement) { + GET_COLLECTIONNODE_OR_RETURN(collectionNode, (void)0); ASCellNode *node = [self supplementaryNodeForElementKind:elementKind atIndexPath:indexPath]; ASDisplayNodeAssert([node.supplementaryElementKind isEqualToString:elementKind], @"Expected node for supplementary element to have kind '%@', got '%@'.", elementKind, node.supplementaryElementKind); - [_asyncDelegate collectionNode:self.collectionNode didEndDisplayingSupplementaryElementWithNode:node]; + [_asyncDelegate collectionNode:collectionNode didEndDisplayingSupplementaryElementWithNode:node]; } } - (BOOL)collectionView:(UICollectionView *)collectionView shouldSelectItemAtIndexPath:(NSIndexPath *)indexPath { if (_asyncDelegateFlags.collectionNodeShouldSelectItem) { + GET_COLLECTIONNODE_OR_RETURN(collectionNode, NO); indexPath = [self convertIndexPathToCollectionNode:indexPath]; if (indexPath != nil) { - return [_asyncDelegate collectionNode:self.collectionNode shouldSelectItemAtIndexPath:indexPath]; + return [_asyncDelegate collectionNode:collectionNode shouldSelectItemAtIndexPath:indexPath]; } } else if (_asyncDelegateFlags.collectionViewShouldSelectItem) { #pragma clang diagnostic push @@ -903,9 +939,10 @@ static NSString * const kCellReuseIdentifier = @"_ASCollectionViewCell"; - (void)collectionView:(UICollectionView *)collectionView didSelectItemAtIndexPath:(nonnull NSIndexPath *)indexPath { if (_asyncDelegateFlags.collectionNodeDidSelectItem) { + GET_COLLECTIONNODE_OR_RETURN(collectionNode, (void)0); indexPath = [self convertIndexPathToCollectionNode:indexPath]; if (indexPath != nil) { - [_asyncDelegate collectionNode:self.collectionNode didSelectItemAtIndexPath:indexPath]; + [_asyncDelegate collectionNode:collectionNode didSelectItemAtIndexPath:indexPath]; } } else if (_asyncDelegateFlags.collectionViewDidSelectItem) { #pragma clang diagnostic push @@ -918,9 +955,10 @@ static NSString * const kCellReuseIdentifier = @"_ASCollectionViewCell"; - (BOOL)collectionView:(UICollectionView *)collectionView shouldDeselectItemAtIndexPath:(NSIndexPath *)indexPath { if (_asyncDelegateFlags.collectionNodeShouldDeselectItem) { + GET_COLLECTIONNODE_OR_RETURN(collectionNode, NO); indexPath = [self convertIndexPathToCollectionNode:indexPath]; if (indexPath != nil) { - return [_asyncDelegate collectionNode:self.collectionNode shouldDeselectItemAtIndexPath:indexPath]; + return [_asyncDelegate collectionNode:collectionNode shouldDeselectItemAtIndexPath:indexPath]; } } else if (_asyncDelegateFlags.collectionViewShouldDeselectItem) { #pragma clang diagnostic push @@ -934,9 +972,10 @@ static NSString * const kCellReuseIdentifier = @"_ASCollectionViewCell"; - (void)collectionView:(UICollectionView *)collectionView didDeselectItemAtIndexPath:(nonnull NSIndexPath *)indexPath { if (_asyncDelegateFlags.collectionNodeDidDeselectItem) { + GET_COLLECTIONNODE_OR_RETURN(collectionNode, (void)0); indexPath = [self convertIndexPathToCollectionNode:indexPath]; if (indexPath != nil) { - [_asyncDelegate collectionNode:self.collectionNode didDeselectItemAtIndexPath:indexPath]; + [_asyncDelegate collectionNode:collectionNode didDeselectItemAtIndexPath:indexPath]; } } else if (_asyncDelegateFlags.collectionViewDidDeselectItem) { #pragma clang diagnostic push @@ -949,9 +988,10 @@ static NSString * const kCellReuseIdentifier = @"_ASCollectionViewCell"; - (BOOL)collectionView:(UICollectionView *)collectionView shouldHighlightItemAtIndexPath:(NSIndexPath *)indexPath { if (_asyncDelegateFlags.collectionNodeShouldHighlightItem) { + GET_COLLECTIONNODE_OR_RETURN(collectionNode, NO); indexPath = [self convertIndexPathToCollectionNode:indexPath]; if (indexPath != nil) { - return [_asyncDelegate collectionNode:self.collectionNode shouldHighlightItemAtIndexPath:indexPath]; + return [_asyncDelegate collectionNode:collectionNode shouldHighlightItemAtIndexPath:indexPath]; } else { return YES; } @@ -967,9 +1007,10 @@ static NSString * const kCellReuseIdentifier = @"_ASCollectionViewCell"; - (void)collectionView:(UICollectionView *)collectionView didHighlightItemAtIndexPath:(nonnull NSIndexPath *)indexPath { if (_asyncDelegateFlags.collectionNodeDidHighlightItem) { + GET_COLLECTIONNODE_OR_RETURN(collectionNode, (void)0); indexPath = [self convertIndexPathToCollectionNode:indexPath]; if (indexPath != nil) { - [_asyncDelegate collectionNode:self.collectionNode didHighlightItemAtIndexPath:indexPath]; + [_asyncDelegate collectionNode:collectionNode didHighlightItemAtIndexPath:indexPath]; } } else if (_asyncDelegateFlags.collectionViewDidHighlightItem) { #pragma clang diagnostic push @@ -982,9 +1023,10 @@ static NSString * const kCellReuseIdentifier = @"_ASCollectionViewCell"; - (void)collectionView:(UICollectionView *)collectionView didUnhighlightItemAtIndexPath:(nonnull NSIndexPath *)indexPath { if (_asyncDelegateFlags.collectionNodeDidUnhighlightItem) { + GET_COLLECTIONNODE_OR_RETURN(collectionNode, (void)0); indexPath = [self convertIndexPathToCollectionNode:indexPath]; if (indexPath != nil) { - [_asyncDelegate collectionNode:self.collectionNode didUnhighlightItemAtIndexPath:indexPath]; + [_asyncDelegate collectionNode:collectionNode didUnhighlightItemAtIndexPath:indexPath]; } } else if (_asyncDelegateFlags.collectionViewDidUnhighlightItem) { #pragma clang diagnostic push @@ -997,9 +1039,10 @@ static NSString * const kCellReuseIdentifier = @"_ASCollectionViewCell"; - (BOOL)collectionView:(UICollectionView *)collectionView shouldShowMenuForItemAtIndexPath:(nonnull NSIndexPath *)indexPath { if (_asyncDelegateFlags.collectionNodeShouldShowMenuForItem) { + GET_COLLECTIONNODE_OR_RETURN(collectionNode, NO); indexPath = [self convertIndexPathToCollectionNode:indexPath]; if (indexPath != nil) { - return [_asyncDelegate collectionNode:self.collectionNode shouldShowMenuForItemAtIndexPath:indexPath]; + return [_asyncDelegate collectionNode:collectionNode shouldShowMenuForItemAtIndexPath:indexPath]; } } else if (_asyncDelegateFlags.collectionViewShouldShowMenuForItem) { #pragma clang diagnostic push @@ -1013,9 +1056,10 @@ static NSString * const kCellReuseIdentifier = @"_ASCollectionViewCell"; - (BOOL)collectionView:(UICollectionView *)collectionView canPerformAction:(nonnull SEL)action forItemAtIndexPath:(nonnull NSIndexPath *)indexPath withSender:(nullable id)sender { if (_asyncDelegateFlags.collectionNodeCanPerformActionForItem) { + GET_COLLECTIONNODE_OR_RETURN(collectionNode, NO); indexPath = [self convertIndexPathToCollectionNode:indexPath]; if (indexPath != nil) { - return [_asyncDelegate collectionNode:self.collectionNode canPerformAction:action forItemAtIndexPath:indexPath sender:sender]; + return [_asyncDelegate collectionNode:collectionNode canPerformAction:action forItemAtIndexPath:indexPath sender:sender]; } } else if (_asyncDelegateFlags.collectionViewCanPerformActionForItem) { #pragma clang diagnostic push @@ -1029,9 +1073,10 @@ static NSString * const kCellReuseIdentifier = @"_ASCollectionViewCell"; - (void)collectionView:(UICollectionView *)collectionView performAction:(nonnull SEL)action forItemAtIndexPath:(nonnull NSIndexPath *)indexPath withSender:(nullable id)sender { if (_asyncDelegateFlags.collectionNodePerformActionForItem) { + GET_COLLECTIONNODE_OR_RETURN(collectionNode, (void)0); indexPath = [self convertIndexPathToCollectionNode:indexPath]; if (indexPath != nil) { - [_asyncDelegate collectionNode:self.collectionNode performAction:action forItemAtIndexPath:indexPath sender:sender]; + [_asyncDelegate collectionNode:collectionNode performAction:action forItemAtIndexPath:indexPath sender:sender]; } } else if (_asyncDelegateFlags.collectionViewPerformActionForItem) { #pragma clang diagnostic push @@ -1047,6 +1092,7 @@ static NSString * const kCellReuseIdentifier = @"_ASCollectionViewCell"; ASInterfaceState interfaceState = [self interfaceStateForRangeController:_rangeController]; if (ASInterfaceStateIncludesVisible(interfaceState)) { [_rangeController updateCurrentRangeWithMode:ASLayoutRangeModeFull]; + [self _checkForBatchFetching]; } for (_ASCollectionViewCell *collectionCell in _cellsForVisibilityUpdates) { @@ -1070,7 +1116,7 @@ static NSString * const kCellReuseIdentifier = @"_ASCollectionViewCell"; if (targetContentOffset != NULL) { ASDisplayNodeAssert(_batchContext != nil, @"Batch context should exist"); - [self _beginBatchFetchingIfNeededWithScrollView:self forScrollDirection:[self scrollDirection] contentOffset:*targetContentOffset]; + [self _beginBatchFetchingIfNeededWithContentOffset:*targetContentOffset]; } if (_asyncDelegateFlags.scrollViewWillEndDragging) { @@ -1196,7 +1242,8 @@ static NSString * const kCellReuseIdentifier = @"_ASCollectionViewCell"; // if the delegate does not respond to this method, there is no point in starting to fetch BOOL canFetch = _asyncDelegateFlags.collectionNodeWillBeginBatchFetch || _asyncDelegateFlags.collectionViewWillBeginBatchFetch; if (canFetch && _asyncDelegateFlags.shouldBatchFetchForCollectionNode) { - return [_asyncDelegate shouldBatchFetchForCollectionNode:self.collectionNode]; + GET_COLLECTIONNODE_OR_RETURN(collectionNode, NO); + return [_asyncDelegate shouldBatchFetchForCollectionNode:collectionNode]; } else if (canFetch && _asyncDelegateFlags.shouldBatchFetchForCollectionView) { #pragma clang diagnostic push #pragma clang diagnostic ignored "-Wdeprecated-declarations" @@ -1210,9 +1257,10 @@ static NSString * const kCellReuseIdentifier = @"_ASCollectionViewCell"; - (void)_scheduleCheckForBatchFetchingForNumberOfChanges:(NSUInteger)changes { // Prevent fetching will continually trigger in a loop after reaching end of content and no new content was provided - if (changes == 0) { + if (changes == 0 && _hasEverCheckedForBatchFetchingDueToUpdate) { return; } + _hasEverCheckedForBatchFetchingDueToUpdate = YES; // Push this to the next runloop to be sure the scroll view has the right content size dispatch_async(dispatch_get_main_queue(), ^{ @@ -1227,12 +1275,12 @@ static NSString * const kCellReuseIdentifier = @"_ASCollectionViewCell"; return; } - [self _beginBatchFetchingIfNeededWithScrollView:self forScrollDirection:[self scrollableDirections] contentOffset:self.contentOffset]; + [self _beginBatchFetchingIfNeededWithContentOffset:self.contentOffset]; } -- (void)_beginBatchFetchingIfNeededWithScrollView:(UIScrollView *)scrollView forScrollDirection:(ASScrollDirection)scrollDirection contentOffset:(CGPoint)contentOffset +- (void)_beginBatchFetchingIfNeededWithContentOffset:(CGPoint)contentOffset { - if (ASDisplayShouldFetchBatchForScrollView(self, scrollDirection, contentOffset)) { + if (ASDisplayShouldFetchBatchForScrollView(self, self.scrollDirection, self.scrollableDirections, contentOffset)) { [self _beginBatchFetching]; } } @@ -1242,7 +1290,8 @@ static NSString * const kCellReuseIdentifier = @"_ASCollectionViewCell"; [_batchContext beginBatchFetching]; if (_asyncDelegateFlags.collectionNodeWillBeginBatchFetch) { dispatch_async(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0), ^{ - [_asyncDelegate collectionNode:self.collectionNode willBeginBatchFetchWithContext:_batchContext]; + GET_COLLECTIONNODE_OR_RETURN(collectionNode, (void)0); + [_asyncDelegate collectionNode:collectionNode willBeginBatchFetchWithContext:_batchContext]; }); } else if (_asyncDelegateFlags.collectionViewWillBeginBatchFetch) { dispatch_async(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0), ^{ @@ -1261,9 +1310,11 @@ static NSString * const kCellReuseIdentifier = @"_ASCollectionViewCell"; ASCellNodeBlock block = nil; if (_asyncDataSourceFlags.collectionNodeNodeBlockForItem) { - block = [_asyncDataSource collectionNode:self.collectionNode nodeBlockForItemAtIndexPath:indexPath]; + GET_COLLECTIONNODE_OR_RETURN(collectionNode, ^{ return [[ASCellNode alloc] init]; }); + block = [_asyncDataSource collectionNode:collectionNode nodeBlockForItemAtIndexPath:indexPath]; } else if (_asyncDataSourceFlags.collectionNodeNodeForItem) { - ASCellNode *node = [_asyncDataSource collectionNode:self.collectionNode nodeForItemAtIndexPath:indexPath]; + GET_COLLECTIONNODE_OR_RETURN(collectionNode, ^{ return [[ASCellNode alloc] init]; }); + ASCellNode *node = [_asyncDataSource collectionNode:collectionNode nodeForItemAtIndexPath:indexPath]; if ([node isKindOfClass:[ASCellNode class]]) { block = ^{ return node; @@ -1317,7 +1368,8 @@ static NSString * const kCellReuseIdentifier = @"_ASCollectionViewCell"; - (NSUInteger)dataController:(ASDataController *)dataController rowsInSection:(NSUInteger)section { if (_asyncDataSourceFlags.collectionNodeNumberOfItemsInSection) { - return [_asyncDataSource collectionNode:self.collectionNode numberOfItemsInSection:section]; + GET_COLLECTIONNODE_OR_RETURN(collectionNode, 0); + return [_asyncDataSource collectionNode:collectionNode numberOfItemsInSection:section]; } else if (_asyncDataSourceFlags.collectionViewNumberOfItemsInSection) { #pragma clang diagnostic push #pragma clang diagnostic ignored "-Wdeprecated-declarations" @@ -1330,7 +1382,8 @@ static NSString * const kCellReuseIdentifier = @"_ASCollectionViewCell"; - (NSUInteger)numberOfSectionsInDataController:(ASDataController *)dataController { if (_asyncDataSourceFlags.numberOfSectionsInCollectionNode) { - return [_asyncDataSource numberOfSectionsInCollectionNode:self.collectionNode]; + GET_COLLECTIONNODE_OR_RETURN(collectionNode, 0); + return [_asyncDataSource numberOfSectionsInCollectionNode:collectionNode]; } else if (_asyncDataSourceFlags.numberOfSectionsInCollectionView) { #pragma clang diagnostic push #pragma clang diagnostic ignored "-Wdeprecated-declarations" @@ -1352,7 +1405,8 @@ static NSString * const kCellReuseIdentifier = @"_ASCollectionViewCell"; { ASCellNode *node = nil; if (_asyncDataSourceFlags.collectionNodeNodeForSupplementaryElement) { - node = [_asyncDataSource collectionNode:self.collectionNode nodeForSupplementaryElementOfKind:kind atIndexPath:indexPath]; + GET_COLLECTIONNODE_OR_RETURN(collectionNode, [[ASCellNode alloc] init] ); + node = [_asyncDataSource collectionNode:collectionNode nodeForSupplementaryElementOfKind:kind atIndexPath:indexPath]; } else if (_asyncDataSourceFlags.collectionViewNodeForSupplementaryElement) { #pragma clang diagnostic push #pragma clang diagnostic ignored "-Wdeprecated-declarations" @@ -1389,7 +1443,8 @@ static NSString * const kCellReuseIdentifier = @"_ASCollectionViewCell"; id context = nil; if (_asyncDataSourceFlags.collectionNodeContextForSection) { - context = [_asyncDataSource collectionNode:self.collectionNode contextForSection:section]; + GET_COLLECTIONNODE_OR_RETURN(collectionNode, nil); + context = [_asyncDataSource collectionNode:collectionNode contextForSection:section]; } if (context != nil) { @@ -1480,11 +1535,6 @@ static NSString * const kCellReuseIdentifier = @"_ASCollectionViewCell"; _performingBatchUpdates = NO; } -- (void)didCompleteUpdatesInRangeController:(ASRangeController *)rangeController -{ - [self _checkForBatchFetching]; -} - - (void)rangeController:(ASRangeController *)rangeController didInsertNodes:(NSArray *)nodes atIndexPaths:(NSArray *)indexPaths withAnimationOptions:(ASDataControllerAnimationOptions)animationOptions { ASDisplayNodeAssertMainThread(); @@ -1634,18 +1684,6 @@ static NSString * const kCellReuseIdentifier = @"_ASCollectionViewCell"; _nextLayoutInvalidationStyle = invalidationStyle; } -#pragma mark - Memory Management - -- (void)clearContents -{ - [_rangeController clearContents]; -} - -- (void)clearFetchedData -{ - [_rangeController clearFetchedData]; -} - #pragma mark - _ASDisplayView behavior substitutions // Need these to drive interfaceState so we know when we are visible, if not nested in another range-managing element. // Because our superclass is a true UIKit class, we cannot also subclass _ASDisplayView. diff --git a/AsyncDisplayKit/ASDisplayNode+Beta.h b/AsyncDisplayKit/ASDisplayNode+Beta.h index e0a0e2696a..7e2f8d569b 100644 --- a/AsyncDisplayKit/ASDisplayNode+Beta.h +++ b/AsyncDisplayKit/ASDisplayNode+Beta.h @@ -58,8 +58,8 @@ typedef struct { * * This property defaults to NO. It will be removed in a future release. */ -+ (BOOL)suppressesInvalidCollectionUpdateExceptions AS_WARN_UNUSED_RESULT; -+ (void)setSuppressesInvalidCollectionUpdateExceptions:(BOOL)suppresses; ++ (BOOL)suppressesInvalidCollectionUpdateExceptions AS_WARN_UNUSED_RESULT ASDISPLAYNODE_DEPRECATED_MSG("Collection update exceptions are thrown if assertions are enabled."); ++ (void)setSuppressesInvalidCollectionUpdateExceptions:(BOOL)suppresses ASDISPLAYNODE_DEPRECATED_MSG("Collection update exceptions are thrown if assertions are enabled.");; /** * @abstract Recursively ensures node and all subnodes are displayed. diff --git a/AsyncDisplayKit/ASDisplayNode+Deprecated.h b/AsyncDisplayKit/ASDisplayNode+Deprecated.h index ec7836d175..a55fbf86a6 100644 --- a/AsyncDisplayKit/ASDisplayNode+Deprecated.h +++ b/AsyncDisplayKit/ASDisplayNode+Deprecated.h @@ -115,4 +115,21 @@ ASLayoutElementStyleForwardingDeclaration */ @property (nonatomic, assign) BOOL usesImplicitHierarchyManagement ASDISPLAYNODE_DEPRECATED_MSG("Set .automaticallyManagesSubnodes instead."); +/** + * @abstract Indicates that the node should fetch any external data, such as images. + * + * @discussion Subclasses may override this method to be notified when they should begin to preload. Fetching + * should be done asynchronously. The node is also responsible for managing the memory of any data. + * The data may be remote and accessed via the network, but could also be a local database query. + */ +- (void)fetchData ASDISPLAYNODE_REQUIRES_SUPER ASDISPLAYNODE_DEPRECATED_MSG("Use -didEnterPreloadState instead."); + +/** + * Provides an opportunity to clear any fetched data (e.g. remote / network or database-queried) on the current node. + * + * @discussion This will not clear data recursively for all subnodes. Either call -recursivelyClearPreloadedData or + * selectively clear fetched data. + */ +- (void)clearFetchedData ASDISPLAYNODE_REQUIRES_SUPER ASDISPLAYNODE_DEPRECATED_MSG("Use -didExitPreloadState instead."); + @end diff --git a/AsyncDisplayKit/ASDisplayNode+Subclasses.h b/AsyncDisplayKit/ASDisplayNode+Subclasses.h index f6ed7b6f47..51ecd2d369 100644 --- a/AsyncDisplayKit/ASDisplayNode+Subclasses.h +++ b/AsyncDisplayKit/ASDisplayNode+Subclasses.h @@ -195,7 +195,7 @@ NS_ASSUME_NONNULL_BEGIN * @note Called on the display queue and/or main queue (MUST BE THREAD SAFE) */ + (void)drawRect:(CGRect)bounds withParameters:(nullable id )parameters - isCancelled:(asdisplaynode_iscancelled_block_t)isCancelledBlock + isCancelled:(__attribute((noescape)) asdisplaynode_iscancelled_block_t)isCancelledBlock isRasterizing:(BOOL)isRasterizing; /** @@ -212,7 +212,7 @@ NS_ASSUME_NONNULL_BEGIN * @note Called on the display queue and/or main queue (MUST BE THREAD SAFE) */ + (nullable UIImage *)displayWithParameters:(nullable id)parameters - isCancelled:(asdisplaynode_iscancelled_block_t)isCancelledBlock; + isCancelled:(__attribute((noescape)) asdisplaynode_iscancelled_block_t)isCancelledBlock; /** * @abstract Delegate override for drawParameters @@ -323,23 +323,6 @@ NS_ASSUME_NONNULL_BEGIN */ @property (nonatomic, readonly, assign, getter=isInHierarchy) BOOL inHierarchy; -/** - * @abstract Indicates that the node should fetch any external data, such as images. - * - * @discussion Subclasses may override this method to be notified when they should begin to fetch data. Fetching - * should be done asynchronously. The node is also responsible for managing the memory of any data. - * The data may be remote and accessed via the network, but could also be a local database query. - */ -- (void)fetchData ASDISPLAYNODE_REQUIRES_SUPER; - -/** - * Provides an opportunity to clear any fetched data (e.g. remote / network or database-queried) on the current node. - * - * @discussion This will not clear data recursively for all subnodes. Either call -recursivelyClearFetchedData or - * selectively clear fetched data. - */ -- (void)clearFetchedData ASDISPLAYNODE_REQUIRES_SUPER; - /** * Provides an opportunity to clear backing store and other memory-intensive intermediates, such as text layout managers * on the current node. diff --git a/AsyncDisplayKit/ASDisplayNode.h b/AsyncDisplayKit/ASDisplayNode.h index 97a7b8c206..940e2f74fa 100644 --- a/AsyncDisplayKit/ASDisplayNode.h +++ b/AsyncDisplayKit/ASDisplayNode.h @@ -478,31 +478,6 @@ extern NSInteger const ASDefaultDrawingPriority; */ - (void)recursivelyClearContents; -/** - * @abstract Calls -clearFetchedData on the receiver and its subnode hierarchy. - * - * @discussion Clears any memory-intensive fetched content. - * This method is used to notify the node that it should purge any content that is both expensive to fetch and to - * retain in memory. - * - * @see [ASDisplayNode(Subclassing) clearFetchedData] and [ASDisplayNode(Subclassing) fetchData] - */ -- (void)recursivelyClearFetchedData; - -/** - * @abstract Calls -fetchData on the receiver and its subnode hierarchy. - * - * @discussion Fetches content from remote sources for the current node and all subnodes. - * - * @see [ASDisplayNode(Subclassing) fetchData] and [ASDisplayNode(Subclassing) clearFetchedData] - */ -- (void)recursivelyFetchData; - -/** - * @abstract Triggers a recursive call to fetchData when the node has an interfaceState of ASInterfaceStatePreload - */ -- (void)setNeedsDataFetch; - /** * @abstract Toggle displaying a placeholder over the node that covers content until the node and all subnodes are * displayed. @@ -636,11 +611,11 @@ extern NSInteger const ASDefaultDrawingPriority; /** * Marks the node as needing layout. Convenience for use whether the view / layer is loaded or not. Safe to call from a background thread. - * - * If this node was measured, calling this method triggers an internal relayout: the calculated layout is invalidated, - * and the supernode is notified or (if this node is the root one) a full measurement pass is executed using the old constrained size. * - * Note: ASCellNode has special behavior in that calling this method will automatically notify + * If the node determines its own desired layout size will change in the next layout pass, it will propagate this + * information up the tree so its parents can have a chance to consider and apply if necessary the new size onto the node. + * + * Note: ASCellNode has special behavior in that calling this method will automatically notify * the containing ASTableView / ASCollectionView that the cell should be resized, if necessary. */ - (void)setNeedsLayout; @@ -795,7 +770,7 @@ extern NSInteger const ASDefaultDrawingPriority; /** - * @abstract Invalidates the current layout and begins a relayout of the node with the current `constrainedSize`. Must be called on main thread. + * @abstract Invalidates the layout and begins a relayout of the node with the current `constrainedSize`. Must be called on main thread. * * @discussion It is called right after the measurement and before -animateLayoutTransition:. * diff --git a/AsyncDisplayKit/ASDisplayNode.mm b/AsyncDisplayKit/ASDisplayNode.mm index 2e9bf5b4f1..5f00e29867 100644 --- a/AsyncDisplayKit/ASDisplayNode.mm +++ b/AsyncDisplayKit/ASDisplayNode.mm @@ -184,6 +184,12 @@ static ASDisplayNodeMethodOverrides GetASDisplayNodeMethodOverrides(Class c) if (ASDisplayNodeSubclassOverridesSelector(c, @selector(layoutSpecThatFits:))) { overrides |= ASDisplayNodeMethodOverrideLayoutSpecThatFits; } + if (ASDisplayNodeSubclassOverridesSelector(c, @selector(fetchData))) { + overrides |= ASDisplayNodeMethodOverrideFetchData; + } + if (ASDisplayNodeSubclassOverridesSelector(c, @selector(clearFetchedData))) { + overrides |= ASDisplayNodeMethodOverrideClearFetchedData; + } return overrides; } @@ -211,7 +217,7 @@ static ASDisplayNodeMethodOverrides GetASDisplayNodeMethodOverrides(Class c) ASDisplayNodeAssert(!ASDisplayNodeSubclassOverridesSelector(self, @selector(layoutThatFits:)), @"Subclass %@ must not override layoutThatFits: method. Instead overwrite calculateLayoutThatFits:.", classString); ASDisplayNodeAssert(!ASDisplayNodeSubclassOverridesSelector(self, @selector(layoutThatFits:parentSize:)), @"Subclass %@ must not override layoutThatFits:parentSize method. Instead overwrite calculateLayoutThatFits:.", classString); ASDisplayNodeAssert(!ASDisplayNodeSubclassOverridesSelector(self, @selector(recursivelyClearContents)), @"Subclass %@ must not override recursivelyClearContents method.", classString); - ASDisplayNodeAssert(!ASDisplayNodeSubclassOverridesSelector(self, @selector(recursivelyClearFetchedData)), @"Subclass %@ must not override recursivelyClearFetchedData method.", classString); + ASDisplayNodeAssert(!ASDisplayNodeSubclassOverridesSelector(self, @selector(recursivelyClearPreloadedData)), @"Subclass %@ must not override recursivelyClearFetchedData method.", classString); } // Below we are pre-calculating values per-class and dynamically adding a method (_staticInitialize) to populate these values @@ -284,7 +290,7 @@ static ASDisplayNodeMethodOverrides GetASDisplayNodeMethodOverrides(Class c) andHandler:^(ASDisplayNode * _Nonnull dequeuedItem, BOOL isQueueDrained) { [dequeuedItem _recursivelyTriggerDisplayAndBlock:NO]; if (isQueueDrained) { - CFAbsoluteTime timestamp = CFAbsoluteTimeGetCurrent(); + CFTimeInterval timestamp = CACurrentMediaTime(); [[NSNotificationCenter defaultCenter] postNotificationName:ASRenderingEngineDidDisplayScheduledNodesNotification object:nil userInfo:@{ASRenderingEngineDidDisplayNodesScheduledBeforeTimestamp: @(timestamp)}]; @@ -315,10 +321,11 @@ static ASDisplayNodeMethodOverrides GetASDisplayNodeMethodOverrides(Class c) _environmentState = ASEnvironmentStateMakeDefault(); _calculatedDisplayNodeLayout = std::make_shared(); + _pendingDisplayNodeLayout = nullptr; _defaultLayoutTransitionDuration = 0.2; _defaultLayoutTransitionDelay = 0.0; - _defaultLayoutTransitionOptions = UIViewAnimationOptionBeginFromCurrentState; + _defaultLayoutTransitionOptions = UIViewAnimationOptionCurveEaseInOut | UIViewAnimationOptionTransitionNone; _flags.canClearContentsOfLayer = YES; _flags.canCallSetNeedsDisplayOfLayer = YES; @@ -456,23 +463,13 @@ static ASDisplayNodeMethodOverrides GetASDisplayNodeMethodOverrides(Class c) - (void)_scheduleIvarsForMainDeallocation { - /** - * UIKit components must be deallocated on the main thread. We use this shared - * run loop queue to gradually deallocate them across many turns of the main run loop. - */ - static ASRunLoopQueue *queue; - static dispatch_once_t onceToken; - dispatch_once(&onceToken, ^{ - queue = [[ASRunLoopQueue alloc] initWithRunLoop:CFRunLoopGetMain() andHandler:^(id _Nonnull dequeuedItem, BOOL isQueueDrained) { }]; - queue.batchSize = 10; - }); - NSValue *ivarsObj = [[self class] _ivarsThatMayNeedMainDeallocation]; // Unwrap the ivar array unsigned int count = 0; - int scanResult = sscanf(ivarsObj.objCType, "[%u^{objc_ivar}]", &count); - NSAssert(scanResult, @"Unexpected type in NSValue: %s", ivarsObj.objCType); + // Will be unused if assertions are disabled. + __unused int scanResult = sscanf(ivarsObj.objCType, "[%u^{objc_ivar}]", &count); + ASDisplayNodeAssert(scanResult == 1, @"Unexpected type in NSValue: %s", ivarsObj.objCType); Ivar ivars[count]; [ivarsObj getValue:ivars]; @@ -480,7 +477,7 @@ static ASDisplayNodeMethodOverrides GetASDisplayNodeMethodOverrides(Class c) id value = object_getIvar(self, ivar); if (ASClassRequiresMainThreadDeallocation(object_getClass(value))) { LOG(@"Trampolining ivar '%s' value %@ for main deallocation.", ivar_getName(ivar), value); - [queue enqueue:value]; + ASPerformMainThreadDeallocation(value); } else { LOG(@"Not trampolining ivar '%s' value %@.", ivar_getName(ivar), value); } @@ -517,8 +514,9 @@ static ASDisplayNodeMethodOverrides GetASDisplayNodeMethodOverrides(Class c) NSValue *ivarsObj = [c _ivarsThatMayNeedMainDeallocation]; // Unwrap the ivar array and append it to our working array unsigned int count = 0; - int scanResult = sscanf(ivarsObj.objCType, "[%u^{objc_ivar}]", &count); - NSAssert(scanResult, @"Unexpected type in NSValue: %s", ivarsObj.objCType); + // Will be unused if assertions are disabled. + __unused int scanResult = sscanf(ivarsObj.objCType, "[%u^{objc_ivar}]", &count); + ASDisplayNodeAssert(scanResult == 1, @"Unexpected type in NSValue: %s", ivarsObj.objCType); ASDisplayNodeCAssert(resultCount + count < kMaxDealloc2MainIvarsPerClassTree, @"More than %d dealloc2main ivars are not supported. Count: %d", kMaxDealloc2MainIvarsPerClassTree, resultCount + count); [ivarsObj getValue:resultIvars + resultCount]; resultCount += count; @@ -810,6 +808,70 @@ static ASDisplayNodeMethodOverrides GetASDisplayNodeMethodOverrides(Class c) #pragma mark - Layout +- (void)setNeedsLayoutFromAbove +{ + ASDisplayNodeAssertThreadAffinity(self); + + __instanceLock__.lock(); + + // Mark the node for layout in the next layout pass + [self setNeedsLayout]; + + // Escalate to the root; entire tree must allow adjustments so the layout fits the new child. + // Much of the layout will be re-used as cached (e.g. other items in an unconstrained stack) + ASDisplayNode *supernode = _supernode; + if (supernode) { + // Threading model requires that we unlock before calling a method on our parent. + __instanceLock__.unlock(); + [supernode setNeedsLayoutFromAbove]; + return; + } + + // We are the root node and need to re-flow the layout; at least one child needs a new size. + CGSize boundsSizeForLayout = ASCeilSizeValues(self.bounds.size); + + // Figure out constrainedSize to use + ASSizeRange constrainedSize = ASSizeRangeMake(boundsSizeForLayout); + if (_pendingDisplayNodeLayout != nullptr) { + constrainedSize = _pendingDisplayNodeLayout->constrainedSize; + } else if (_calculatedDisplayNodeLayout->layout != nil) { + constrainedSize = _calculatedDisplayNodeLayout->constrainedSize; + } + + // Perform a measurement pass to get the full tree layout, adapting to the child's new size. + ASLayout *layout = [self layoutThatFits:constrainedSize]; + + // Check if the returned layout has a different size than our current bounds. + if (CGSizeEqualToSize(boundsSizeForLayout, layout.size) == NO) { + // If so, inform our container we need an update (e.g Table, Collection, ViewController, etc). + [self _locked_displayNodeDidInvalidateSizeNewSize:layout.size]; + } + + __instanceLock__.unlock(); +} + +- (void)_locked_displayNodeDidInvalidateSizeNewSize:(CGSize)size +{ + ASDisplayNodeAssertThreadAffinity(self); + + // The default implementation of display node changes the size of itself to the new size + CGRect oldBounds = self.bounds; + CGSize oldSize = oldBounds.size; + CGSize newSize = size; + + if (! CGSizeEqualToSize(oldSize, newSize)) { + self.bounds = (CGRect){ oldBounds.origin, newSize }; + + // Frame's origin must be preserved. Since it is computed from bounds size, anchorPoint + // and position (see frame setter in ASDisplayNode+UIViewBridge), position needs to be adjusted. + CGPoint anchorPoint = self.anchorPoint; + CGPoint oldPosition = self.position; + CGFloat xDelta = (newSize.width - oldSize.width) * anchorPoint.x; + CGFloat yDelta = (newSize.height - oldSize.height) * anchorPoint.y; + self.position = CGPointMake(oldPosition.x + xDelta, oldPosition.y + yDelta); + } +} + - (ASLayout *)layoutThatFits:(ASSizeRange)constrainedSize { #pragma clang diagnostic push @@ -821,75 +883,28 @@ static ASDisplayNodeMethodOverrides GetASDisplayNodeMethodOverrides(Class c) - (ASLayout *)layoutThatFits:(ASSizeRange)constrainedSize parentSize:(CGSize)parentSize { ASDN::MutexLocker l(__instanceLock__); + + // If one or multiple layout transitions are in flight it still can happen that layout information is requested + // on other threads. As the pending and calculated layout to be updated in the layout transition in here just a + // layout calculation wil be performed without side effect + if ([self _isLayoutTransitionInvalid]) { + return [self calculateLayoutThatFits:constrainedSize restrictedToSize:self.style.size relativeToParentSize:parentSize]; + } - if ([self shouldCalculateLayoutWithConstrainedSize:constrainedSize parentSize:parentSize] == NO) { - ASDisplayNodeAssertNotNil(_calculatedDisplayNodeLayout->layout, @"-[ASDisplayNode layoutThatFits:parentSize:] _layout should not be nil! %@", self); - return _calculatedDisplayNodeLayout->layout ? : [ASLayout layoutWithLayoutElement:self size:{0, 0}]; + if (_calculatedDisplayNodeLayout->isValidForConstrainedSizeParentSize(constrainedSize, parentSize)) { + ASDisplayNodeAssertNotNil(_calculatedDisplayNodeLayout->layout, @"-[ASDisplayNode layoutThatFits:parentSize:] _calculatedDisplayNodeLayout->layout should not be nil! %@", self); + return _calculatedDisplayNodeLayout->layout ?: [ASLayout layoutWithLayoutElement:self size:{0, 0}]; } - [self cancelLayoutTransition]; - - BOOL didCreateNewContext = NO; - BOOL didOverrideExistingContext = NO; - BOOL shouldVisualizeLayout = ASHierarchyStateIncludesVisualizeLayout(_hierarchyState); - ASLayoutElementContext context; - if (ASLayoutElementContextIsNull(ASLayoutElementGetCurrentContext())) { - context = ASLayoutElementContextMake(ASLayoutElementContextDefaultTransitionID, shouldVisualizeLayout); - ASLayoutElementSetCurrentContext(context); - didCreateNewContext = YES; - } else { - context = ASLayoutElementGetCurrentContext(); - if (context.needsVisualizeNode != shouldVisualizeLayout) { - context.needsVisualizeNode = shouldVisualizeLayout; - ASLayoutElementSetCurrentContext(context); - didOverrideExistingContext = YES; - } - } - - // Prepare for layout transition - auto previousLayout = _calculatedDisplayNodeLayout; - auto pendingLayout = std::make_shared( + // Creat a pending display node layout for the layout pass + _pendingDisplayNodeLayout = std::make_shared( [self calculateLayoutThatFits:constrainedSize restrictedToSize:self.style.size relativeToParentSize:parentSize], constrainedSize, parentSize ); - if (didCreateNewContext) { - ASLayoutElementClearCurrentContext(); - } else if (didOverrideExistingContext) { - context.needsVisualizeNode = !context.needsVisualizeNode; - ASLayoutElementSetCurrentContext(context); - } - - _pendingLayoutTransition = [[ASLayoutTransition alloc] initWithNode:self - pendingLayout:pendingLayout - previousLayout:previousLayout]; - - // Only complete the pending layout transition if the node is not a subnode of a node that is currently - // in a layout transition - if (ASHierarchyStateIncludesLayoutPending(_hierarchyState) == NO) { - // Complete the pending layout transition immediately - [self _completePendingLayoutTransition]; - } - - ASDisplayNodeAssertNotNil(pendingLayout->layout, @"-[ASDisplayNode layoutThatFits:parentSize:] newLayout should not be nil! %@", self); - return pendingLayout->layout; -} - -- (BOOL)shouldCalculateLayoutWithConstrainedSize:(ASSizeRange)constrainedSize parentSize:(CGSize)parentSize -{ - ASDN::MutexLocker l(__instanceLock__); - - // Don't remeasure if in layout pending state and a new transition already started - if (ASHierarchyStateIncludesLayoutPending(_hierarchyState)) { - ASLayoutElementContext context = ASLayoutElementGetCurrentContext(); - if (ASLayoutElementContextIsNull(context) || _pendingTransitionID != context.transitionID) { - return NO; - } - } - - // Check if display node layout is still valid - return _calculatedDisplayNodeLayout->isValidForConstrainedSizeParentSize(constrainedSize, parentSize) == NO; + ASDisplayNodeAssertNotNil(_pendingDisplayNodeLayout->layout, @"-[ASDisplayNode layoutThatFits:parentSize:] _pendingDisplayNodeLayout->layout should not be nil! %@", self); + return _pendingDisplayNodeLayout->layout ?: [ASLayout layoutWithLayoutElement:self size:{0, 0}]; } - (ASLayoutElementType)layoutElementType @@ -922,14 +937,11 @@ static ASDisplayNodeMethodOverrides GetASDisplayNodeMethodOverrides(Class c) shouldMeasureAsync:(BOOL)shouldMeasureAsync measurementCompletion:(void(^)())completion { - if (_calculatedDisplayNodeLayout->layout == nil) { - // No measure pass happened before, it's not possible to reuse the constrained size for the transition - // Using CGSizeZero for the sizeRange can cause negative values in client layout code. - return; - } + ASDisplayNodeAssertMainThread(); + + [self setNeedsLayout]; - [self invalidateCalculatedLayout]; - [self transitionLayoutWithSizeRange:_calculatedDisplayNodeLayout->constrainedSize + [self transitionLayoutWithSizeRange:[self _locked_constrainedSizeForLayoutPass] animated:animated shouldMeasureAsync:shouldMeasureAsync measurementCompletion:completion]; @@ -941,9 +953,17 @@ static ASDisplayNodeMethodOverrides GetASDisplayNodeMethodOverrides(Class c) shouldMeasureAsync:(BOOL)shouldMeasureAsync measurementCompletion:(void(^)())completion { - // Passed constrainedSize is the the same as the node's current constrained size it's a noop ASDisplayNodeAssertMainThread(); - if ([self shouldCalculateLayoutWithConstrainedSize:constrainedSize parentSize:constrainedSize.max] == NO) { + + if (constrainedSize.max.width <= 0.0 || constrainedSize.max.height <= 0.0) { + // Using CGSizeZero for the sizeRange can cause negative values in client layout code. + // Most likely called transitionLayout: without providing a size, before first layout pass. + return; + } + + // Check if we are a subnode in a layout transition. + // In this case no measurement is needed as we're part of the layout transition. + if ([self _isLayoutTransitionInvalid]) { return; } @@ -951,21 +971,24 @@ static ASDisplayNodeMethodOverrides GetASDisplayNodeMethodOverrides(Class c) ASDN::MutexLocker l(__instanceLock__); ASDisplayNodeAssert(ASHierarchyStateIncludesLayoutPending(_hierarchyState) == NO, @"Can't start a transition when one of the supernodes is performing one."); } - + + // Every new layout transition has a transition id associated to check in subsequent transitions for cancelling int32_t transitionID = [self _startNewTransition]; - // Move all subnodes in a pending state + // Move all subnodes in layout pending state for this transition ASDisplayNodePerformBlockOnEverySubnode(self, NO, ^(ASDisplayNode * _Nonnull node) { ASDisplayNodeAssert([node _isTransitionInProgress] == NO, @"Can't start a transition when one of the subnodes is performing one."); node.hierarchyState |= ASHierarchyStateLayoutPending; node.pendingTransitionID = transitionID; }); + // Transition block that executes the layout transition void (^transitionBlock)(void) = ^{ if ([self _shouldAbortTransitionWithID:transitionID]) { return; } + // Perform a full layout creation pass with passed in constrained size to create the new layout for the transition ASLayout *newLayout; { ASDN::MutexLocker l(__instanceLock__); @@ -998,7 +1021,7 @@ static ASDisplayNodeMethodOverrides GetASDisplayNodeMethodOverrides(Class c) return; } - // Update display node layout + // Update calculated layout auto previousLayout = _calculatedDisplayNodeLayout; auto pendingLayout = std::make_shared( newLayout, @@ -1012,8 +1035,6 @@ static ASDisplayNodeMethodOverrides GetASDisplayNodeMethodOverrides(Class c) [node _completePendingLayoutTransition]; node.hierarchyState &= (~ASHierarchyStateLayoutPending); }); - - [self _finishOrCancelTransition]; // Measurement pass completion if (completion) { @@ -1034,9 +1055,13 @@ static ASDisplayNodeMethodOverrides GetASDisplayNodeMethodOverrides(Class c) // Kick off animating the layout transition [self animateLayoutTransition:_pendingLayoutTransitionContext]; + + // Mark transaction as finished + [self _finishOrCancelTransition]; }); }; + // Start transition based on flag on current or background thread if (shouldMeasureAsync) { ASPerformBlockOnBackgroundThread(transitionBlock); } else { @@ -1064,6 +1089,18 @@ static ASDisplayNodeMethodOverrides GetASDisplayNodeMethodOverrides(Class c) return _transitionInProgress; } +- (BOOL)_isLayoutTransitionInvalid +{ + ASDN::MutexLocker l(__instanceLock__); + if (ASHierarchyStateIncludesLayoutPending(_hierarchyState)) { + ASLayoutElementContext context = ASLayoutElementGetCurrentContext(); + if (ASLayoutElementContextIsNull(context) || _pendingTransitionID != context.transitionID) { + return YES; + } + } + return NO; +} + /// Starts a new transition and returns the transition id - (int32_t)_startNewTransition { @@ -1276,21 +1313,27 @@ static ASDisplayNodeMethodOverrides GetASDisplayNodeMethodOverrides(Class c) // We generate placeholders at measureWithSizeRange: time so that a node is guaranteed to have a placeholder ready to go. // This is also because measurement is usually asynchronous, but placeholders need to be set up synchronously. // First measurement is guaranteed to be before the node is onscreen, so we can create the image async. but still have it appear sync. - if (_placeholderEnabled && [self _displaysAsynchronously]) { + if (_placeholderEnabled && !_placeholderImage && [self _displaysAsynchronously]) { // Zero-sized nodes do not require a placeholder. ASLayout *layout = _calculatedDisplayNodeLayout->layout; CGSize layoutSize = (layout ? layout.size : CGSizeZero); - if (CGSizeEqualToSize(layoutSize, CGSizeZero)) { + if (layoutSize.width * layoutSize.height <= 0.0) { return; } - - if (!_placeholderImage) { + + // If we've displayed our contents, we don't need a placeholder. + // Contents is a thread-affined property and can't be read off main after loading. + if (self.isNodeLoaded) { ASPerformBlockOnMainThread(^{ if (self.contents == nil) { _placeholderImage = [self placeholderImage]; } }); + } else { + if (self.contents == nil) { + _placeholderImage = [self placeholderImage]; + } } } @@ -1494,51 +1537,23 @@ static ASDisplayNodeMethodOverrides GetASDisplayNodeMethodOverrides(Class c) [self displayImmediately]; } -//Calling this with the lock held can lead to deadlocks. Always call *unlocked* +- (void)invalidateCalculatedLayout +{ + ASDN::MutexLocker l(__instanceLock__); + + // This will cause the next layout pass to compute a new layout instead of returning + // the cached layout in case the constrained or parent size did not change + _calculatedDisplayNodeLayout->invalidate(); + if (_pendingDisplayNodeLayout != nullptr) { + _pendingDisplayNodeLayout->invalidate(); + } +} + - (void)__setNeedsLayout { - ASDisplayNodeAssertThreadAffinity(self); - - __instanceLock__.lock(); - - if (_calculatedDisplayNodeLayout->layout == nil) { - // Can't proceed without a layout as no constrained size would be available. If not layout exists at this moment - // no measurement pass did happen just bail out for now - __instanceLock__.unlock(); - return; - } - + ASDN::MutexLocker l(__instanceLock__); + [self invalidateCalculatedLayout]; - - if (_supernode) { - ASDisplayNode *supernode = _supernode; - __instanceLock__.unlock(); - // Cause supernode's layout to be invalidated - // We need to release the lock to prevent a deadlock - [supernode setNeedsLayout]; - return; - } - - // This is the root node. Trigger a full measurement pass on *current* thread. Old constrained size is re-used. - [self layoutThatFits:_calculatedDisplayNodeLayout->constrainedSize]; - - CGRect oldBounds = self.bounds; - CGSize oldSize = oldBounds.size; - CGSize newSize = _calculatedDisplayNodeLayout->layout.size; - - if (! CGSizeEqualToSize(oldSize, newSize)) { - self.bounds = (CGRect){ oldBounds.origin, newSize }; - - // Frame's origin must be preserved. Since it is computed from bounds size, anchorPoint - // and position (see frame setter in ASDisplayNode+UIViewBridge), position needs to be adjusted. - CGPoint anchorPoint = self.anchorPoint; - CGPoint oldPosition = self.position; - CGFloat xDelta = (newSize.width - oldSize.width) * anchorPoint.x; - CGFloat yDelta = (newSize.height - oldSize.height) * anchorPoint.y; - self.position = CGPointMake(oldPosition.x + xDelta, oldPosition.y + yDelta); - } - - __instanceLock__.unlock(); } - (void)__setNeedsDisplay @@ -1557,58 +1572,158 @@ static ASDisplayNodeMethodOverrides GetASDisplayNodeMethodOverrides(Class c) { ASDisplayNodeAssertMainThread(); ASDN::MutexLocker l(__instanceLock__); - CGRect bounds = self.bounds; - - [self measureNodeWithBoundsIfNecessary:bounds]; - + CGRect bounds = _threadSafeBounds; + if (CGRectEqualToRect(bounds, CGRectZero)) { // Performing layout on a zero-bounds view often results in frame calculations // with negative sizes after applying margins, which will cause // measureWithSizeRange: on subnodes to assert. + LOG(@"Warning: No size given for node before node was trying to layout itself: %@. Please provide a frame for the node.", self); return; } - // Handle placeholder layer creation in case the size of the node changed after the initial placeholder layer - // was created - if ([self _shouldHavePlaceholderLayer]) { - [self _setupPlaceholderLayerIfNeeded]; + // If a current layout transition is in progress there is no need to do a measurement and layout pass in here as + // this is supposed to happen within the layout transition process + if ([self _isTransitionInProgress]) { + return; } - _placeholderLayer.frame = bounds; + + // This method will confirm that the layout is up to date (and update if needed). + // Importantly, it will also APPLY the layout to all of our subnodes if (unless parent is transitioning). + [self _locked_measureNodeWithBoundsIfNecessary:bounds]; + _pendingDisplayNodeLayout = nullptr; + + [self _locked_layoutPlaceholderIfNecessary]; [self layout]; [self layoutDidFinish]; } -- (void)measureNodeWithBoundsIfNecessary:(CGRect)bounds +/// Needs to be called with lock held +- (void)_locked_measureNodeWithBoundsIfNecessary:(CGRect)bounds { - BOOL supportsRangeManagedInterfaceState = NO; - BOOL hasDirtyLayout = NO; - CGSize calculatedLayoutSize = CGSizeZero; - { - ASDN::MutexLocker l(__instanceLock__); - supportsRangeManagedInterfaceState = [self supportsRangeManagedInterfaceState]; - hasDirtyLayout = _calculatedDisplayNodeLayout->isDirty(); - calculatedLayoutSize = _calculatedDisplayNodeLayout->layout.size; + // Check if we are a subnode in a layout transition. + // In this case no measurement is needed as it's part of the layout transition + if ([self _isLayoutTransitionInvalid]) { + return; } - - // Check if it's a subnode in a layout transition. In this case no measurement is needed as it's part of - // the layout transition - if (ASHierarchyStateIncludesLayoutPending(_hierarchyState)) { - ASLayoutElementContext context = ASLayoutElementGetCurrentContext(); - if (ASLayoutElementContextIsNull(context) || _pendingTransitionID != context.transitionID) { + + CGSize boundsSizeForLayout = ASCeilSizeValues(bounds.size); + + // Prefer _pendingDisplayNodeLayout over _calculatedDisplayNodeLayout (if exists, it's the newest) + // If there is no _pending, check if _calculated is valid to reuse (avoiding recalculation below). + if (_pendingDisplayNodeLayout == nullptr) { + if (_calculatedDisplayNodeLayout->isDirty() == NO + && (_calculatedDisplayNodeLayout->requestedLayoutFromAbove == YES + || CGSizeEqualToSize(_calculatedDisplayNodeLayout->layout.size, boundsSizeForLayout))) { return; } } - // If no measure pass happened or the bounds changed between layout passes we manually trigger a measurement pass - // for the node using a size range equal to whatever bounds were provided to the node - if (supportsRangeManagedInterfaceState == NO && (hasDirtyLayout || CGSizeEqualToSize(calculatedLayoutSize, bounds.size) == NO)) { - if (CGRectEqualToRect(bounds, CGRectZero)) { - LOG(@"Warning: No size given for node before node was trying to layout itself: %@. Please provide a frame for the node.", self); - } else { - [self layoutThatFits:ASSizeRangeMake(bounds.size)]; + // _calculatedDisplayNodeLayout is not reusable we need to transition to a new one + [self cancelLayoutTransition]; + + BOOL didCreateNewContext = NO; + BOOL didOverrideExistingContext = NO; + BOOL shouldVisualizeLayout = ASHierarchyStateIncludesVisualizeLayout(_hierarchyState); + ASLayoutElementContext context = ASLayoutElementGetCurrentContext(); + if (ASLayoutElementContextIsNull(context)) { + context = ASLayoutElementContextMake(ASLayoutElementContextDefaultTransitionID, shouldVisualizeLayout); + ASLayoutElementSetCurrentContext(context); + didCreateNewContext = YES; + } else { + if (context.needsVisualizeNode != shouldVisualizeLayout) { + context.needsVisualizeNode = shouldVisualizeLayout; + ASLayoutElementSetCurrentContext(context); + didOverrideExistingContext = YES; } } + + // Figure out previous and pending layouts for layout transition + std::shared_ptr nextLayout = _pendingDisplayNodeLayout; + #define layoutSizeDifferentFromBounds !CGSizeEqualToSize(nextLayout->layout.size, boundsSizeForLayout) + + // nextLayout was likely created by a call to layoutThatFits:, check if is valid and can be applied. + // If our bounds size is different than it, or invalid, recalculate. Use #define to avoid nullptr-> + if (nextLayout == nullptr || nextLayout->isDirty() == YES || layoutSizeDifferentFromBounds) { + // Use the last known constrainedSize passed from a parent during layout (if never, use bounds). + ASSizeRange constrainedSize = [self _locked_constrainedSizeForLayoutPass]; + ASLayout *layout = [self calculateLayoutThatFits:constrainedSize + restrictedToSize:self.style.size + relativeToParentSize:boundsSizeForLayout]; + + nextLayout = std::make_shared(layout, constrainedSize, boundsSizeForLayout); + } + + if (didCreateNewContext) { + ASLayoutElementClearCurrentContext(); + } else if (didOverrideExistingContext) { + context.needsVisualizeNode = !context.needsVisualizeNode; + ASLayoutElementSetCurrentContext(context); + } + + // If our new layout's desired size for self doesn't match current size, ask our parent to update it. + // This can occur for either pre-calculated or newly-calculated layouts. + if (nextLayout->requestedLayoutFromAbove == NO + && CGSizeEqualToSize(boundsSizeForLayout, nextLayout->layout.size) == NO) { + // The layout that we have specifies that this node (self) would like to be a different size + // than it currently is. Because that size has been computed within the constrainedSize, we + // expect that calling setNeedsLayoutFromAbove will result in our parent resizing us to this. + // However, in some cases apps may manually interfere with this (setting a different bounds). + // In this case, we need to detect that we've already asked to be resized to match this + // particular ASLayout object, and shouldn't loop asking again unless we have a different ASLayout. + nextLayout->requestedLayoutFromAbove = YES; + [self setNeedsLayoutFromAbove]; + } + + // Prepare to transition to nextLayout + ASDisplayNodeAssertNotNil(nextLayout->layout, @"nextLayout->layout should not be nil! %@", self); + _pendingLayoutTransition = [[ASLayoutTransition alloc] initWithNode:self + pendingLayout:nextLayout + previousLayout:_calculatedDisplayNodeLayout]; + + // If a parent is currently executing a layout transition, perform our layout application after it. + if (ASHierarchyStateIncludesLayoutPending(_hierarchyState) == NO) { + // If no transition, apply our new layout immediately (common case). + [self _completePendingLayoutTransition]; + } +} + +- (ASSizeRange)_locked_constrainedSizeForLayoutPass +{ + // TODO: The logic in -setNeedsLayoutFromAbove seems correct and doesn't use this method. + // logic seems correct. For what case does -this method need to do the CGSizeEqual checks? + // IF WE CAN REMOVE BOUNDS CHECKS HERE, THEN WE CAN ALSO REMOVE "REQUESTED FROM ABOVE" CHECK + + CGSize boundsSizeForLayout = ASCeilSizeValues(self.threadSafeBounds.size); + + // Checkout if constrained size of pending or calculated display node layout can be used + if (_pendingDisplayNodeLayout != nullptr + && (_pendingDisplayNodeLayout->requestedLayoutFromAbove + || CGSizeEqualToSize(_pendingDisplayNodeLayout->layout.size, boundsSizeForLayout))) { + // We assume the size from the last returned layoutThatFits: layout was applied so use the pending display node + // layout constrained size + return _pendingDisplayNodeLayout->constrainedSize; + } else if (_calculatedDisplayNodeLayout->layout != nil + && (_calculatedDisplayNodeLayout->requestedLayoutFromAbove + || CGSizeEqualToSize(_calculatedDisplayNodeLayout->layout.size, boundsSizeForLayout))) { + // We assume the _calculatedDisplayNodeLayout is still valid and the frame is not different + return _calculatedDisplayNodeLayout->constrainedSize; + } else { + // In this case neither the _pendingDisplayNodeLayout or the _calculatedDisplayNodeLayout constrained size can + // be reused, so the current bounds is used. This is usual the case if a frame was set manually that differs to + // the one returned from layoutThatFits: or layoutThatFits: was never called + return ASSizeRangeMake(boundsSizeForLayout); + } +} + +- (void)_locked_layoutPlaceholderIfNecessary +{ + if ([self _shouldHavePlaceholderLayer]) { + [self _setupPlaceholderLayerIfNeeded]; + } + // Update the placeholderLayer size in case the node size has changed since the placeholder was added. + _placeholderLayer.frame = self.threadSafeBounds; } - (void)layoutDidFinish @@ -2698,12 +2813,18 @@ void recursivelyTriggerDisplayForLayer(CALayer *layer, BOOL shouldBlock) - (CGSize)calculatedSize { ASDN::MutexLocker l(__instanceLock__); + if (_pendingDisplayNodeLayout != nullptr) { + return _pendingDisplayNodeLayout->layout.size; + } return _calculatedDisplayNodeLayout->layout.size; } - (ASSizeRange)constrainedSizeForCalculatedLayout { ASDN::MutexLocker l(__instanceLock__); + if (_pendingDisplayNodeLayout != nullptr) { + return _pendingDisplayNodeLayout->constrainedSize; + } return _calculatedDisplayNodeLayout->constrainedSize; } @@ -2737,15 +2858,6 @@ void recursivelyTriggerDisplayForLayer(CALayer *layer, BOOL shouldBlock) return nil; } -- (void)invalidateCalculatedLayout -{ - ASDN::MutexLocker l(__instanceLock__); - - // This will cause the next call to -layoutThatFits:parentSize: to compute a new layout instead of returning - // the cached layout in case the constrained or parent size did not change - _calculatedDisplayNodeLayout->invalidate(); -} - - (void)__didLoad { ASDN::MutexLocker l(__instanceLock__); @@ -2824,34 +2936,24 @@ void recursivelyTriggerDisplayForLayer(CALayer *layer, BOOL shouldBlock) }); } -- (void)fetchData -{ - // subclass override -} - -- (void)setNeedsDataFetch +- (void)setNeedsPreload { if (self.isInPreloadState) { - [self recursivelyFetchData]; + [self recursivelyPreload]; } } -- (void)recursivelyFetchData +- (void)recursivelyPreload { ASDisplayNodePerformBlockOnEveryNode(nil, self, YES, ^(ASDisplayNode * _Nonnull node) { - [node fetchData]; + [node didEnterPreloadState]; }); } -- (void)clearFetchedData -{ - // subclass override -} - -- (void)recursivelyClearFetchedData +- (void)recursivelyClearPreloadedData { ASDisplayNodePerformBlockOnEveryNode(nil, self, YES, ^(ASDisplayNode * _Nonnull node) { - [node clearFetchedData]; + [node didExitPreloadState]; }); } @@ -2877,13 +2979,23 @@ void recursivelyTriggerDisplayForLayer(CALayer *layer, BOOL shouldBlock) - (void)didEnterPreloadState { - [self fetchData]; + if (_methodOverrides & ASDisplayNodeMethodOverrideFetchData) { +#pragma clang diagnostic push +#pragma clang diagnostic ignored "-Wdeprecated-declarations" + [self fetchData]; +#pragma clang diagnostic pop + } } - (void)didExitPreloadState { - if ([self supportsRangeManagedInterfaceState]) { + if (_methodOverrides & ASDisplayNodeMethodOverrideClearFetchedData) { + if ([self supportsRangeManagedInterfaceState]) { +#pragma clang diagnostic push +#pragma clang diagnostic ignored "-Wdeprecated-declarations" [self clearFetchedData]; +#pragma clang diagnostic pop + } } } @@ -2941,7 +3053,7 @@ void recursivelyTriggerDisplayForLayer(CALayer *layer, BOOL shouldBlock) // Trigger asynchronous measurement if it is not already cached or being calculated. } - // For the FetchData and Display ranges, we don't want to call -clear* if not being managed by a range controller. + // For the Preload and Display ranges, we don't want to call -clear* if not being managed by a range controller. // Otherwise we get flashing behavior from normal UIKit manipulations like navigation controller push / pop. // Still, the interfaceState should be updated to the current state of the node; just don't act on the transition. @@ -2956,7 +3068,7 @@ void recursivelyTriggerDisplayForLayer(CALayer *layer, BOOL shouldBlock) [self didExitPreloadState]; } } - + // Entered or exited contents rendering state. BOOL nowDisplay = ASInterfaceStateIncludesDisplay(newState); BOOL wasDisplay = ASInterfaceStateIncludesDisplay(oldState); @@ -3789,7 +3901,7 @@ static const char *ASDisplayNodeAssociatedNodeKey = "ASAssociatedNode"; { // Deprecated preferredFrameSize just calls through to set width and height self.style.preferredSize = preferredFrameSize; - [self invalidateCalculatedLayout]; + [self setNeedsLayout]; } - (CGSize)preferredFrameSize @@ -3842,6 +3954,16 @@ ASLayoutElementStyleForwarding } } +- (void)fetchData +{ + // subclass override +} + +- (void)clearFetchedData +{ + // subclass override +} + - (void)cancelLayoutTransitionsInProgress { [self cancelLayoutTransition]; diff --git a/AsyncDisplayKit/ASDisplayNodeExtras.h b/AsyncDisplayKit/ASDisplayNodeExtras.h index f7e0a61ba0..e7cbf30192 100644 --- a/AsyncDisplayKit/ASDisplayNodeExtras.h +++ b/AsyncDisplayKit/ASDisplayNodeExtras.h @@ -14,6 +14,9 @@ #import #import +/// For deallocation of objects on the main thread across multiple run loops. +extern void ASPerformMainThreadDeallocation(_Nullable id object); + // Because inline methods can't be extern'd and need to be part of the translation unit of code // that compiles with them to actually inline, we both declare and define these in the header. ASDISPLAYNODE_INLINE BOOL ASInterfaceStateIncludesVisible(ASInterfaceState interfaceState) diff --git a/AsyncDisplayKit/ASDisplayNodeExtras.mm b/AsyncDisplayKit/ASDisplayNodeExtras.mm index bdf1fc79d6..97b22578b7 100644 --- a/AsyncDisplayKit/ASDisplayNodeExtras.mm +++ b/AsyncDisplayKit/ASDisplayNodeExtras.mm @@ -13,6 +13,24 @@ #import "ASDisplayNode+FrameworkPrivate.h" #import +#import "ASRunLoopQueue.h" + +extern void ASPerformMainThreadDeallocation(_Nullable id object) +{ + /** + * UIKit components must be deallocated on the main thread. We use this shared + * run loop queue to gradually deallocate them across many turns of the main run loop. + */ + static ASRunLoopQueue *queue; + static dispatch_once_t onceToken; + dispatch_once(&onceToken, ^{ + queue = [[ASRunLoopQueue alloc] initWithRunLoop:CFRunLoopGetMain() andHandler:nil]; + queue.batchSize = 10; + }); + if (object != nil) { + [queue enqueue:object]; + } +} extern ASInterfaceState ASInterfaceStateForDisplayNode(ASDisplayNode *displayNode, UIWindow *window) { diff --git a/AsyncDisplayKit/ASEditableTextNode.mm b/AsyncDisplayKit/ASEditableTextNode.mm index b47dc7ca14..4fd2dfdc8f 100644 --- a/AsyncDisplayKit/ASEditableTextNode.mm +++ b/AsyncDisplayKit/ASEditableTextNode.mm @@ -415,7 +415,7 @@ [_textKitComponents.textStorage setAttributedString:attributedStringToDisplay]; // Calculated size depends on the seeded text. - [self invalidateCalculatedLayout]; + [self setNeedsLayout]; // Update if placeholder is shown. [self _updateDisplayingPlaceholder]; diff --git a/AsyncDisplayKit/ASImageNode.mm b/AsyncDisplayKit/ASImageNode.mm index f58a8c769f..d59063ec42 100644 --- a/AsyncDisplayKit/ASImageNode.mm +++ b/AsyncDisplayKit/ASImageNode.mm @@ -204,7 +204,7 @@ struct ASImageNodeDrawParameters { if (!ASObjectIsEqual(_image, image)) { _image = image; - [self invalidateCalculatedLayout]; + [self setNeedsLayout]; if (image) { [self setNeedsDisplay]; diff --git a/AsyncDisplayKit/ASMapNode.mm b/AsyncDisplayKit/ASMapNode.mm index 5b347d7c81..e60a834eb9 100644 --- a/AsyncDisplayKit/ASMapNode.mm +++ b/AsyncDisplayKit/ASMapNode.mm @@ -74,9 +74,9 @@ [super setLayerBacked:layerBacked]; } -- (void)fetchData +- (void)didEnterPreloadState { - [super fetchData]; + [super didEnterPreloadState]; ASPerformBlockOnMainThread(^{ if (self.isLiveMap) { [self addLiveMap]; @@ -86,9 +86,9 @@ }); } -- (void)clearFetchedData +- (void)didExitPreloadState { - [super clearFetchedData]; + [super didExitPreloadState]; ASPerformBlockOnMainThread(^{ if (self.isLiveMap) { [self removeLiveMap]; diff --git a/AsyncDisplayKit/ASMultiplexImageNode.mm b/AsyncDisplayKit/ASMultiplexImageNode.mm index 55512148e6..6759f80ffc 100644 --- a/AsyncDisplayKit/ASMultiplexImageNode.mm +++ b/AsyncDisplayKit/ASMultiplexImageNode.mm @@ -218,12 +218,12 @@ typedef void(^ASMultiplexImageLoadCompletionBlock)(UIImage *image, id imageIdent [super clearContents]; // This actually clears the contents, so we need to do this first for our displayedImageIdentifier to be meaningful. [self _setDisplayedImageIdentifier:nil withImage:nil]; - // NOTE: We intentionally do not cancel image downloads until `clearFetchedData`. + // NOTE: We intentionally do not cancel image downloads until `clearPreloadedData`. } -- (void)clearFetchedData +- (void)didExitPreloadState { - [super clearFetchedData]; + [super didExitPreloadState]; [_phImageRequestOperation cancel]; @@ -238,9 +238,9 @@ typedef void(^ASMultiplexImageLoadCompletionBlock)(UIImage *image, id imageIdent self.image = nil; } -- (void)fetchData +- (void)didEnterPreloadState { - [super fetchData]; + [super didEnterPreloadState]; [self _loadImageIdentifiers]; } @@ -283,7 +283,7 @@ typedef void(^ASMultiplexImageLoadCompletionBlock)(UIImage *image, id imageIdent { [super displayWillStart]; - [self fetchData]; + [self didEnterPreloadState]; if (_downloaderImplementsSetPriority) { { @@ -398,7 +398,7 @@ typedef void(^ASMultiplexImageLoadCompletionBlock)(UIImage *image, id imageIdent _imageIdentifiers = [[NSArray alloc] initWithArray:imageIdentifiers copyItems:YES]; } - [self setNeedsDataFetch]; + [self setNeedsPreload]; } - (void)reloadImageIdentifierSources diff --git a/AsyncDisplayKit/ASNetworkImageNode.mm b/AsyncDisplayKit/ASNetworkImageNode.mm index 9d1da0e40a..aef194180b 100755 --- a/AsyncDisplayKit/ASNetworkImageNode.mm +++ b/AsyncDisplayKit/ASNetworkImageNode.mm @@ -147,7 +147,7 @@ static const CGSize kMinReleaseImageOnBackgroundSize = {20.0, 20.0}; }); } - [self setNeedsDataFetch]; + [self setNeedsPreload]; } - (NSURL *)URL @@ -267,8 +267,8 @@ static const CGSize kMinReleaseImageOnBackgroundSize = {20.0, 20.0}; } } - // TODO: Consider removing this; it predates ASInterfaceState, which now ensures that even non-range-managed nodes get a -fetchData call. - [self fetchData]; + // TODO: Consider removing this; it predates ASInterfaceState, which now ensures that even non-range-managed nodes get a -preload call. + [self didEnterPreloadState]; if (self.image == nil && _downloaderFlags.downloaderImplementsSetPriority) { ASDN::MutexLocker l(__instanceLock__); @@ -308,9 +308,9 @@ static const CGSize kMinReleaseImageOnBackgroundSize = {20.0, 20.0}; [self _updateProgressImageBlockOnDownloaderIfNeeded]; } -- (void)clearFetchedData +- (void)didExitPreloadState { - [super clearFetchedData]; + [super didExitPreloadState]; { ASDN::MutexLocker l(__instanceLock__); @@ -323,9 +323,9 @@ static const CGSize kMinReleaseImageOnBackgroundSize = {20.0, 20.0}; } } -- (void)fetchData +- (void)didEnterPreloadState { - [super fetchData]; + [super didEnterPreloadState]; { ASDN::MutexLocker l(__instanceLock__); diff --git a/AsyncDisplayKit/ASRunLoopQueue.h b/AsyncDisplayKit/ASRunLoopQueue.h index 4eacc232da..cd9a572997 100644 --- a/AsyncDisplayKit/ASRunLoopQueue.h +++ b/AsyncDisplayKit/ASRunLoopQueue.h @@ -16,8 +16,19 @@ NS_ASSUME_NONNULL_BEGIN @interface ASRunLoopQueue : NSObject +/** + * Create a new queue with the given run loop and handler. + * + * @param runloop The run loop that will drive this queue. + * @param handlerBlock An optional block to be run for each enqueued object. + * + * @discussion You may pass @c nil for the handler if you simply want the objects to + * be retained at enqueue time, and released during the run loop step. This is useful + * for creating a "main deallocation queue", as @c ASDeallocQueue creates its own + * worker thread with its own run loop. + */ - (instancetype)initWithRunLoop:(CFRunLoopRef)runloop - andHandler:(void(^)(ObjectType dequeuedItem, BOOL isQueueDrained))handlerBlock; + andHandler:(nullable void(^)(ObjectType dequeuedItem, BOOL isQueueDrained))handlerBlock; - (void)enqueue:(ObjectType)object; diff --git a/AsyncDisplayKit/ASRunLoopQueue.mm b/AsyncDisplayKit/ASRunLoopQueue.mm index aac7ad1d17..7e50424a59 100644 --- a/AsyncDisplayKit/ASRunLoopQueue.mm +++ b/AsyncDisplayKit/ASRunLoopQueue.mm @@ -16,6 +16,7 @@ #import #import +#import #define ASRunLoopQueueLoggingEnabled 0 @@ -222,7 +223,12 @@ static void runLoopSourceCallback(void *info) { - (void)processQueue { - std::deque itemsToProcess = std::deque(); + BOOL hasExecutionBlock = (_queueConsumer != nil); + + // If we have an execution block, this vector will be populated, otherwise remains empty. + // This is to avoid needlessly retaining/releasing the objects if we don't have a block. + std::vector itemsToProcess; + BOOL isQueueDrained = NO; { ASDN::MutexLocker l(_internalQueueLock); @@ -235,25 +241,23 @@ static void runLoopSourceCallback(void *info) { ASProfilingSignpostStart(0, self); // Snatch the next batch of items. - NSUInteger totalNodeCount = _internalQueue.size(); - for (int i = 0; i < MIN(self.batchSize, totalNodeCount); i++) { - id node = _internalQueue[0]; - itemsToProcess.push_back(node); - _internalQueue.pop_front(); + auto firstItemToProcess = _internalQueue.cbegin(); + auto lastItemToProcess = MIN(_internalQueue.cend(), firstItemToProcess + self.batchSize); + + if (hasExecutionBlock) { + itemsToProcess = std::vector(firstItemToProcess, lastItemToProcess); } + _internalQueue.erase(firstItemToProcess, lastItemToProcess); if (_internalQueue.empty()) { isQueueDrained = YES; } } - unsigned long numberOfItems = itemsToProcess.size(); - for (int i = 0; i < numberOfItems; i++) { - if (isQueueDrained && i == numberOfItems - 1) { - _queueConsumer(itemsToProcess[i], YES); - } else { - _queueConsumer(itemsToProcess[i], isQueueDrained); - } + // itemsToProcess will be empty if _queueConsumer == nil so no need to check again. + auto itemsEnd = itemsToProcess.cend(); + for (auto iterator = itemsToProcess.begin(); iterator < itemsEnd; iterator++) { + _queueConsumer(*iterator, isQueueDrained && iterator == itemsEnd - 1); } // If the queue is not fully drained yet force another run loop to process next batch of items diff --git a/AsyncDisplayKit/ASTableNode.mm b/AsyncDisplayKit/ASTableNode.mm index 74881c170e..10e54c38e4 100644 --- a/AsyncDisplayKit/ASTableNode.mm +++ b/AsyncDisplayKit/ASTableNode.mm @@ -127,12 +127,6 @@ } } -- (void)dealloc -{ - self.delegate = nil; - self.dataSource = nil; -} - - (ASTableView *)view { return (ASTableView *)[super view]; @@ -144,10 +138,10 @@ [self.rangeController clearContents]; } -- (void)clearFetchedData +- (void)didExitPreloadState { - [super clearFetchedData]; - [self.rangeController clearFetchedData]; + [super didExitPreloadState]; + [self.rangeController clearPreloadedData]; } - (void)interfaceStateDidChange:(ASInterfaceState)newState fromState:(ASInterfaceState)oldState @@ -203,7 +197,8 @@ // Manually trampoline to the main thread. The view requires this be called on main // and asserting here isn't an option – it is a common pattern for users to clear // the delegate/dataSource in dealloc, which may be running on a background thread. - ASTableView *view = self.view; + // It is important that we avoid retaining self in this block, so that this method is dealloc-safe. + ASTableView *view = (ASTableView *)_view; ASPerformBlockOnMainThread(^{ view.asyncDelegate = delegate; }); @@ -229,7 +224,8 @@ // Manually trampoline to the main thread. The view requires this be called on main // and asserting here isn't an option – it is a common pattern for users to clear // the delegate/dataSource in dealloc, which may be running on a background thread. - ASTableView *view = self.view; + // It is important that we avoid retaining self in this block, so that this method is dealloc-safe. + ASTableView *view = (ASTableView *)_view; ASPerformBlockOnMainThread(^{ view.asyncDataSource = dataSource; }); diff --git a/AsyncDisplayKit/ASTableView.mm b/AsyncDisplayKit/ASTableView.mm index e5cb5e657f..1465e1125c 100644 --- a/AsyncDisplayKit/ASTableView.mm +++ b/AsyncDisplayKit/ASTableView.mm @@ -23,6 +23,7 @@ #import "ASInternalHelpers.h" #import "ASLayout.h" #import "_ASDisplayLayer.h" +#import "_ASCoreAnimationExtras.h" #import "ASTableNode.h" #import "ASEqualityHelpers.h" #import "ASTableView+Undeprecated.h" @@ -33,6 +34,15 @@ static NSString * const kCellReuseIdentifier = @"_ASTableViewCell"; //#define LOG(...) NSLog(__VA_ARGS__) #define LOG(...) +/** + * See note at the top of ASCollectionView.mm near declaration of macro GET_COLLECTIONNODE_OR_RETURN + */ +#define GET_TABLENODE_OR_RETURN(__var, __val) \ + ASTableNode *__var = self.tableNode; \ + if (__var == nil) { \ + return __val; \ + } + #pragma mark - #pragma mark ASCellNode<->UITableViewCell bridging. @@ -132,6 +142,9 @@ static NSString * const kCellReuseIdentifier = @"_ASTableViewCell"; BOOL _performingBatchUpdates; NSMutableSet *_cellsForVisibilityUpdates; + // See documentation on same property in ASCollectionView + BOOL _hasEverCheckedForBatchFetchingDueToUpdate; + // The section index overlay view, if there is one present. // This is useful because we need to measure our row nodes against (width - indexView.width). __weak UIView *_sectionIndexView; @@ -354,6 +367,7 @@ static NSString * const kCellReuseIdentifier = @"_ASTableViewCell"; ASDisplayNodeAssert(_asyncDataSourceFlags.tableNodeNumberOfRowsInSection || _asyncDataSourceFlags.tableViewNumberOfRowsInSection, @"Data source must implement tableNode:numberOfRowsInSection:"); } + _dataController.validationErrorSource = asyncDataSource; super.dataSource = (id)_proxyDataSource; } @@ -603,7 +617,7 @@ static NSString * const kCellReuseIdentifier = @"_ASTableViewCell"; } else { [self beginUpdates]; [_dataController relayoutAllNodes]; - [self endUpdates]; + [self endUpdatesAnimated:(ASDisplayNodeLayerHasAnimations(self.layer) == NO) completion:nil]; } } @@ -805,7 +819,8 @@ static NSString * const kCellReuseIdentifier = @"_ASTableViewCell"; ASDisplayNodeAssertNotNil(cellNode, @"Expected node associated with cell that will be displayed not to be nil. indexPath: %@", indexPath); if (_asyncDelegateFlags.tableNodeWillDisplayNodeForRow) { - [_asyncDelegate tableNode:self.tableNode willDisplayRowWithNode:cellNode]; + GET_TABLENODE_OR_RETURN(tableNode, (void)0); + [_asyncDelegate tableNode:tableNode willDisplayRowWithNode:cellNode]; } else if (_asyncDelegateFlags.tableViewWillDisplayNodeForRow) { #pragma clang diagnostic push #pragma clang diagnostic ignored "-Wdeprecated-declarations" @@ -834,7 +849,9 @@ static NSString * const kCellReuseIdentifier = @"_ASTableViewCell"; ASDisplayNodeAssertNotNil(cellNode, @"Expected node associated with removed cell not to be nil."); if (_asyncDelegateFlags.tableNodeDidEndDisplayingNodeForRow) { - [_asyncDelegate tableNode:self.tableNode didEndDisplayingRowWithNode:cellNode]; + if (ASTableNode *tableNode = self.tableNode) { + [_asyncDelegate tableNode:tableNode didEndDisplayingRowWithNode:cellNode]; + } } else if (_asyncDelegateFlags.tableViewDidEndDisplayingNodeForRow) { #pragma clang diagnostic push #pragma clang diagnostic ignored "-Wdeprecated-declarations" @@ -850,12 +867,13 @@ static NSString * const kCellReuseIdentifier = @"_ASTableViewCell"; - (NSIndexPath *)tableView:(UITableView *)tableView willSelectRowAtIndexPath:(nonnull NSIndexPath *)indexPath { if (_asyncDelegateFlags.tableNodeWillSelectRow) { + GET_TABLENODE_OR_RETURN(tableNode, indexPath); NSIndexPath *result = [self convertIndexPathToTableNode:indexPath]; // If this item was is gone, just let the table view do its default behavior and select. if (result == nil) { return indexPath; } else { - result = [_asyncDelegate tableNode:self.tableNode willSelectRowAtIndexPath:result]; + result = [_asyncDelegate tableNode:tableNode willSelectRowAtIndexPath:result]; result = [self convertIndexPathFromTableNode:result waitingIfNeeded:YES]; return result; } @@ -872,9 +890,10 @@ static NSString * const kCellReuseIdentifier = @"_ASTableViewCell"; - (void)tableView:(UITableView *)tableView didSelectRowAtIndexPath:(nonnull NSIndexPath *)indexPath { if (_asyncDelegateFlags.tableNodeDidSelectRow) { + GET_TABLENODE_OR_RETURN(tableNode, (void)0); indexPath = [self convertIndexPathToTableNode:indexPath]; if (indexPath != nil) { - [_asyncDelegate tableNode:self.tableNode didSelectRowAtIndexPath:indexPath]; + [_asyncDelegate tableNode:tableNode didSelectRowAtIndexPath:indexPath]; } } else if (_asyncDelegateFlags.tableViewDidSelectRow) { #pragma clang diagnostic push @@ -887,12 +906,13 @@ static NSString * const kCellReuseIdentifier = @"_ASTableViewCell"; - (NSIndexPath *)tableView:(UITableView *)tableView willDeselectRowAtIndexPath:(nonnull NSIndexPath *)indexPath { if (_asyncDelegateFlags.tableNodeWillDeselectRow) { + GET_TABLENODE_OR_RETURN(tableNode, indexPath); NSIndexPath *result = [self convertIndexPathToTableNode:indexPath]; // If this item was is gone, just let the table view do its default behavior and deselect. if (result == nil) { return indexPath; } else { - result = [_asyncDelegate tableNode:self.tableNode willDeselectRowAtIndexPath:result]; + result = [_asyncDelegate tableNode:tableNode willDeselectRowAtIndexPath:result]; result = [self convertIndexPathFromTableNode:result waitingIfNeeded:YES]; return result; } @@ -908,9 +928,10 @@ static NSString * const kCellReuseIdentifier = @"_ASTableViewCell"; - (void)tableView:(UITableView *)tableView didDeselectRowAtIndexPath:(nonnull NSIndexPath *)indexPath { if (_asyncDelegateFlags.tableNodeDidDeselectRow) { + GET_TABLENODE_OR_RETURN(tableNode, (void)0); indexPath = [self convertIndexPathToTableNode:indexPath]; if (indexPath != nil) { - [_asyncDelegate tableNode:self.tableNode didDeselectRowAtIndexPath:indexPath]; + [_asyncDelegate tableNode:tableNode didDeselectRowAtIndexPath:indexPath]; } } else if (_asyncDelegateFlags.tableViewDidDeselectRow) { #pragma clang diagnostic push @@ -923,9 +944,10 @@ static NSString * const kCellReuseIdentifier = @"_ASTableViewCell"; - (BOOL)tableView:(UITableView *)tableView shouldHighlightRowAtIndexPath:(nonnull NSIndexPath *)indexPath { if (_asyncDelegateFlags.tableNodeShouldHighlightRow) { + GET_TABLENODE_OR_RETURN(tableNode, NO); indexPath = [self convertIndexPathToTableNode:indexPath]; if (indexPath != nil) { - return [_asyncDelegate tableNode:self.tableNode shouldHighlightRowAtIndexPath:indexPath]; + return [_asyncDelegate tableNode:tableNode shouldHighlightRowAtIndexPath:indexPath]; } } else if (_asyncDelegateFlags.tableViewShouldHighlightRow) { #pragma clang diagnostic push @@ -939,9 +961,10 @@ static NSString * const kCellReuseIdentifier = @"_ASTableViewCell"; - (void)tableView:(UITableView *)tableView didHighlightRowAtIndexPath:(nonnull NSIndexPath *)indexPath { if (_asyncDelegateFlags.tableNodeDidHighlightRow) { + GET_TABLENODE_OR_RETURN(tableNode, (void)0); indexPath = [self convertIndexPathToTableNode:indexPath]; if (indexPath != nil) { - return [_asyncDelegate tableNode:self.tableNode didHighlightRowAtIndexPath:indexPath]; + return [_asyncDelegate tableNode:tableNode didHighlightRowAtIndexPath:indexPath]; } } else if (_asyncDelegateFlags.tableViewDidHighlightRow) { #pragma clang diagnostic push @@ -954,9 +977,10 @@ static NSString * const kCellReuseIdentifier = @"_ASTableViewCell"; - (void)tableView:(UITableView *)tableView didUnhighlightRowAtIndexPath:(nonnull NSIndexPath *)indexPath { if (_asyncDelegateFlags.tableNodeDidHighlightRow) { + GET_TABLENODE_OR_RETURN(tableNode, (void)0); indexPath = [self convertIndexPathToTableNode:indexPath]; if (indexPath != nil) { - return [_asyncDelegate tableNode:self.tableNode didUnhighlightRowAtIndexPath:indexPath]; + return [_asyncDelegate tableNode:tableNode didUnhighlightRowAtIndexPath:indexPath]; } } else if (_asyncDelegateFlags.tableViewDidUnhighlightRow) { #pragma clang diagnostic push @@ -969,9 +993,10 @@ static NSString * const kCellReuseIdentifier = @"_ASTableViewCell"; - (BOOL)tableView:(UITableView *)tableView shouldShowMenuForRowAtIndexPath:(nonnull NSIndexPath *)indexPath { if (_asyncDelegateFlags.tableNodeShouldShowMenuForRow) { + GET_TABLENODE_OR_RETURN(tableNode, NO); indexPath = [self convertIndexPathToTableNode:indexPath]; if (indexPath != nil) { - return [_asyncDelegate tableNode:self.tableNode shouldShowMenuForRowAtIndexPath:indexPath]; + return [_asyncDelegate tableNode:tableNode shouldShowMenuForRowAtIndexPath:indexPath]; } } else if (_asyncDelegateFlags.tableViewShouldShowMenuForRow) { #pragma clang diagnostic push @@ -985,9 +1010,10 @@ static NSString * const kCellReuseIdentifier = @"_ASTableViewCell"; - (BOOL)tableView:(UITableView *)tableView canPerformAction:(nonnull SEL)action forRowAtIndexPath:(nonnull NSIndexPath *)indexPath withSender:(nullable id)sender { if (_asyncDelegateFlags.tableNodeCanPerformActionForRow) { + GET_TABLENODE_OR_RETURN(tableNode, NO); indexPath = [self convertIndexPathToTableNode:indexPath]; if (indexPath != nil) { - return [_asyncDelegate tableNode:self.tableNode canPerformAction:action forRowAtIndexPath:indexPath withSender:sender]; + return [_asyncDelegate tableNode:tableNode canPerformAction:action forRowAtIndexPath:indexPath withSender:sender]; } } else if (_asyncDelegateFlags.tableViewCanPerformActionForRow) { #pragma clang diagnostic push @@ -1001,9 +1027,10 @@ static NSString * const kCellReuseIdentifier = @"_ASTableViewCell"; - (void)tableView:(UITableView *)tableView performAction:(nonnull SEL)action forRowAtIndexPath:(nonnull NSIndexPath *)indexPath withSender:(nullable id)sender { if (_asyncDelegateFlags.tableNodePerformActionForRow) { + GET_TABLENODE_OR_RETURN(tableNode, (void)0); indexPath = [self convertIndexPathToTableNode:indexPath]; if (indexPath != nil) { - [_asyncDelegate tableNode:self.tableNode performAction:action forRowAtIndexPath:indexPath withSender:sender]; + [_asyncDelegate tableNode:tableNode performAction:action forRowAtIndexPath:indexPath withSender:sender]; } } else if (_asyncDelegateFlags.tableViewPerformActionForRow) { #pragma clang diagnostic push @@ -1019,6 +1046,7 @@ static NSString * const kCellReuseIdentifier = @"_ASTableViewCell"; ASInterfaceState interfaceState = [self interfaceStateForRangeController:_rangeController]; if (ASInterfaceStateIncludesVisible(interfaceState)) { [_rangeController updateCurrentRangeWithMode:ASLayoutRangeModeFull]; + [self _checkForBatchFetching]; } for (_ASTableViewCell *tableCell in _cellsForVisibilityUpdates) { @@ -1041,7 +1069,7 @@ static NSString * const kCellReuseIdentifier = @"_ASTableViewCell"; if (targetContentOffset != NULL) { ASDisplayNodeAssert(_batchContext != nil, @"Batch context should exist"); - [self _beginBatchFetchingIfNeededWithScrollView:self forScrollDirection:[self scrollDirection] contentOffset:*targetContentOffset]; + [self _beginBatchFetchingIfNeededWithContentOffset:*targetContentOffset]; } if (_asyncDelegateFlags.scrollViewWillEndDragging) { @@ -1132,7 +1160,8 @@ static NSString * const kCellReuseIdentifier = @"_ASTableViewCell"; // if the delegate does not respond to this method, there is no point in starting to fetch BOOL canFetch = _asyncDelegateFlags.tableNodeWillBeginBatchFetch || _asyncDelegateFlags.tableViewWillBeginBatchFetch; if (canFetch && _asyncDelegateFlags.shouldBatchFetchForTableNode) { - return [_asyncDelegate shouldBatchFetchForTableNode:self.tableNode]; + GET_TABLENODE_OR_RETURN(tableNode, NO); + return [_asyncDelegate shouldBatchFetchForTableNode:tableNode]; } else if (canFetch && _asyncDelegateFlags.shouldBatchFetchForTableView) { #pragma clang diagnostic push #pragma clang diagnostic ignored "-Wdeprecated-declarations" @@ -1146,9 +1175,10 @@ static NSString * const kCellReuseIdentifier = @"_ASTableViewCell"; - (void)_scheduleCheckForBatchFetchingForNumberOfChanges:(NSUInteger)changes { // Prevent fetching will continually trigger in a loop after reaching end of content and no new content was provided - if (changes == 0) { + if (changes == 0 && _hasEverCheckedForBatchFetchingDueToUpdate) { return; } + _hasEverCheckedForBatchFetchingDueToUpdate = YES; // Push this to the next runloop to be sure the scroll view has the right content size dispatch_async(dispatch_get_main_queue(), ^{ @@ -1163,12 +1193,12 @@ static NSString * const kCellReuseIdentifier = @"_ASTableViewCell"; return; } - [self _beginBatchFetchingIfNeededWithScrollView:self forScrollDirection:[self scrollableDirections] contentOffset:self.contentOffset]; + [self _beginBatchFetchingIfNeededWithContentOffset:self.contentOffset]; } -- (void)_beginBatchFetchingIfNeededWithScrollView:(UIScrollView *)scrollView forScrollDirection:(ASScrollDirection)scrollDirection contentOffset:(CGPoint)contentOffset +- (void)_beginBatchFetchingIfNeededWithContentOffset:(CGPoint)contentOffset { - if (ASDisplayShouldFetchBatchForScrollView(self, scrollDirection, contentOffset)) { + if (ASDisplayShouldFetchBatchForScrollView(self, self.scrollDirection, ASScrollDirectionVerticalDirections, contentOffset)) { [self _beginBatchFetching]; } } @@ -1178,7 +1208,8 @@ static NSString * const kCellReuseIdentifier = @"_ASTableViewCell"; [_batchContext beginBatchFetching]; if (_asyncDelegateFlags.tableNodeWillBeginBatchFetch) { dispatch_async(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0), ^{ - [_asyncDelegate tableNode:self.tableNode willBeginBatchFetchWithContext:_batchContext]; + GET_TABLENODE_OR_RETURN(tableNode, (void)0); + [_asyncDelegate tableNode:tableNode willBeginBatchFetchWithContext:_batchContext]; }); } else if (_asyncDelegateFlags.tableViewWillBeginBatchFetch) { dispatch_async(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0), ^{ @@ -1201,22 +1232,33 @@ static NSString * const kCellReuseIdentifier = @"_ASTableViewCell"; { ASDisplayNodeAssertMainThread(); + CGRect bounds = self.bounds; // 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 (CGSizeEqualToSize(self.bounds.size, CGSizeZero)) { + if (CGRectIsEmpty(bounds)) { return @[]; } - - // NOTE: A prior comment claimed that `indexPathsForVisibleRows` may return extra index paths for grouped-style - // tables. This is seen as an acceptable issue for the time being. - + + NSMutableArray *visibleIndexPaths = [self.indexPathsForVisibleRows mutableCopy]; + + [visibleIndexPaths sortUsingSelector:@selector(compare:)]; + + // In some cases (grouped-style tables with particular geometry) indexPathsForVisibleRows will return extra index paths. + // This is a very serious issue because we rely on the fact that any node that is marked Visible is hosted inside of a cell, + // or else we may not mark it invisible before the node is released. See testIssue2252. + // Calling indexPathForCell: and cellForRowAtIndexPath: are both pretty expensive – this is the quickest approach we have. + // It would be possible to cache this NSPredicate as an ivar, but that would require unsafeifying self and calling @c bounds + // for each item. Since the performance cost is pretty small, prefer simplicity. + if (self.style == UITableViewStyleGrouped && visibleIndexPaths.count != self.visibleCells.count) { + [visibleIndexPaths filterUsingPredicate:[NSPredicate predicateWithBlock:^BOOL(NSIndexPath *indexPath, NSDictionary * _Nullable bindings) { + return CGRectIntersectsRect(bounds, [self rectForRowAtIndexPath:indexPath]); + }]]; + } + NSIndexPath *pendingVisibleIndexPath = _pendingVisibleIndexPath; if (pendingVisibleIndexPath == nil) { - return self.indexPathsForVisibleRows; + return visibleIndexPaths; } - - NSMutableArray *visibleIndexPaths = [self.indexPathsForVisibleRows mutableCopy]; - [visibleIndexPaths sortUsingSelector:@selector(compare:)]; BOOL isPendingIndexPathVisible = (NSNotFound != [visibleIndexPaths indexOfObject:pendingVisibleIndexPath inSortedRange:NSMakeRange(0, visibleIndexPaths.count) options:kNilOptions usingComparator:^(id _Nonnull obj1, id _Nonnull obj2) { return [obj1 compare:obj2]; @@ -1306,11 +1348,6 @@ static NSString * const kCellReuseIdentifier = @"_ASTableViewCell"; } } -- (void)didCompleteUpdatesInRangeController:(ASRangeController *)rangeController -{ - [self _checkForBatchFetching]; -} - - (void)rangeController:(ASRangeController *)rangeController didInsertNodes:(NSArray *)nodes atIndexPaths:(NSArray *)indexPaths withAnimationOptions:(ASDataControllerAnimationOptions)animationOptions { ASDisplayNodeAssertMainThread(); @@ -1414,9 +1451,14 @@ static NSString * const kCellReuseIdentifier = @"_ASTableViewCell"; ASCellNodeBlock block = nil; if (_asyncDataSourceFlags.tableNodeNodeBlockForRow) { - block = [_asyncDataSource tableNode:self.tableNode nodeBlockForRowAtIndexPath:indexPath]; + if (ASTableNode *tableNode = self.tableNode) { + block = [_asyncDataSource tableNode:tableNode nodeBlockForRowAtIndexPath:indexPath]; + } } else if (_asyncDataSourceFlags.tableNodeNodeForRow) { - ASCellNode *node = [_asyncDataSource tableNode:self.tableNode nodeForRowAtIndexPath:indexPath]; + ASCellNode *node = nil; + if (ASTableNode *tableNode = self.tableNode) { + node = [_asyncDataSource tableNode:tableNode nodeForRowAtIndexPath:indexPath]; + } if ([node isKindOfClass:[ASCellNode class]]) { block = ^{ return node; @@ -1466,7 +1508,8 @@ static NSString * const kCellReuseIdentifier = @"_ASTableViewCell"; { ASSizeRange constrainedSize = kInvalidSizeRange; if (_asyncDelegateFlags.tableNodeConstrainedSizeForRow) { - ASSizeRange delegateConstrainedSize = [_asyncDelegate tableNode:self.tableNode constrainedSizeForRowAtIndexPath:indexPath]; + GET_TABLENODE_OR_RETURN(tableNode, constrainedSize); + ASSizeRange delegateConstrainedSize = [_asyncDelegate tableNode:tableNode constrainedSizeForRowAtIndexPath:indexPath]; // ignore widths in the returned size range (for TableView) constrainedSize = ASSizeRangeMake(CGSizeMake(_nodesConstrainedWidth, delegateConstrainedSize.min.height), CGSizeMake(_nodesConstrainedWidth, delegateConstrainedSize.max.height)); @@ -1488,7 +1531,8 @@ static NSString * const kCellReuseIdentifier = @"_ASTableViewCell"; - (NSUInteger)dataController:(ASDataController *)dataController rowsInSection:(NSUInteger)section { if (_asyncDataSourceFlags.tableNodeNumberOfRowsInSection) { - return [_asyncDataSource tableNode:self.tableNode numberOfRowsInSection:section]; + GET_TABLENODE_OR_RETURN(tableNode, 0); + return [_asyncDataSource tableNode:tableNode numberOfRowsInSection:section]; } else if (_asyncDataSourceFlags.tableViewNumberOfRowsInSection) { #pragma clang diagnostic push #pragma clang diagnostic ignored "-Wdeprecated-declarations" @@ -1502,7 +1546,8 @@ static NSString * const kCellReuseIdentifier = @"_ASTableViewCell"; - (NSUInteger)numberOfSectionsInDataController:(ASDataController *)dataController { if (_asyncDataSourceFlags.numberOfSectionsInTableNode) { - return [_asyncDataSource numberOfSectionsInTableNode:self.tableNode]; + GET_TABLENODE_OR_RETURN(tableNode, 0); + return [_asyncDataSource numberOfSectionsInTableNode:tableNode]; } else if (_asyncDataSourceFlags.numberOfSectionsInTableView) { #pragma clang diagnostic push #pragma clang diagnostic ignored "-Wdeprecated-declarations" @@ -1536,7 +1581,7 @@ static NSString * const kCellReuseIdentifier = @"_ASTableViewCell"; // Normally the content view width equals to the constrained size width (which equals to the table view width). // If there is a mismatch between these values, for example after the table view entered or left editing mode, // content view width is preferred and used to re-measure the cell node. - if (contentViewWidth != constrainedSize.max.width) { + if (CGSizeEqualToSize(node.calculatedSize, CGSizeZero) == NO && contentViewWidth != constrainedSize.max.width) { constrainedSize.min.width = contentViewWidth; constrainedSize.max.width = contentViewWidth; @@ -1553,7 +1598,7 @@ static NSString * const kCellReuseIdentifier = @"_ASTableViewCell"; // If the node height changed, trigger a height requery. if (oldSize.height != calculatedSize.height) { [self beginUpdates]; - [self endUpdates]; + [self endUpdatesAnimated:(ASDisplayNodeLayerHasAnimations(self.layer) == NO) completion:nil]; } } } @@ -1604,18 +1649,6 @@ static NSString * const kCellReuseIdentifier = @"_ASTableViewCell"; [super endUpdates]; } -#pragma mark - Memory Management - -- (void)clearContents -{ - [_rangeController clearContents]; -} - -- (void)clearFetchedData -{ - [_rangeController clearFetchedData]; -} - #pragma mark - Helper Methods // Note: This is called every layout, and so it is very performance sensitive. diff --git a/AsyncDisplayKit/ASTextNode.mm b/AsyncDisplayKit/ASTextNode.mm index 25ff6cd86a..9eb503a13e 100644 --- a/AsyncDisplayKit/ASTextNode.mm +++ b/AsyncDisplayKit/ASTextNode.mm @@ -317,7 +317,6 @@ static NSArray *DefaultLinkAttributeNames = @[ NSLinkAttributeName ]; BOOL needsUpdate = !UIEdgeInsetsEqualToEdgeInsets(textContainerInset, _textContainerInset); if (needsUpdate) { _textContainerInset = textContainerInset; - [self invalidateCalculatedLayout]; [self setNeedsLayout]; } } @@ -474,7 +473,7 @@ static NSArray *DefaultLinkAttributeNames = @[ NSLinkAttributeName ]; } // Tell the display node superclasses that the cached layout is incorrect now - [self invalidateCalculatedLayout]; + [self setNeedsLayout]; // Force display to create renderer with new size and redisplay with new string [self setNeedsDisplay]; @@ -497,7 +496,7 @@ static NSArray *DefaultLinkAttributeNames = @[ NSLinkAttributeName ]; _exclusionPaths = [exclusionPaths copy]; [self _invalidateRenderer]; - [self invalidateCalculatedLayout]; + [self setNeedsLayout]; [self setNeedsDisplay]; } diff --git a/AsyncDisplayKit/ASVideoNode.mm b/AsyncDisplayKit/ASVideoNode.mm index 59d68495a3..1a355a1e11 100644 --- a/AsyncDisplayKit/ASVideoNode.mm +++ b/AsyncDisplayKit/ASVideoNode.mm @@ -14,6 +14,7 @@ #import #import "ASDisplayNodeInternal.h" #import "ASDisplayNode+Subclasses.h" +#import "ASDisplayNode+FrameworkPrivate.h" #import "ASVideoNode.h" #import "ASEqualityHelpers.h" #import "ASInternalHelpers.h" @@ -368,9 +369,9 @@ static NSString * const kRate = @"rate"; } } -- (void)fetchData +- (void)didEnterPreloadState { - [super fetchData]; + [super didEnterPreloadState]; ASDN::MutexLocker l(__instanceLock__); AVAsset *asset = self.asset; @@ -408,9 +409,9 @@ static NSString * const kRate = @"rate"; } } -- (void)clearFetchedData +- (void)didExitPreloadState { - [super clearFetchedData]; + [super didExitPreloadState]; { ASDN::MutexLocker l(__instanceLock__); @@ -508,10 +509,10 @@ static NSString * const kRate = @"rate"; - (void)_setAndFetchAsset:(AVAsset *)asset url:(NSURL *)assetURL { - [self clearFetchedData]; + [self didExitPreloadState]; _asset = asset; _assetURL = assetURL; - [self setNeedsDataFetch]; + [self setNeedsPreload]; } - (void)setVideoComposition:(AVVideoComposition *)videoComposition @@ -620,7 +621,7 @@ static NSString * const kRate = @"rate"; } if (_player == nil) { - [self setNeedsDataFetch]; + [self setNeedsPreload]; } if (_playerNode == nil) { diff --git a/AsyncDisplayKit/ASViewController.h b/AsyncDisplayKit/ASViewController.h index 1420af98d7..1c94f912b1 100644 --- a/AsyncDisplayKit/ASViewController.h +++ b/AsyncDisplayKit/ASViewController.h @@ -66,16 +66,6 @@ typedef ASTraitCollection * _Nonnull (^ASDisplayTraitsForTraitWindowSizeBlock)(C // Refer to examples/SynchronousConcurrency, AsyncViewController.m @property (nonatomic, assign) BOOL neverShowPlaceholders; - -/** - * The constrained size used to measure the backing node. - * - * @discussion Defaults to providing a size range that uses the view controller view's bounds as - * both the min and max definitions. Override this method to provide a custom size range to the - * backing node. - */ -- (ASSizeRange)nodeConstrainedSize AS_WARN_UNUSED_RESULT; - @end @interface ASViewController (ASRangeControllerUpdateRangeProtocol) @@ -90,6 +80,19 @@ typedef ASTraitCollection * _Nonnull (^ASDisplayTraitsForTraitWindowSizeBlock)(C @end +@interface ASViewController (Deprecated) + +/** + * The constrained size used to measure the backing node. + * + * @discussion Defaults to providing a size range that uses the view controller view's bounds as + * both the min and max definitions. Override this method to provide a custom size range to the + * backing node. + */ +- (ASSizeRange)nodeConstrainedSize AS_WARN_UNUSED_RESULT ASDISPLAYNODE_DEPRECATED_MSG("Set the size directly to the view's frame"); + +@end + @interface ASViewController (Unavailable) - (instancetype)initWithNibName:(NSString *)nibNameOrNil bundle:(NSBundle *)nibBundleOrNil AS_UNAVAILABLE("ASViewController requires using -initWithNode:"); diff --git a/AsyncDisplayKit/ASViewController.mm b/AsyncDisplayKit/ASViewController.mm index 2a4c922889..1e9d1045c4 100644 --- a/AsyncDisplayKit/ASViewController.mm +++ b/AsyncDisplayKit/ASViewController.mm @@ -60,6 +60,21 @@ _selfConformsToRangeModeProtocol = [self conformsToProtocol:@protocol(ASRangeControllerUpdateRangeProtocol)]; _nodeConformsToRangeModeProtocol = [_node conformsToProtocol:@protocol(ASRangeControllerUpdateRangeProtocol)]; _automaticallyAdjustRangeModeBasedOnViewEvents = _selfConformsToRangeModeProtocol || _nodeConformsToRangeModeProtocol; + + // In case the node will get loaded + if (_node.nodeLoaded) { + // Node already loaded the view + [self view]; + } else { + // If the node didn't load yet add ourselves as on did load observer to laod the view in case the node gets loaded + // before the view controller + __weak __typeof__(self) weakSelf = self; + [_node onDidLoad:^(__kindof ASDisplayNode * _Nonnull node) { + if ([weakSelf isViewLoaded] == NO) { + [weakSelf view]; + } + }]; + } return self; } @@ -119,11 +134,12 @@ [self progagateNewEnvironmentTraitCollection:environmentTraitCollection]; }]; } else { +#pragma clang diagnostic push +#pragma clang diagnostic ignored "-Wdeprecated-declarations" + // Call layoutThatFits: to let the node prepare for a layout that will happen shortly in the layout pass of the view. + // If the node's constrained size didn't change between the last layout pass it's a no-op [_node layoutThatFits:[self nodeConstrainedSize]]; - } - - if (!AS_AT_LEAST_IOS9) { - [self _legacyHandleViewDidLayoutSubviews]; +#pragma clang diagnostic pop } } @@ -143,12 +159,10 @@ ASVisibilityDidMoveToParentViewController; [super viewWillAppear:animated]; _ensureDisplayed = YES; - // We do this early layout because we need to get any ASCollectionNodes etc. into the - // hierarchy before UIKit applies the scroll view inset adjustments, if you are using - // automatic subnode management. - [_node layoutThatFits:[self nodeConstrainedSize]]; - - [_node recursivelyFetchData]; + // A layout pass is forced this early to get nodes like ASCollectionNode, ASTableNode etc. + // into the hierarchy before UIKit applies the scroll view inset adjustments, if automatic subnode management + // is enabled. Otherwise the insets would not be applied. + [_node.view layoutIfNeeded]; if (_parentManagesVisibilityDepth == NO) { [self setVisibilityDepth:0]; @@ -227,12 +241,7 @@ ASVisibilityDepthImplementation; - (ASSizeRange)nodeConstrainedSize { - if (AS_AT_LEAST_IOS9) { - CGSize viewSize = self.view.bounds.size; - return ASSizeRangeMake(viewSize); - } else { - return [self _legacyConstrainedSize]; - } + return ASSizeRangeMake(self.view.bounds.size); } - (ASInterfaceState)interfaceState @@ -240,51 +249,6 @@ ASVisibilityDepthImplementation; return _node.interfaceState; } -#pragma mark - Legacy Layout Handling - -- (BOOL)_shouldLayoutTheLegacyWay -{ - BOOL isModalViewController = (self.presentingViewController != nil && self.presentedViewController == nil); - BOOL hasNavigationController = (self.navigationController != nil); - BOOL hasParentViewController = (self.parentViewController != nil); - if (isModalViewController && !hasNavigationController && !hasParentViewController) { - return YES; - } - - // Check if the view controller is a root view controller - BOOL isRootViewController = self.view.window.rootViewController == self; - if (isRootViewController) { - return YES; - } - - return NO; -} - -- (ASSizeRange)_legacyConstrainedSize -{ - // In modal presentation the view does not have the right bounds in iOS7 and iOS8. As workaround using the superviews - // view bounds - UIView *view = self.view; - CGSize viewSize = view.bounds.size; - if ([self _shouldLayoutTheLegacyWay]) { - UIView *superview = view.superview; - if (superview != nil) { - viewSize = superview.bounds.size; - } - } - return ASSizeRangeMake(viewSize, viewSize); -} - -- (void)_legacyHandleViewDidLayoutSubviews -{ - // In modal presentation or as root viw controller the view does not automatic resize in iOS7 and iOS8. - // As workaround we adjust the frame of the view manually - if ([self _shouldLayoutTheLegacyWay]) { - CGSize maxConstrainedSize = [self nodeConstrainedSize].max; - _node.frame = (CGRect){.origin = CGPointZero, .size = maxConstrainedSize}; - } -} - #pragma mark - ASEnvironmentTraitCollection - (ASEnvironmentTraitCollection)environmentTraitCollectionForUITraitCollection:(UITraitCollection *)traitCollection @@ -313,10 +277,12 @@ ASVisibilityDepthImplementation; for (id child in children) { ASEnvironmentStatePropagateDown(child, environmentState.environmentTraitCollection); } - - // once we've propagated all the traits, layout this node. +#pragma clang diagnostic push +#pragma clang diagnostic ignored "-Wdeprecated-declarations" + // Once we've propagated all the traits, layout this node. // Remeasure the node with the latest constrained size – old constrained size may be incorrect. [self.node layoutThatFits:[self nodeConstrainedSize]]; +#pragma clang diagnostic pop } } diff --git a/AsyncDisplayKit/Details/ASChangeSetDataController.mm b/AsyncDisplayKit/Details/ASChangeSetDataController.mm index 864f295a4b..c41820dbb0 100644 --- a/AsyncDisplayKit/Details/ASChangeSetDataController.mm +++ b/AsyncDisplayKit/Details/ASChangeSetDataController.mm @@ -66,7 +66,20 @@ } [self invalidateDataSourceItemCounts]; - [_changeSet markCompletedWithNewItemCounts:[self itemCountsFromDataSource]]; + + // Attempt to mark the update completed. This is when update validation will occur inside the changeset. + // If an invalid update exception is thrown, we catch it and inject our "validationErrorSource" object, + // which is the table/collection node's data source, into the exception reason to help debugging. + @try { + [_changeSet markCompletedWithNewItemCounts:[self itemCountsFromDataSource]]; + } @catch (NSException *e) { + id responsibleDataSource = self.validationErrorSource; + if (e.name == ASCollectionInvalidUpdateException && responsibleDataSource != nil) { + [NSException raise:ASCollectionInvalidUpdateException format:@"%@: %@", [responsibleDataSource class], e.reason]; + } else { + @throw e; + } + } ASDataControllerLogEvent(self, @"triggeredUpdate: %@", _changeSet); diff --git a/AsyncDisplayKit/Details/ASCollectionViewLayoutController.mm b/AsyncDisplayKit/Details/ASCollectionViewLayoutController.mm index cb4b6f77bf..196e17108c 100644 --- a/AsyncDisplayKit/Details/ASCollectionViewLayoutController.mm +++ b/AsyncDisplayKit/Details/ASCollectionViewLayoutController.mm @@ -62,6 +62,15 @@ typedef struct ASRangeGeometry ASRangeGeometry; for (UICollectionViewLayoutAttributes *la in layoutAttributes) { //ASDisplayNodeAssert(![indexPathSet containsObject:la.indexPath], @"Shouldn't already contain indexPath"); + + // Manually filter out elements that don't intersect the range bounds. + // If a layout returns elements outside the requested rect this can be a huge problem. + // For instance in a paging flow, you may only want to preload 3 pages (one center, one on each side) + // but if flow layout includes the 4th page (which it does! as of iOS 9&10), you will preload a 4th + // page as well. + if (CATransform3DIsIdentity(la.transform3D) && CGRectIntersectsRect(la.frame, rangeBounds) == NO) { + continue; + } [indexPathSet addObject:la.indexPath]; } diff --git a/AsyncDisplayKit/Details/ASCollectionViewLayoutInspector.h b/AsyncDisplayKit/Details/ASCollectionViewLayoutInspector.h index 6f2a2cb541..82fb232f05 100644 --- a/AsyncDisplayKit/Details/ASCollectionViewLayoutInspector.h +++ b/AsyncDisplayKit/Details/ASCollectionViewLayoutInspector.h @@ -82,6 +82,7 @@ extern ASSizeRange NodeConstrainedSizeForScrollDirection(ASCollectionView *colle @end + NS_ASSUME_NONNULL_END -#endif +#endif \ No newline at end of file diff --git a/AsyncDisplayKit/Details/ASCollectionViewLayoutInspector.m b/AsyncDisplayKit/Details/ASCollectionViewLayoutInspector.m index 3bedc6578a..49540663b1 100644 --- a/AsyncDisplayKit/Details/ASCollectionViewLayoutInspector.m +++ b/AsyncDisplayKit/Details/ASCollectionViewLayoutInspector.m @@ -95,4 +95,4 @@ ASSizeRange NodeConstrainedSizeForScrollDirection(ASCollectionView *collectionVi @end -#endif +#endif \ No newline at end of file diff --git a/AsyncDisplayKit/Details/ASDataController.h b/AsyncDisplayKit/Details/ASDataController.h index 00af36fde6..d7beb4f23f 100644 --- a/AsyncDisplayKit/Details/ASDataController.h +++ b/AsyncDisplayKit/Details/ASDataController.h @@ -34,7 +34,8 @@ typedef NSUInteger ASDataControllerAnimationOptions; */ typedef ASCellNode * _Nonnull(^ASCellNodeBlock)(); -FOUNDATION_EXPORT NSString * const ASDataControllerRowNodeKind; +extern NSString * const ASDataControllerRowNodeKind; +extern NSString * const ASCollectionInvalidUpdateException; /** Data source for data controller @@ -122,6 +123,11 @@ FOUNDATION_EXPORT NSString * const ASDataControllerRowNodeKind; */ @property (nonatomic, weak, readonly) id dataSource; +/** + An object that will be included in the backtrace of any update validation exceptions that occur. + */ +@property (nonatomic, weak) id validationErrorSource; + /** Delegate to notify when data is updated. */ diff --git a/AsyncDisplayKit/Details/ASDataController.mm b/AsyncDisplayKit/Details/ASDataController.mm index 261cc62efa..83c6b8af4e 100644 --- a/AsyncDisplayKit/Details/ASDataController.mm +++ b/AsyncDisplayKit/Details/ASDataController.mm @@ -38,6 +38,7 @@ const static char * kASDataControllerEditingQueueKey = "kASDataControllerEditing const static char * kASDataControllerEditingQueueContext = "kASDataControllerEditingQueueContext"; NSString * const ASDataControllerRowNodeKind = @"_ASDataControllerRowNodeKind"; +NSString * const ASCollectionInvalidUpdateException = @"ASCollectionInvalidUpdateException"; #if AS_MEASURE_AVOIDED_DATACONTROLLER_WORK @interface ASDataController (AvoidedWorkMeasuring) diff --git a/AsyncDisplayKit/Details/ASImageProtocols.h b/AsyncDisplayKit/Details/ASImageProtocols.h index 721f8faeea..159a1084f9 100644 --- a/AsyncDisplayKit/Details/ASImageProtocols.h +++ b/AsyncDisplayKit/Details/ASImageProtocols.h @@ -55,7 +55,7 @@ typedef void(^ASImageCacherCompletion)(id _Nullable i completion:(ASImageCacherCompletion)completion; /** - @abstract Called during clearFetchedData. Allows the cache to optionally trim items. + @abstract Called during clearPreloadedData. Allows the cache to optionally trim items. @note Depending on your caches implementation you may *not* wish to respond to this method. It is however useful if you have a memory and disk cache in which case you'll likely want to clear out the memory cache. */ diff --git a/AsyncDisplayKit/Details/ASLayoutRangeType.h b/AsyncDisplayKit/Details/ASLayoutRangeType.h index d978da4580..068808ecdc 100644 --- a/AsyncDisplayKit/Details/ASLayoutRangeType.h +++ b/AsyncDisplayKit/Details/ASLayoutRangeType.h @@ -31,8 +31,8 @@ typedef NS_ENUM(NSUInteger, ASLayoutRangeMode) { ASLayoutRangeModeFull, /** - * Visible Only mode is used when a range controller should set its display and fetch data regions to only the size of their bounds. - * This causes all additional backing stores & fetched data to be released, while ensuring a user revisiting the view will + * Visible Only mode is used when a range controller should set its display and preload regions to only the size of their bounds. + * This causes all additional backing stores & preloaded data to be released, while ensuring a user revisiting the view will * still be able to see the expected content. This mode is automatically set on all ASRangeControllers when the app suspends, * allowing the operating system to keep the app alive longer and increase the chance it is still warm when the user returns. */ @@ -40,7 +40,7 @@ typedef NS_ENUM(NSUInteger, ASLayoutRangeMode) { /** * Low Memory mode is used when a range controller should discard ALL graphics buffers, including for the area that would be visible - * the next time the user views it (bounds). The only range it preserves is Fetch Data, which is limited to the bounds, allowing + * the next time the user views it (bounds). The only range it preserves is Preload, which is limited to the bounds, allowing * the content to be restored relatively quickly by re-decoding images (the compressed images are ~10% the size of the decoded ones, * and text is a tiny fraction of its rendered size). */ diff --git a/AsyncDisplayKit/Details/ASPINRemoteImageDownloader.m b/AsyncDisplayKit/Details/ASPINRemoteImageDownloader.m index 52762c3f02..0fa8a2da93 100644 --- a/AsyncDisplayKit/Details/ASPINRemoteImageDownloader.m +++ b/AsyncDisplayKit/Details/ASPINRemoteImageDownloader.m @@ -105,15 +105,10 @@ static ASPINRemoteImageDownloader *sharedDownloader = nil; + (void)setSharedImageManagerWithConfiguration:(nullable NSURLSessionConfiguration *)configuration { NSAssert(sharedDownloader == nil, @"Singleton has been created and session can no longer be configured."); - __unused PINRemoteImageManager *sharedManager = [[self class] sharedPINRemoteImageManagerWithConfiguration:configuration]; + __unused PINRemoteImageManager *sharedManager = [self sharedPINRemoteImageManagerWithConfiguration:configuration]; } -- (PINRemoteImageManager *)sharedPINRemoteImageManager -{ - return [self sharedPINRemoteImageManagerWithConfiguration:nil]; -} - -- (PINRemoteImageManager *)sharedPINRemoteImageManagerWithConfiguration:(NSURLSessionConfiguration *)configuration ++ (PINRemoteImageManager *)sharedPINRemoteImageManagerWithConfiguration:(NSURLSessionConfiguration *)configuration { static ASPINRemoteImageManager *sharedPINRemoteImageManager; static dispatch_once_t onceToken; @@ -135,7 +130,8 @@ static ASPINRemoteImageDownloader *sharedDownloader = nil; userInfo:nil]; @throw e; } - sharedPINRemoteImageManager = [[ASPINRemoteImageManager alloc] initWithSessionConfiguration:configuration alternativeRepresentationProvider:self]; + sharedPINRemoteImageManager = [[ASPINRemoteImageManager alloc] initWithSessionConfiguration:configuration + alternativeRepresentationProvider:[self sharedDownloader]]; #else sharedPINRemoteImageManager = [[ASPINRemoteImageManager alloc] initWithSessionConfiguration:configuration]; #endif @@ -143,6 +139,11 @@ static ASPINRemoteImageDownloader *sharedDownloader = nil; return sharedPINRemoteImageManager; } +- (PINRemoteImageManager *)sharedPINRemoteImageManager +{ + return [ASPINRemoteImageDownloader sharedPINRemoteImageManagerWithConfiguration:nil]; +} + - (BOOL)sharedImageManagerSupportsMemoryRemoval { static BOOL sharedImageManagerSupportsMemoryRemoval = NO; diff --git a/AsyncDisplayKit/Details/ASRangeController.h b/AsyncDisplayKit/Details/ASRangeController.h index 1434b716a0..c4ea07be82 100644 --- a/AsyncDisplayKit/Details/ASRangeController.h +++ b/AsyncDisplayKit/Details/ASRangeController.h @@ -71,7 +71,7 @@ NS_ASSUME_NONNULL_BEGIN // These methods call the corresponding method on each node, visiting each one that // the range controller has set a non-default interface state on. - (void)clearContents; -- (void)clearFetchedData; +- (void)clearPreloadedData; /** * An object that describes the layout behavior of the ranged component (table view, collection view, etc.) @@ -161,13 +161,6 @@ NS_ASSUME_NONNULL_BEGIN */ - (void)rangeController:(ASRangeController * )rangeController didEndUpdatesAnimated:(BOOL)animated completion:(void (^)(BOOL))completion; -/** - * Completed updates to cell node addition and removal. - * - * @param rangeController Sender. - */ -- (void)didCompleteUpdatesInRangeController:(ASRangeController *)rangeController; - /** * Called for nodes insertion. * diff --git a/AsyncDisplayKit/Details/ASRangeController.mm b/AsyncDisplayKit/Details/ASRangeController.mm index 14438a8116..458d33be79 100644 --- a/AsyncDisplayKit/Details/ASRangeController.mm +++ b/AsyncDisplayKit/Details/ASRangeController.mm @@ -26,6 +26,10 @@ #define AS_RANGECONTROLLER_LOG_UPDATE_FREQ 0 +#ifndef ASRangeControllerAutomaticLowMemoryHandling +#define ASRangeControllerAutomaticLowMemoryHandling 1 +#endif + @interface ASRangeController () { BOOL _rangeIsValid; @@ -34,9 +38,9 @@ BOOL _layoutControllerImplementsSetViewportSize; NSSet *_allPreviousIndexPaths; ASLayoutRangeMode _currentRangeMode; - BOOL _didUpdateCurrentRange; + BOOL _preserveCurrentRangeMode; BOOL _didRegisterForNodeDisplayNotifications; - CFAbsoluteTime _pendingDisplayNodesTimestamp; + CFTimeInterval _pendingDisplayNodesTimestamp; #if AS_RANGECONTROLLER_LOG_UPDATE_FREQ NSUInteger _updateCountThisFrame; @@ -60,7 +64,7 @@ static UIApplicationState __ApplicationState = UIApplicationStateActive; _rangeIsValid = YES; _currentRangeMode = ASLayoutRangeModeInvalid; - _didUpdateCurrentRange = NO; + _preserveCurrentRangeMode = NO; [[[self class] allRangeControllersWeakSet] addObject:self]; @@ -142,9 +146,9 @@ static UIApplicationState __ApplicationState = UIApplicationStateActive; - (void)updateCurrentRangeWithMode:(ASLayoutRangeMode)rangeMode { + _preserveCurrentRangeMode = YES; if (_currentRangeMode != rangeMode) { _currentRangeMode = rangeMode; - _didUpdateCurrentRange = YES; [self setNeedsUpdate]; } @@ -218,7 +222,7 @@ static UIApplicationState __ApplicationState = UIApplicationStateActive; ASLayoutRangeMode rangeMode = _currentRangeMode; // If the range mode is explicitly set via updateCurrentRangeWithMode: it will last in that mode until the // range controller becomes visible again or explicitly changes the range mode again - if ((!_didUpdateCurrentRange && ASInterfaceStateIncludesVisible(selfInterfaceState)) || [[self class] isFirstRangeUpdateForRangeMode:rangeMode]) { + if ((!_preserveCurrentRangeMode && ASInterfaceStateIncludesVisible(selfInterfaceState)) || [[self class] isFirstRangeUpdateForRangeMode:rangeMode]) { rangeMode = [ASRangeController rangeModeForInterfaceState:selfInterfaceState currentRangeMode:_currentRangeMode]; } @@ -248,7 +252,7 @@ static UIApplicationState __ApplicationState = UIApplicationStateActive; // Typically the preloadIndexPaths will be the largest, and be a superset of the others, though it may be disjoint. // Because allIndexPaths is an NSMutableOrderedSet, this adds the non-duplicate items /after/ the existing items. - // This means that during iteration, we will first visit visible, then display, then fetch data nodes. + // This means that during iteration, we will first visit visible, then display, then preload nodes. [allIndexPaths unionSet:displayIndexPaths]; [allIndexPaths unionSet:preloadIndexPaths]; @@ -261,7 +265,7 @@ static UIApplicationState __ApplicationState = UIApplicationStateActive; _allPreviousIndexPaths = allCurrentIndexPaths; _currentRangeMode = rangeMode; - _didUpdateCurrentRange = NO; + _preserveCurrentRangeMode = NO; if (!_rangeIsValid) { [allIndexPaths addObjectsFromArray:ASIndexPathsForTwoDimensionalArray(allNodes)]; @@ -290,14 +294,14 @@ static UIApplicationState __ApplicationState = UIApplicationStateActive; } } else { // If selfInterfaceState isn't visible, then visibleIndexPaths represents what /will/ be immediately visible at the - // instant we come onscreen. So, fetch data and display all of those things, but don't waste resources preloading yet. + // instant we come onscreen. So, preload and display all of those things, but don't waste resources preloading yet. // We handle this as a separate case to minimize set operations for offscreen preloading, including containsObject:. if ([allCurrentIndexPaths containsObject:indexPath]) { // DO NOT set Visible: even though these elements are in the visible range / "viewport", // our overall container object is itself not visible yet. The moment it becomes visible, we will run the condition above - // Set Layout, Fetch Data + // Set Layout, Preload interfaceState |= ASInterfaceStatePreload; if (rangeMode != ASLayoutRangeModeLowMemory) { @@ -337,7 +341,7 @@ static UIApplicationState __ApplicationState = UIApplicationStateActive; if (nodeShouldScheduleDisplay) { [self registerForNodeDisplayNotificationsForInterfaceStateIfNeeded:selfInterfaceState]; if (_didRegisterForNodeDisplayNotifications) { - _pendingDisplayNodesTimestamp = CFAbsoluteTimeGetCurrent(); + _pendingDisplayNodesTimestamp = CACurrentMediaTime(); } } } @@ -377,7 +381,6 @@ static UIApplicationState __ApplicationState = UIApplicationStateActive; [modifiedIndexPaths sortUsingSelector:@selector(compare:)]; NSLog(@"Range update complete; modifiedIndexPaths: %@", [self descriptionWithIndexPaths:modifiedIndexPaths]); #endif - [_delegate didCompleteUpdatesInRangeController:self]; ASProfilingSignpostEnd(1, self); } @@ -500,7 +503,7 @@ static UIApplicationState __ApplicationState = UIApplicationStateActive; } } -- (void)clearFetchedData +- (void)clearPreloadedData { for (NSArray *section in [_dataSource completedNodes]) { for (ASDisplayNode *node in section) { @@ -534,7 +537,7 @@ static UIApplicationState __ApplicationState = UIApplicationStateActive; [center addObserver:self selector:@selector(willEnterForeground:) name:UIApplicationWillEnterForegroundNotification object:nil]; } -static ASLayoutRangeMode __rangeModeForMemoryWarnings = ASLayoutRangeModeVisibleOnly; +static ASLayoutRangeMode __rangeModeForMemoryWarnings = ASLayoutRangeModeLowMemory; + (void)setRangeModeForMemoryWarnings:(ASLayoutRangeMode)rangeMode { ASDisplayNodeAssert(rangeMode == ASLayoutRangeModeVisibleOnly || rangeMode == ASLayoutRangeModeLowMemory, @"It is highly inadvisable to engage a larger range mode when a memory warning occurs, as this will almost certainly cause app eviction"); @@ -546,8 +549,8 @@ static ASLayoutRangeMode __rangeModeForMemoryWarnings = ASLayoutRangeModeVisible NSArray *allRangeControllers = [[self allRangeControllersWeakSet] allObjects]; for (ASRangeController *rangeController in allRangeControllers) { BOOL isDisplay = ASInterfaceStateIncludesDisplay([rangeController interfaceState]); - [rangeController updateCurrentRangeWithMode:isDisplay ? ASLayoutRangeModeMinimum : __rangeModeForMemoryWarnings]; - [rangeController setNeedsUpdate]; + [rangeController updateCurrentRangeWithMode:isDisplay ? ASLayoutRangeModeVisibleOnly : __rangeModeForMemoryWarnings]; + // There's no need to call needs update as updateCurrentRangeWithMode sets this if necessary. [rangeController updateIfNeeded]; } @@ -570,7 +573,7 @@ static ASLayoutRangeMode __rangeModeForMemoryWarnings = ASLayoutRangeModeVisible __ApplicationState = UIApplicationStateBackground; 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 setNeedsUpdate]; + // There's no need to call needs update as updateCurrentRangeWithMode sets this if necessary. [rangeController updateIfNeeded]; } @@ -586,7 +589,7 @@ static ASLayoutRangeMode __rangeModeForMemoryWarnings = ASLayoutRangeModeVisible for (ASRangeController *rangeController in allRangeControllers) { BOOL isVisible = ASInterfaceStateIncludesVisible([rangeController interfaceState]); [rangeController updateCurrentRangeWithMode:isVisible ? ASLayoutRangeModeMinimum : ASLayoutRangeModeVisibleOnly]; - [rangeController setNeedsUpdate]; + // There's no need to call needs update as updateCurrentRangeWithMode sets this if necessary. [rangeController updateIfNeeded]; } diff --git a/AsyncDisplayKit/Details/_ASDisplayLayer.h b/AsyncDisplayKit/Details/_ASDisplayLayer.h index 6cd40af080..a518f61828 100644 --- a/AsyncDisplayKit/Details/_ASDisplayLayer.h +++ b/AsyncDisplayKit/Details/_ASDisplayLayer.h @@ -103,7 +103,7 @@ typedef BOOL(^asdisplaynode_iscancelled_block_t)(void); @param isCancelledBlock Execute this block to check whether the current drawing operation has been cancelled to avoid unnecessary work. A return value of YES means cancel drawing and return. @param isRasterizing YES if the layer is being rasterized into another layer, in which case drawRect: probably wants to avoid doing things like filling its bounds with a zero-alpha color to clear the backing store. */ -+ (void)drawRect:(CGRect)bounds withParameters:(id)parameters isCancelled:(asdisplaynode_iscancelled_block_t)isCancelledBlock isRasterizing:(BOOL)isRasterizing; ++ (void)drawRect:(CGRect)bounds withParameters:(id)parameters isCancelled:(__attribute((noescape)) asdisplaynode_iscancelled_block_t)isCancelledBlock isRasterizing:(BOOL)isRasterizing; /** @summary Delegate override to provide new layer contents as a UIImage. @@ -111,19 +111,19 @@ typedef BOOL(^asdisplaynode_iscancelled_block_t)(void); @param isCancelledBlock Execute this block to check whether the current drawing operation has been cancelled to avoid unnecessary work. A return value of YES means cancel drawing and return. @return A UIImage with contents that are ready to display on the main thread. Make sure that the image is already decoded before returning it here. */ -+ (UIImage *)displayWithParameters:(id)parameters isCancelled:(asdisplaynode_iscancelled_block_t)isCancelledBlock; ++ (UIImage *)displayWithParameters:(id)parameters isCancelled:(__attribute((noescape)) asdisplaynode_iscancelled_block_t)isCancelledBlock; /** * @abstract instance version of drawRect class method * @see drawRect:withParameters:isCancelled:isRasterizing class method */ -- (void)drawRect:(CGRect)bounds withParameters:(id )parameters isCancelled:(asdisplaynode_iscancelled_block_t)isCancelledBlock isRasterizing:(BOOL)isRasterizing; +- (void)drawRect:(CGRect)bounds withParameters:(id )parameters isCancelled:(__attribute((noescape)) asdisplaynode_iscancelled_block_t)isCancelledBlock isRasterizing:(BOOL)isRasterizing; /** * @abstract instance version of display class method * @see displayWithParameters:isCancelled class method */ -- (UIImage *)displayWithParameters:(id )parameters isCancelled:(asdisplaynode_iscancelled_block_t)isCancelled; +- (UIImage *)displayWithParameters:(id )parameters isCancelled:(__attribute((noescape)) asdisplaynode_iscancelled_block_t)isCancelled; // Called on the main thread only diff --git a/AsyncDisplayKit/Details/_ASDisplayView.mm b/AsyncDisplayKit/Details/_ASDisplayView.mm index 4068d5e4a8..5742566538 100644 --- a/AsyncDisplayKit/Details/_ASDisplayView.mm +++ b/AsyncDisplayKit/Details/_ASDisplayView.mm @@ -16,6 +16,7 @@ #import "ASDisplayNode+FrameworkPrivate.h" #import "ASDisplayNode+Subclasses.h" #import "ASObjectDescriptionHelpers.h" +#import "ASLayout.h" @interface _ASDisplayView () @property (nullable, atomic, weak, readwrite) ASDisplayNode *asyncdisplaykit_node; @@ -202,6 +203,12 @@ #endif } +- (CGSize)sizeThatFits:(CGSize)size +{ + ASDisplayNode *node = _asyncdisplaykit_node; // Create strong reference to weak ivar. + return node ? [node layoutThatFits:ASSizeRangeMake(size)].size : [super sizeThatFits:size]; +} + - (void)setNeedsDisplay { ASDisplayNodeAssertMainThread(); diff --git a/AsyncDisplayKit/Private/ASBatchFetching.h b/AsyncDisplayKit/Private/ASBatchFetching.h index 8ede3218bd..6eca8a5940 100644 --- a/AsyncDisplayKit/Private/ASBatchFetching.h +++ b/AsyncDisplayKit/Private/ASBatchFetching.h @@ -31,16 +31,18 @@ ASDISPLAYNODE_EXTERN_C_BEGIN * ASCollectionView batch fetching API. @param context The scroll view that in-flight fetches are happening. @param scrollDirection The current scrolling direction of the scroll view. + @param scrollableDirections The possible scrolling directions of the scroll view. @param targetOffset The offset that the scrollview will scroll to. @return Whether or not the current state should proceed with batch fetching. */ -BOOL ASDisplayShouldFetchBatchForScrollView(UIScrollView *scrollView, ASScrollDirection scrollDirection, CGPoint contentOffset); +BOOL ASDisplayShouldFetchBatchForScrollView(UIScrollView *scrollView, ASScrollDirection scrollDirection, ASScrollDirection scrollableDirections, CGPoint contentOffset); /** @abstract Determine if batch fetching should begin based on the state of the parameters. @param context The batch fetching context that contains knowledge about in-flight fetches. @param scrollDirection The current scrolling direction of the scroll view. + @param scrollableDirections The possible scrolling directions of the scroll view. @param bounds The bounds of the scrollview. @param contentSize The content size of the scrollview. @param targetOffset The offset that the scrollview will scroll to. @@ -51,6 +53,7 @@ BOOL ASDisplayShouldFetchBatchForScrollView(UIScrollView *scrollView, ASScrollDirection scrollDirection, CGPoint contentOffset) +BOOL ASDisplayShouldFetchBatchForScrollView(UIScrollView *scrollView, ASScrollDirection scrollDirection, ASScrollDirection scrollableDirections, CGPoint contentOffset) { // Don't fetch if the scroll view does not allow if (![scrollView canBatchFetch]) { @@ -24,11 +24,12 @@ BOOL ASDisplayShouldFetchBatchForScrollView(UIScrollView _calculatedDisplayNodeLayout; + std::shared_ptr _pendingDisplayNodeLayout; ASDisplayNodeViewBlock _viewBlock; ASDisplayNodeLayerBlock _layerBlock; @@ -188,12 +191,13 @@ FOUNDATION_EXPORT NSString * const ASRenderingEngineDidDisplayNodesScheduledBefo + (void)scheduleNodeForRecursiveDisplay:(ASDisplayNode *)node; -// The _ASDisplayLayer backing the node, if any. +/// The _ASDisplayLayer backing the node, if any. @property (nonatomic, readonly, strong) _ASDisplayLayer *asyncLayer; -// Bitmask to check which methods an object overrides. +/// Bitmask to check which methods an object overrides. @property (nonatomic, assign, readonly) ASDisplayNodeMethodOverrides methodOverrides; +/// Thread safe way to access the bounds of the node @property (nonatomic, assign) CGRect threadSafeBounds; @@ -201,21 +205,28 @@ FOUNDATION_EXPORT NSString * const ASRenderingEngineDidDisplayNodesScheduledBefo - (BOOL)__shouldLoadViewOrLayer; /** - Invoked before a call to setNeedsLayout to the underlying view + * Invoked before a call to setNeedsLayout to the underlying view */ - (void)__setNeedsLayout; /** - Invoked after a call to setNeedsDisplay to the underlying view + * Invoked after a call to setNeedsDisplay to the underlying view */ - (void)__setNeedsDisplay; +/** + * Called from [CALayer layoutSublayers:]. Executes the layout pass for the node + */ - (void)__layout; + +/* + * Internal method to set the supernode + */ - (void)__setSupernode:(ASDisplayNode *)supernode; /** - Internal method to add / replace / insert subnode and remove from supernode without checking if - node has automaticallyManagesSubnodes set to YES. + * Internal method to add / replace / insert subnode and remove from supernode without checking if + * node has automaticallyManagesSubnodes set to YES. */ - (void)_addSubnode:(ASDisplayNode *)subnode; - (void)_replaceSubnode:(ASDisplayNode *)oldSubnode withSubnode:(ASDisplayNode *)replacementSubnode; @@ -230,16 +241,16 @@ FOUNDATION_EXPORT NSString * const ASRenderingEngineDidDisplayNodesScheduledBefo - (void)__incrementVisibilityNotificationsDisabled; - (void)__decrementVisibilityNotificationsDisabled; -// Helper method to summarize whether or not the node run through the display process +/// Helper method to summarize whether or not the node run through the display process - (BOOL)__implementsDisplay; -// Display the node's view/layer immediately on the current thread, bypassing the background thread rendering. Will be deprecated. +/// Display the node's view/layer immediately on the current thread, bypassing the background thread rendering. Will be deprecated. - (void)displayImmediately; -// Alternative initialiser for backing with a custom view class. Supports asynchronous display with _ASDisplayView subclasses. +/// Alternative initialiser for backing with a custom view class. Supports asynchronous display with _ASDisplayView subclasses. - (instancetype)initWithViewClass:(Class)viewClass; -// Alternative initialiser for backing with a custom layer class. Supports asynchronous display with _ASDisplayLayer subclasses. +/// Alternative initialiser for backing with a custom layer class. Supports asynchronous display with _ASDisplayLayer subclasses. - (instancetype)initWithLayerClass:(Class)layerClass; @property (nonatomic, assign) CGFloat contentsScaleForDisplay; diff --git a/AsyncDisplayKit/Private/ASDisplayNodeLayout.h b/AsyncDisplayKit/Private/ASDisplayNodeLayout.h index 64837acc25..a9a2d5a8a7 100644 --- a/AsyncDisplayKit/Private/ASDisplayNodeLayout.h +++ b/AsyncDisplayKit/Private/ASDisplayNodeLayout.h @@ -24,6 +24,7 @@ struct ASDisplayNodeLayout { ASLayout *layout; ASSizeRange constrainedSize; CGSize parentSize; + BOOL requestedLayoutFromAbove; BOOL _dirty; /* @@ -33,13 +34,13 @@ struct ASDisplayNodeLayout { * @param parentSize Parent size used to create the layout */ ASDisplayNodeLayout(ASLayout *layout, ASSizeRange constrainedSize, CGSize parentSize) - : layout(layout), constrainedSize(constrainedSize), parentSize(parentSize), _dirty(NO) {}; + : layout(layout), constrainedSize(constrainedSize), parentSize(parentSize), requestedLayoutFromAbove(NO), _dirty(NO) {}; /* * Creates a layout without any layout associated. By default this display node layout is dirty. */ ASDisplayNodeLayout() - : layout(nil), constrainedSize({{0, 0}, {0, 0}}), parentSize({0, 0}), _dirty(YES) {}; + : layout(nil), constrainedSize({{0, 0}, {0, 0}}), parentSize({0, 0}), requestedLayoutFromAbove(NO), _dirty(YES) {}; /** * Returns if the display node layout is dirty as it was invalidated or it was created without a layout. diff --git a/AsyncDisplayKit/Private/ASInternalHelpers.h b/AsyncDisplayKit/Private/ASInternalHelpers.h index e00c336a80..ec650bbadc 100644 --- a/AsyncDisplayKit/Private/ASInternalHelpers.h +++ b/AsyncDisplayKit/Private/ASInternalHelpers.h @@ -33,8 +33,12 @@ void ASPerformBackgroundDeallocation(id object); CGFloat ASScreenScale(); +CGSize ASFloorSizeValues(CGSize s); + CGFloat ASFloorPixelValue(CGFloat f); +CGSize ASCeilSizeValues(CGSize s); + CGFloat ASCeilPixelValue(CGFloat f); CGFloat ASRoundPixelValue(CGFloat f); diff --git a/AsyncDisplayKit/Private/ASInternalHelpers.m b/AsyncDisplayKit/Private/ASInternalHelpers.m index 66e7516f66..4a78f14d65 100644 --- a/AsyncDisplayKit/Private/ASInternalHelpers.m +++ b/AsyncDisplayKit/Private/ASInternalHelpers.m @@ -137,12 +137,22 @@ CGFloat ASScreenScale() return __scale; } +CGSize ASFloorSizeValues(CGSize s) +{ + return CGSizeMake(ASFloorPixelValue(s.width), ASFloorPixelValue(s.height)); +} + CGFloat ASFloorPixelValue(CGFloat f) { CGFloat scale = ASScreenScale(); return floor(f * scale) / scale; } +CGSize ASCeilSizeValues(CGSize s) +{ + return CGSizeMake(ASCeilPixelValue(s.width), ASCeilPixelValue(s.height)); +} + CGFloat ASCeilPixelValue(CGFloat f) { CGFloat scale = ASScreenScale(); diff --git a/AsyncDisplayKit/Private/ASLayoutTransition.mm b/AsyncDisplayKit/Private/ASLayoutTransition.mm index 9c8df8cbd2..e22601f6ea 100644 --- a/AsyncDisplayKit/Private/ASLayoutTransition.mm +++ b/AsyncDisplayKit/Private/ASLayoutTransition.mm @@ -233,7 +233,7 @@ static inline std::vector findNodesInLayoutAtIndexesWithFilteredNode if (idx > lastIndex) { break; } if (idx >= firstIndex && [indexes containsIndex:idx]) { ASDisplayNode *node = (ASDisplayNode *)sublayout.layoutElement; - ASDisplayNodeCAssert(node, @"A flattened layout must consist exclusively of node sublayouts"); + ASDisplayNodeCAssert(node, @"ASDisplayNode was deallocated before it was added to a subnode. It's likely the case that you use automatically manages subnodes and allocate a ASDisplayNode in layoutSpecThatFits: and don't have any strong reference to it."); // Ignore the odd case in which a non-node sublayout is accessed and the type cast fails if (node != nil) { BOOL notFiltered = (filteredNodes == nil || [filteredNodes indexOfObjectIdenticalTo:node] == NSNotFound); diff --git a/AsyncDisplayKit/Private/ASStackPositionedLayout.mm b/AsyncDisplayKit/Private/ASStackPositionedLayout.mm index c38cf797bf..1fdef8eb0b 100644 --- a/AsyncDisplayKit/Private/ASStackPositionedLayout.mm +++ b/AsyncDisplayKit/Private/ASStackPositionedLayout.mm @@ -118,12 +118,13 @@ ASStackPositionedLayout ASStackPositionedLayout::compute(const ASStackUnposition return stackedLayout(style, violation, unpositionedLayout, constrainedSize); } case ASStackLayoutJustifyContentSpaceBetween: { + // Spacing between the items, no spaces at the edges, evenly distributed const auto numOfSpacings = numOfItems - 1; - return stackedLayout(style, 0, std::floor(violation / numOfSpacings), std::fmod(violation, numOfSpacings), unpositionedLayout, constrainedSize); + return stackedLayout(style, 0, violation / numOfSpacings, 0, unpositionedLayout, constrainedSize); } case ASStackLayoutJustifyContentSpaceAround: { // Spacing between items are twice the spacing on the edges - CGFloat spacingUnit = std::floor(violation / (numOfItems * 2)); + CGFloat spacingUnit = violation / (numOfItems * 2); return stackedLayout(style, spacingUnit, spacingUnit * 2, 0, unpositionedLayout, constrainedSize); } } diff --git a/AsyncDisplayKit/Private/_ASCoreAnimationExtras.h b/AsyncDisplayKit/Private/_ASCoreAnimationExtras.h index 2998ea5f5a..77087d2a83 100644 --- a/AsyncDisplayKit/Private/_ASCoreAnimationExtras.h +++ b/AsyncDisplayKit/Private/_ASCoreAnimationExtras.h @@ -53,4 +53,11 @@ extern UIViewContentMode ASDisplayNodeUIContentModeFromCAContentsGravity(NSStrin */ extern UIImage *ASDisplayNodeStretchableBoxContentsWithColor(UIColor *color, CGSize innerSize); +/** + Checks whether a layer has ongoing animations + @param layer A layer to check if animations are ongoing + @return YES if the layer has ongoing animations, otherwise NO + */ +extern BOOL ASDisplayNodeLayerHasAnimations(CALayer *layer); + ASDISPLAYNODE_EXTERN_C_END diff --git a/AsyncDisplayKit/Private/_ASCoreAnimationExtras.mm b/AsyncDisplayKit/Private/_ASCoreAnimationExtras.mm index 64eea95892..cfab3b2629 100644 --- a/AsyncDisplayKit/Private/_ASCoreAnimationExtras.mm +++ b/AsyncDisplayKit/Private/_ASCoreAnimationExtras.mm @@ -154,3 +154,8 @@ UIViewContentMode ASDisplayNodeUIContentModeFromCAContentsGravity(NSString *cons // If asserts disabled, fall back to this return UIViewContentModeScaleToFill; } + +BOOL ASDisplayNodeLayerHasAnimations(CALayer *layer) +{ + return (layer.animationKeys.count != 0); +} diff --git a/AsyncDisplayKit/Private/_ASHierarchyChangeSet.mm b/AsyncDisplayKit/Private/_ASHierarchyChangeSet.mm index d46f2acca9..e88551500c 100644 --- a/AsyncDisplayKit/Private/_ASHierarchyChangeSet.mm +++ b/AsyncDisplayKit/Private/_ASHierarchyChangeSet.mm @@ -17,15 +17,25 @@ #import "ASDisplayNode+Beta.h" #import "ASObjectDescriptionHelpers.h" #import +#import "ASDataController.h" +#import "ASBaseDefines.h" -// NOTE: We log before throwing so they don't have to let it bubble up to see the error. -#define ASFailUpdateValidation(...)\ - if ([ASDisplayNode suppressesInvalidCollectionUpdateExceptions]) {\ - NSLog(__VA_ARGS__);\ - } else {\ - NSLog(__VA_ARGS__);\ - ASDisplayNodeFailAssert(__VA_ARGS__);\ - } +// If assertions are enabled and they haven't forced us to suppress the exception, +// then throw, otherwise log. +#if ASDISPLAYNODE_ASSERTIONS_ENABLED + #define ASFailUpdateValidation(...)\ + _Pragma("clang diagnostic push")\ + _Pragma("clang diagnostic ignored \"-Wdeprecated-declarations\"")\ + if ([ASDisplayNode suppressesInvalidCollectionUpdateExceptions]) {\ + NSLog(__VA_ARGS__);\ + } else {\ + NSLog(__VA_ARGS__);\ + [NSException raise:ASCollectionInvalidUpdateException format:__VA_ARGS__];\ + }\ + _Pragma("clang diagnostic pop") +#else + #define ASFailUpdateValidation(...) NSLog(__VA_ARGS__); +#endif BOOL ASHierarchyChangeTypeIsFinal(_ASHierarchyChangeType changeType) { switch (changeType) { diff --git a/AsyncDisplayKitTests/ASBatchFetchingTests.m b/AsyncDisplayKitTests/ASBatchFetchingTests.m index a4c0b50209..3a5804ead7 100644 --- a/AsyncDisplayKitTests/ASBatchFetchingTests.m +++ b/AsyncDisplayKitTests/ASBatchFetchingTests.m @@ -21,35 +21,35 @@ #define PASSING_RECT CGRectMake(0,0,1,1) #define PASSING_SIZE CGSizeMake(1,1) #define PASSING_POINT CGPointMake(1,1) -#define VERTICAL_RECT(h) CGRectMake(0,0,0,h) +#define VERTICAL_RECT(h) CGRectMake(0,0,1,h) #define VERTICAL_SIZE(h) CGSizeMake(0,h) #define VERTICAL_OFFSET(y) CGPointMake(0,y) -#define HORIZONTAL_RECT(w) CGRectMake(0,0,w,0) +#define HORIZONTAL_RECT(w) CGRectMake(0,0,w,1) #define HORIZONTAL_SIZE(w) CGSizeMake(w,0) #define HORIZONTAL_OFFSET(x) CGPointMake(x,0) - (void)testBatchNullState { ASBatchContext *context = [[ASBatchContext alloc] init]; - BOOL shouldFetch = ASDisplayShouldFetchBatchForContext(context, ASScrollDirectionDown, CGRectZero, CGSizeZero, CGPointZero, 0.0); + BOOL shouldFetch = ASDisplayShouldFetchBatchForContext(context, ASScrollDirectionDown, ASScrollDirectionVerticalDirections, CGRectZero, CGSizeZero, CGPointZero, 0.0); XCTAssert(shouldFetch == NO, @"Should not fetch in the null state"); } - (void)testBatchAlreadyFetching { ASBatchContext *context = [[ASBatchContext alloc] init]; [context beginBatchFetching]; - BOOL shouldFetch = ASDisplayShouldFetchBatchForContext(context, ASScrollDirectionDown, PASSING_RECT, PASSING_SIZE, PASSING_POINT, 1.0); + BOOL shouldFetch = ASDisplayShouldFetchBatchForContext(context, ASScrollDirectionDown, ASScrollDirectionVerticalDirections, PASSING_RECT, PASSING_SIZE, PASSING_POINT, 1.0); XCTAssert(shouldFetch == NO, @"Should not fetch when context is already fetching"); } - (void)testUnsupportedScrollDirections { ASBatchContext *context = [[ASBatchContext alloc] init]; - BOOL fetchRight = ASDisplayShouldFetchBatchForContext(context, ASScrollDirectionRight, PASSING_RECT, PASSING_SIZE, PASSING_POINT, 1.0); + BOOL fetchRight = ASDisplayShouldFetchBatchForContext(context, ASScrollDirectionRight, ASScrollDirectionHorizontalDirections, PASSING_RECT, PASSING_SIZE, PASSING_POINT, 1.0); XCTAssert(fetchRight == YES, @"Should fetch for scrolling right"); - BOOL fetchDown = ASDisplayShouldFetchBatchForContext(context, ASScrollDirectionDown, PASSING_RECT, PASSING_SIZE, PASSING_POINT, 1.0); + BOOL fetchDown = ASDisplayShouldFetchBatchForContext(context, ASScrollDirectionDown, ASScrollDirectionVerticalDirections, PASSING_RECT, PASSING_SIZE, PASSING_POINT, 1.0); XCTAssert(fetchDown == YES, @"Should fetch for scrolling down"); - BOOL fetchUp = ASDisplayShouldFetchBatchForContext(context, ASScrollDirectionUp, PASSING_RECT, PASSING_SIZE, PASSING_POINT, 1.0); + BOOL fetchUp = ASDisplayShouldFetchBatchForContext(context, ASScrollDirectionUp, ASScrollDirectionVerticalDirections, PASSING_RECT, PASSING_SIZE, PASSING_POINT, 1.0); XCTAssert(fetchUp == NO, @"Should not fetch for scrolling up"); - BOOL fetchLeft = ASDisplayShouldFetchBatchForContext(context, ASScrollDirectionLeft, PASSING_RECT, PASSING_SIZE, PASSING_POINT, 1.0); + BOOL fetchLeft = ASDisplayShouldFetchBatchForContext(context, ASScrollDirectionLeft, ASScrollDirectionHorizontalDirections, PASSING_RECT, PASSING_SIZE, PASSING_POINT, 1.0); XCTAssert(fetchLeft == NO, @"Should not fetch for scrolling left"); } @@ -57,7 +57,7 @@ CGFloat screen = 1.0; ASBatchContext *context = [[ASBatchContext alloc] init]; // scroll to 1-screen top offset, height is 1 screen, so bottom is 1 screen away from end of content - BOOL shouldFetch = ASDisplayShouldFetchBatchForContext(context, ASScrollDirectionDown, VERTICAL_RECT(screen), VERTICAL_SIZE(screen * 3.0), VERTICAL_OFFSET(screen * 1.0), 1.0); + BOOL shouldFetch = ASDisplayShouldFetchBatchForContext(context, ASScrollDirectionDown, ASScrollDirectionVerticalDirections, VERTICAL_RECT(screen), VERTICAL_SIZE(screen * 3.0), VERTICAL_OFFSET(screen * 1.0), 1.0); XCTAssert(shouldFetch == YES, @"Fetch should begin when vertically scrolling to exactly 1 leading screen away"); } @@ -65,7 +65,7 @@ CGFloat screen = 1.0; ASBatchContext *context = [[ASBatchContext alloc] init]; // 3 screens of content, scroll only 1/2 of one screen - BOOL shouldFetch = ASDisplayShouldFetchBatchForContext(context, ASScrollDirectionDown, VERTICAL_RECT(screen), VERTICAL_SIZE(screen * 3.0), VERTICAL_OFFSET(screen * 0.5), 1.0); + BOOL shouldFetch = ASDisplayShouldFetchBatchForContext(context, ASScrollDirectionDown, ASScrollDirectionVerticalDirections, VERTICAL_RECT(screen), VERTICAL_SIZE(screen * 3.0), VERTICAL_OFFSET(screen * 0.5), 1.0); XCTAssert(shouldFetch == NO, @"Fetch should not begin when vertically scrolling less than the leading distance away"); } @@ -73,7 +73,7 @@ CGFloat screen = 1.0; ASBatchContext *context = [[ASBatchContext alloc] init]; // 3 screens of content, top offset to 3-screens, height 1 screen, so its 1 screen past the leading - BOOL shouldFetch = ASDisplayShouldFetchBatchForContext(context, ASScrollDirectionDown, VERTICAL_RECT(screen), VERTICAL_SIZE(screen * 3.0), VERTICAL_OFFSET(screen * 3.0), 1.0); + BOOL shouldFetch = ASDisplayShouldFetchBatchForContext(context, ASScrollDirectionDown, ASScrollDirectionVerticalDirections, VERTICAL_RECT(screen), VERTICAL_SIZE(screen * 3.0), VERTICAL_OFFSET(screen * 3.0), 1.0); XCTAssert(shouldFetch == YES, @"Fetch should begin when vertically scrolling past the content size"); } @@ -81,7 +81,7 @@ CGFloat screen = 1.0; ASBatchContext *context = [[ASBatchContext alloc] init]; // scroll to 1-screen left offset, width is 1 screen, so right is 1 screen away from end of content - BOOL shouldFetch = ASDisplayShouldFetchBatchForContext(context, ASScrollDirectionRight, HORIZONTAL_RECT(screen), HORIZONTAL_SIZE(screen * 3.0), HORIZONTAL_OFFSET(screen * 1.0), 1.0); + BOOL shouldFetch = ASDisplayShouldFetchBatchForContext(context, ASScrollDirectionRight, ASScrollDirectionVerticalDirections, HORIZONTAL_RECT(screen), HORIZONTAL_SIZE(screen * 3.0), HORIZONTAL_OFFSET(screen * 1.0), 1.0); XCTAssert(shouldFetch == YES, @"Fetch should begin when horizontally scrolling to exactly 1 leading screen away"); } @@ -89,7 +89,7 @@ CGFloat screen = 1.0; ASBatchContext *context = [[ASBatchContext alloc] init]; // 3 screens of content, scroll only 1/2 of one screen - BOOL shouldFetch = ASDisplayShouldFetchBatchForContext(context, ASScrollDirectionLeft, HORIZONTAL_RECT(screen), HORIZONTAL_SIZE(screen * 3.0), HORIZONTAL_OFFSET(screen * 0.5), 1.0); + BOOL shouldFetch = ASDisplayShouldFetchBatchForContext(context, ASScrollDirectionLeft, ASScrollDirectionHorizontalDirections, HORIZONTAL_RECT(screen), HORIZONTAL_SIZE(screen * 3.0), HORIZONTAL_OFFSET(screen * 0.5), 1.0); XCTAssert(shouldFetch == NO, @"Fetch should not begin when horizontally scrolling less than the leading distance away"); } @@ -97,7 +97,7 @@ CGFloat screen = 1.0; ASBatchContext *context = [[ASBatchContext alloc] init]; // 3 screens of content, left offset to 3-screens, width 1 screen, so its 1 screen past the leading - BOOL shouldFetch = ASDisplayShouldFetchBatchForContext(context, ASScrollDirectionDown, HORIZONTAL_RECT(screen), HORIZONTAL_SIZE(screen * 3.0), HORIZONTAL_OFFSET(screen * 3.0), 1.0); + BOOL shouldFetch = ASDisplayShouldFetchBatchForContext(context, ASScrollDirectionDown, ASScrollDirectionHorizontalDirections, HORIZONTAL_RECT(screen), HORIZONTAL_SIZE(screen * 3.0), HORIZONTAL_OFFSET(screen * 3.0), 1.0); XCTAssert(shouldFetch == YES, @"Fetch should begin when vertically scrolling past the content size"); } @@ -105,7 +105,7 @@ CGFloat screen = 1.0; ASBatchContext *context = [[ASBatchContext alloc] init]; // when the content size is < screen size, the target offset will always be 0 - BOOL shouldFetch = ASDisplayShouldFetchBatchForContext(context, ASScrollDirectionDown, VERTICAL_RECT(screen), VERTICAL_SIZE(screen * 0.5), VERTICAL_OFFSET(0.0), 1.0); + BOOL shouldFetch = ASDisplayShouldFetchBatchForContext(context, ASScrollDirectionDown, ASScrollDirectionVerticalDirections, VERTICAL_RECT(screen), VERTICAL_SIZE(screen * 0.5), VERTICAL_OFFSET(0.0), 1.0); XCTAssert(shouldFetch == YES, @"Fetch should begin when the target is 0 and the content size is smaller than the scree"); } @@ -113,7 +113,7 @@ CGFloat screen = 1.0; ASBatchContext *context = [[ASBatchContext alloc] init]; // when the content size is < screen size, the target offset will always be 0 - BOOL shouldFetch = ASDisplayShouldFetchBatchForContext(context, ASScrollDirectionRight, HORIZONTAL_RECT(screen), HORIZONTAL_SIZE(screen * 0.5), HORIZONTAL_OFFSET(0.0), 1.0); + BOOL shouldFetch = ASDisplayShouldFetchBatchForContext(context, ASScrollDirectionRight, ASScrollDirectionHorizontalDirections, HORIZONTAL_RECT(screen), HORIZONTAL_SIZE(screen * 0.5), HORIZONTAL_OFFSET(0.0), 1.0); XCTAssert(shouldFetch == YES, @"Fetch should begin when the target is 0 and the content size is smaller than the scree"); } diff --git a/AsyncDisplayKitTests/ASCollectionViewTests.mm b/AsyncDisplayKitTests/ASCollectionViewTests.mm index 51a4804777..27ad94ec7d 100644 --- a/AsyncDisplayKitTests/ASCollectionViewTests.mm +++ b/AsyncDisplayKitTests/ASCollectionViewTests.mm @@ -58,6 +58,7 @@ @interface ASCollectionViewTestDelegate : NSObject @property (nonatomic, assign) NSInteger sectionGeneration; +@property (nonatomic, copy) void(^willBeginBatchFetch)(ASBatchContext *); @end @@ -129,6 +130,15 @@ return [[ASCellNode alloc] init]; } +- (void)collectionNode:(ASCollectionNode *)collectionNode willBeginBatchFetchWithContext:(ASBatchContext *)context +{ + if (_willBeginBatchFetch != nil) { + _willBeginBatchFetch(context); + } else { + [context cancelBatchFetching]; + } +} + @end @interface ASCollectionViewTestController: UIViewController @@ -212,8 +222,6 @@ - (void)testReloadIfNeeded { - [ASDisplayNode setSuppressesInvalidCollectionUpdateExceptions:NO]; - __block ASCollectionViewTestController *testController = [[ASCollectionViewTestController alloc] initWithNibName:nil bundle:nil]; __block ASCollectionViewTestDelegate *del = testController.asyncDelegate; __block ASCollectionNode *cn = testController.collectionNode; @@ -323,26 +331,26 @@ - (void)testTuningParametersWithExplicitRangeMode { UICollectionViewFlowLayout *layout = [[UICollectionViewFlowLayout alloc] init]; - ASCollectionView *collectionView = [[ASCollectionView alloc] initWithFrame:CGRectZero collectionViewLayout:layout]; + ASCollectionNode *collectionNode = [[ASCollectionNode alloc] initWithCollectionViewLayout:layout]; ASRangeTuningParameters minimumRenderParams = { .leadingBufferScreenfuls = 0.1, .trailingBufferScreenfuls = 0.1 }; ASRangeTuningParameters minimumPreloadParams = { .leadingBufferScreenfuls = 0.1, .trailingBufferScreenfuls = 0.1 }; ASRangeTuningParameters fullRenderParams = { .leadingBufferScreenfuls = 0.5, .trailingBufferScreenfuls = 0.5 }; ASRangeTuningParameters fullPreloadParams = { .leadingBufferScreenfuls = 1, .trailingBufferScreenfuls = 0.5 }; - [collectionView setTuningParameters:minimumRenderParams forRangeMode:ASLayoutRangeModeMinimum rangeType:ASLayoutRangeTypeDisplay]; - [collectionView setTuningParameters:minimumPreloadParams forRangeMode:ASLayoutRangeModeMinimum rangeType:ASLayoutRangeTypePreload]; - [collectionView setTuningParameters:fullRenderParams forRangeMode:ASLayoutRangeModeFull rangeType:ASLayoutRangeTypeDisplay]; - [collectionView setTuningParameters:fullPreloadParams forRangeMode:ASLayoutRangeModeFull rangeType:ASLayoutRangeTypePreload]; + [collectionNode setTuningParameters:minimumRenderParams forRangeMode:ASLayoutRangeModeMinimum rangeType:ASLayoutRangeTypeDisplay]; + [collectionNode setTuningParameters:minimumPreloadParams forRangeMode:ASLayoutRangeModeMinimum rangeType:ASLayoutRangeTypePreload]; + [collectionNode setTuningParameters:fullRenderParams forRangeMode:ASLayoutRangeModeFull rangeType:ASLayoutRangeTypeDisplay]; + [collectionNode setTuningParameters:fullPreloadParams forRangeMode:ASLayoutRangeModeFull rangeType:ASLayoutRangeTypePreload]; XCTAssertTrue(ASRangeTuningParametersEqualToRangeTuningParameters(minimumRenderParams, - [collectionView tuningParametersForRangeMode:ASLayoutRangeModeMinimum rangeType:ASLayoutRangeTypeDisplay])); + [collectionNode tuningParametersForRangeMode:ASLayoutRangeModeMinimum rangeType:ASLayoutRangeTypeDisplay])); XCTAssertTrue(ASRangeTuningParametersEqualToRangeTuningParameters(minimumPreloadParams, - [collectionView tuningParametersForRangeMode:ASLayoutRangeModeMinimum rangeType:ASLayoutRangeTypePreload])); + [collectionNode tuningParametersForRangeMode:ASLayoutRangeModeMinimum rangeType:ASLayoutRangeTypePreload])); XCTAssertTrue(ASRangeTuningParametersEqualToRangeTuningParameters(fullRenderParams, - [collectionView tuningParametersForRangeMode:ASLayoutRangeModeFull rangeType:ASLayoutRangeTypeDisplay])); + [collectionNode tuningParametersForRangeMode:ASLayoutRangeModeFull rangeType:ASLayoutRangeTypeDisplay])); XCTAssertTrue(ASRangeTuningParametersEqualToRangeTuningParameters(fullPreloadParams, - [collectionView tuningParametersForRangeMode:ASLayoutRangeModeFull rangeType:ASLayoutRangeTypePreload])); + [collectionNode tuningParametersForRangeMode:ASLayoutRangeModeFull rangeType:ASLayoutRangeTypePreload])); } - (void)testTuningParameters @@ -373,7 +381,6 @@ #pragma mark - Update Validations #define updateValidationTestPrologue \ - [ASDisplayNode setSuppressesInvalidCollectionUpdateExceptions:NO];\ ASCollectionViewTestController *testController = [[ASCollectionViewTestController alloc] initWithNibName:nil bundle:nil];\ __unused ASCollectionViewTestDelegate *del = testController.asyncDelegate;\ __unused ASCollectionView *cv = testController.collectionView;\ @@ -808,4 +815,80 @@ [cn performBatchAnimated:NO updates:nil completion:nil]; } +- (void)testThatMultipleBatchFetchesDontHappenUnnecessarily +{ + UIWindow *window = [[UIWindow alloc] initWithFrame:[UIScreen mainScreen].bounds]; + ASCollectionViewTestController *testController = [[ASCollectionViewTestController alloc] initWithNibName:nil bundle:nil]; + window.rootViewController = testController; + + // Start with 1 item so that our content does not fill bounds. + __block NSInteger itemCount = 1; + testController.asyncDelegate->_itemCounts = {itemCount}; + [window makeKeyAndVisible]; + [window layoutIfNeeded]; + + ASCollectionNode *cn = testController.collectionNode; + [cn waitUntilAllUpdatesAreCommitted]; + XCTAssertGreaterThan(cn.bounds.size.height, cn.view.contentSize.height, @"Expected initial data not to fill collection view area."); + + __block NSUInteger batchFetchCount = 0; + XCTestExpectation *expectation = [self expectationWithDescription:@"Batch fetching completed and then some"]; + __weak ASCollectionViewTestController *weakController = testController; + testController.asyncDelegate.willBeginBatchFetch = ^(ASBatchContext *context) { + + // Ensure only 1 batch fetch happens + batchFetchCount += 1; + if (batchFetchCount > 1) { + XCTFail(@"Too many batch fetches!"); + return; + } + + dispatch_async(dispatch_get_main_queue(), ^{ + // Up the item count to 1000 so that we're well beyond the + // edge of the collection view and not ready for another batch fetch. + NSMutableArray *indexPaths = [NSMutableArray array]; + for (; itemCount < 1000; itemCount++) { + [indexPaths addObject:[NSIndexPath indexPathForItem:itemCount inSection:0]]; + } + weakController.asyncDelegate->_itemCounts = {itemCount}; + [cn insertItemsAtIndexPaths:indexPaths]; + [context completeBatchFetching:YES]; + + // Let the run loop turn before we consider the test passed. + dispatch_async(dispatch_get_main_queue(), ^{ + [expectation fulfill]; + }); + }); + }; + [self waitForExpectationsWithTimeout:3 handler:nil]; +} + +- (void)testThatBatchFetchHappensForEmptyCollection +{ + UIWindow *window = [[UIWindow alloc] initWithFrame:[UIScreen mainScreen].bounds]; + ASCollectionViewTestController *testController = [[ASCollectionViewTestController alloc] initWithNibName:nil bundle:nil]; + window.rootViewController = testController; + + // Start with 1 item so that our content does not fill bounds. + testController.asyncDelegate->_itemCounts = {}; + [window makeKeyAndVisible]; + [window layoutIfNeeded]; + + ASCollectionNode *cn = testController.collectionNode; + [cn waitUntilAllUpdatesAreCommitted]; + + __block NSUInteger batchFetchCount = 0; + XCTestExpectation *e = [self expectationWithDescription:@"Batch fetching completed"]; + testController.asyncDelegate.willBeginBatchFetch = ^(ASBatchContext *context) { + // Ensure only 1 batch fetch happens + batchFetchCount += 1; + if (batchFetchCount > 1) { + XCTFail(@"Too many batch fetches!"); + return; + } + [e fulfill]; + }; + [self waitForExpectationsWithTimeout:3 handler:nil]; +} + @end diff --git a/AsyncDisplayKitTests/ASDisplayNodeImplicitHierarchyTests.m b/AsyncDisplayKitTests/ASDisplayNodeImplicitHierarchyTests.m index 6623c55205..ea9a7fc382 100644 --- a/AsyncDisplayKitTests/ASDisplayNodeImplicitHierarchyTests.m +++ b/AsyncDisplayKitTests/ASDisplayNodeImplicitHierarchyTests.m @@ -12,6 +12,7 @@ #import +#import "ASDisplayNodeTestsHelper.h" #import "ASDisplayNode.h" #import "ASDisplayNode+Beta.h" #import "ASDisplayNode+Subclasses.h" @@ -88,7 +89,10 @@ return [ASAbsoluteLayoutSpec absoluteLayoutSpecWithChildren:@[stack1, stack2, node5]]; }; - [node layoutThatFits:ASSizeRangeMake(CGSizeZero)]; + + ASDisplayNodeSizeToFitSizeRange(node, ASSizeRangeMake(CGSizeZero, CGSizeMake(CGFLOAT_MAX, CGFLOAT_MAX))); + [node.view layoutIfNeeded]; + XCTAssertEqual(node.subnodes[0], node1); XCTAssertEqual(node.subnodes[1], node2); XCTAssertEqual(node.subnodes[2], node3); @@ -122,13 +126,14 @@ } }; - [node layoutThatFits:ASSizeRangeMake(CGSizeZero)]; + ASDisplayNodeSizeToFitSizeRange(node, ASSizeRangeMake(CGSizeZero, CGSizeMake(CGFLOAT_MAX, CGFLOAT_MAX))); + [node.view layoutIfNeeded]; XCTAssertEqual(node.subnodes[0], node1); XCTAssertEqual(node.subnodes[1], node2); node.layoutState = @2; - [node invalidateCalculatedLayout]; - [node layoutThatFits:ASSizeRangeMake(CGSizeZero)]; + [node setNeedsLayout]; // After a state change the layout needs to be invalidated + [node.view layoutIfNeeded]; // A new layout pass will trigger the hiearchy transition XCTAssertEqual(node.subnodes[0], node1); XCTAssertEqual(node.subnodes[1], node3); @@ -153,14 +158,17 @@ - (void)testLayoutTransitionMeasurementCompletionBlockIsCalledOnMainThread { + const CGSize kSize = CGSizeMake(100, 100); + ASDisplayNode *displayNode = [[ASDisplayNode alloc] init]; + displayNode.style.preferredSize = kSize; // Trigger explicit view creation to be able to use the Transition API [displayNode view]; XCTestExpectation *expectation = [self expectationWithDescription:@"Call measurement completion block on main"]; - [displayNode transitionLayoutWithSizeRange:ASSizeRangeMake(CGSizeZero, CGSizeZero) animated:YES shouldMeasureAsync:YES measurementCompletion:^{ + [displayNode transitionLayoutWithSizeRange:ASSizeRangeMake(CGSizeZero, CGSizeMake(INFINITY, INFINITY)) animated:YES shouldMeasureAsync:YES measurementCompletion:^{ XCTAssertTrue(ASDisplayNodeThreadIsMain(), @"Measurement completion block should be called on main thread"); [expectation fulfill]; }]; @@ -170,10 +178,12 @@ - (void)testMeasurementInBackgroundThreadWithLoadedNode { + const CGSize kNodeSize = CGSizeMake(100, 100); ASDisplayNode *node1 = [[ASDisplayNode alloc] init]; ASDisplayNode *node2 = [[ASDisplayNode alloc] init]; ASSpecTestDisplayNode *node = [[ASSpecTestDisplayNode alloc] init]; + node.style.preferredSize = kNodeSize; node.automaticallyManagesSubnodes = YES; node.layoutSpecBlock = ^(ASDisplayNode *weakNode, ASSizeRange constrainedSize) { ASSpecTestDisplayNode *strongNode = (ASSpecTestDisplayNode *)weakNode; @@ -185,23 +195,42 @@ }; // Intentionally trigger view creation + [node view]; [node2 view]; XCTestExpectation *expectation = [self expectationWithDescription:@"Fix IHM layout also if one node is already loaded"]; dispatch_async(dispatch_get_global_queue( DISPATCH_QUEUE_PRIORITY_DEFAULT, 0), ^{ - [node layoutThatFits:ASSizeRangeMake(CGSizeZero)]; - XCTAssertEqual(node.subnodes[0], node1); - - node.layoutState = @2; - [node invalidateCalculatedLayout]; - [node layoutThatFits:ASSizeRangeMake(CGSizeZero)]; + // Measurement happens in the background + ASDisplayNodeSizeToFitSizeRange(node, ASSizeRangeMake(CGSizeZero, CGSizeMake(INFINITY, INFINITY))); // Dispatch back to the main thread to let the insertion / deletion of subnodes happening dispatch_async(dispatch_get_main_queue(), ^{ - XCTAssertEqual(node.subnodes[0], node2); - [expectation fulfill]; + + // Layout on main + [node setNeedsLayout]; + [node.view layoutIfNeeded]; + XCTAssertEqual(node.subnodes[0], node1); + + dispatch_async(dispatch_get_global_queue( DISPATCH_QUEUE_PRIORITY_DEFAULT, 0), ^{ + + // Change state and measure in the background + node.layoutState = @2; + [node setNeedsLayout]; + + ASDisplayNodeSizeToFitSizeRange(node, ASSizeRangeMake(CGSizeZero, CGSizeMake(INFINITY, INFINITY))); + + // Dispatch back to the main thread to let the insertion / deletion of subnodes happening + dispatch_async(dispatch_get_main_queue(), ^{ + + // Layout on main again + [node.view layoutIfNeeded]; + XCTAssertEqual(node.subnodes[0], node2); + + [expectation fulfill]; + }); + }); }); }); @@ -214,12 +243,13 @@ - (void)testTransitionLayoutWithAnimationWithLoadedNodes { + const CGSize kNodeSize = CGSizeMake(100, 100); ASDisplayNode *node1 = [[ASDisplayNode alloc] init]; ASDisplayNode *node2 = [[ASDisplayNode alloc] init]; ASSpecTestDisplayNode *node = [[ASSpecTestDisplayNode alloc] init]; node.automaticallyManagesSubnodes = YES; - + node.style.preferredSize = kNodeSize; node.layoutSpecBlock = ^(ASDisplayNode *weakNode, ASSizeRange constrainedSize) { ASSpecTestDisplayNode *strongNode = (ASSpecTestDisplayNode *)weakNode; if ([strongNode.layoutState isEqualToNumber:@1]) { @@ -230,13 +260,13 @@ }; // Intentionally trigger view creation - [node view]; [node1 view]; [node2 view]; XCTestExpectation *expectation = [self expectationWithDescription:@"Fix IHM layout transition also if one node is already loaded"]; - [node layoutThatFits:ASSizeRangeMake(CGSizeZero)]; + ASDisplayNodeSizeToFitSizeRange(node, ASSizeRangeMake(CGSizeZero, CGSizeMake(INFINITY, INFINITY))); + [node.view layoutIfNeeded]; XCTAssertEqual(node.subnodes[0], node1); node.layoutState = @2; diff --git a/AsyncDisplayKitTests/ASDisplayNodeSnapshotTests.m b/AsyncDisplayKitTests/ASDisplayNodeSnapshotTests.m index 2c2286b00a..203a46b94d 100644 --- a/AsyncDisplayKitTests/ASDisplayNodeSnapshotTests.m +++ b/AsyncDisplayKitTests/ASDisplayNodeSnapshotTests.m @@ -28,8 +28,8 @@ node.layoutSpecBlock = ^(ASDisplayNode * _Nonnull node, ASSizeRange constrainedSize) { return [ASInsetLayoutSpec insetLayoutSpecWithInsets:UIEdgeInsetsMake(5, 5, 5, 5) child:subnode]; }; - [node layoutThatFits:ASSizeRangeMake(CGSizeZero, CGSizeMake(100, 100))]; + ASDisplayNodeSizeToFitSizeRange(node, ASSizeRangeMake(CGSizeZero, CGSizeMake(INFINITY, INFINITY))); ASSnapshotVerifyNode(node, nil); } diff --git a/AsyncDisplayKitTests/ASDisplayNodeTests.m b/AsyncDisplayKitTests/ASDisplayNodeTests.m index 914a3570d0..3494d00917 100644 --- a/AsyncDisplayKitTests/ASDisplayNodeTests.m +++ b/AsyncDisplayKitTests/ASDisplayNodeTests.m @@ -89,7 +89,11 @@ for (ASDisplayNode *n in @[ nodes ]) {\ @interface ASTestDisplayNode : ASDisplayNode @property (nonatomic, copy) void (^willDeallocBlock)(__unsafe_unretained ASTestDisplayNode *node); @property (nonatomic, copy) CGSize(^calculateSizeBlock)(ASTestDisplayNode *node, CGSize size); -@property (nonatomic) BOOL hasFetchedData; + +@property (nonatomic, nullable) UIGestureRecognizer *gestureRecognizer; +@property (nonatomic, nullable) id idGestureRecognizer; +@property (nonatomic, nullable) UIImage *bigImage; +@property (nonatomic, nullable) NSArray *randomProperty; @property (nonatomic, nullable) UIGestureRecognizer *gestureRecognizer; @property (nonatomic, nullable) id idGestureRecognizer; @@ -99,6 +103,7 @@ for (ASDisplayNode *n in @[ nodes ]) {\ @property (nonatomic) BOOL displayRangeStateChangedToYES; @property (nonatomic) BOOL displayRangeStateChangedToNO; +@property (nonatomic) BOOL hasPreloaded; @property (nonatomic) BOOL preloadStateChangedToYES; @property (nonatomic) BOOL preloadStateChangedToNO; @end @@ -113,18 +118,6 @@ for (ASDisplayNode *n in @[ nodes ]) {\ return _calculateSizeBlock ? _calculateSizeBlock(self, constrainedSize) : CGSizeZero; } -- (void)fetchData -{ - [super fetchData]; - self.hasFetchedData = YES; -} - -- (void)clearFetchedData -{ - [super clearFetchedData]; - self.hasFetchedData = NO; -} - - (void)didEnterDisplayState { [super didEnterDisplayState]; @@ -141,6 +134,7 @@ for (ASDisplayNode *n in @[ nodes ]) {\ { [super didEnterPreloadState]; self.preloadStateChangedToYES = YES; + self.hasPreloaded = YES; } - (void)didExitPreloadState @@ -1738,76 +1732,76 @@ static inline BOOL _CGPointEqualToPointWithEpsilon(CGPoint point1, CGPoint point } // Check that nodes who have no cell node (no range controller) -// do get their `fetchData` called, and they do report -// the fetch data interface state. +// do get their `preload` called, and they do report +// the preload interface state. - (void)testInterfaceStateForNonCellNode { ASTestWindow *window = [ASTestWindow new]; ASTestDisplayNode *node = [ASTestDisplayNode new]; XCTAssert(node.interfaceState == ASInterfaceStateNone); - XCTAssert(!node.hasFetchedData); + XCTAssert(!node.hasPreloaded); [window addSubview:node.view]; - XCTAssert(node.hasFetchedData); + XCTAssert(node.hasPreloaded); XCTAssert(node.interfaceState == ASInterfaceStateInHierarchy); [node.view removeFromSuperview]; - // We don't want to call -clearFetchedData on nodes that aren't being managed by a range controller. + // We don't want to call -didExitPreloadState on nodes that aren't being managed by a range controller. // Otherwise we get flashing behavior from normal UIKit manipulations like navigation controller push / pop. // Still, the interfaceState should be None to reflect the current state of the node. // We just don't proactively clear contents or fetched data for this state transition. - XCTAssert(node.hasFetchedData); + XCTAssert(node.hasPreloaded); XCTAssert(node.interfaceState == ASInterfaceStateNone); } // Check that nodes who have no cell node (no range controller) -// do get their `fetchData` called, and they do report -// the fetch data interface state. +// do get their `preload` called, and they do report +// the preload interface state. - (void)testInterfaceStateForCellNode { ASCellNode *cellNode = [ASCellNode new]; ASTestDisplayNode *node = [ASTestDisplayNode new]; XCTAssert(node.interfaceState == ASInterfaceStateNone); - XCTAssert(!node.hasFetchedData); + XCTAssert(!node.hasPreloaded); // Simulate range handler updating cell node. [cellNode addSubnode:node]; [cellNode enterInterfaceState:ASInterfaceStatePreload]; - XCTAssert(node.hasFetchedData); + XCTAssert(node.hasPreloaded); XCTAssert(node.interfaceState == ASInterfaceStatePreload); // If the node goes into a view it should not adopt the `InHierarchy` state. ASTestWindow *window = [ASTestWindow new]; [window addSubview:cellNode.view]; - XCTAssert(node.hasFetchedData); + XCTAssert(node.hasPreloaded); XCTAssert(node.interfaceState == ASInterfaceStateInHierarchy); } -- (void)testSetNeedsDataFetchImmediateState +- (void)testSetNeedsPreloadImmediateState { ASCellNode *cellNode = [ASCellNode new]; ASTestDisplayNode *node = [ASTestDisplayNode new]; [cellNode addSubnode:node]; [cellNode enterInterfaceState:ASInterfaceStatePreload]; - node.hasFetchedData = NO; - [cellNode setNeedsDataFetch]; - XCTAssert(node.hasFetchedData); + node.hasPreloaded = NO; + [cellNode setNeedsPreload]; + XCTAssert(node.hasPreloaded); } -- (void)testFetchDataExitingAndEnteringRange +- (void)testPreloadExitingAndEnteringRange { ASCellNode *cellNode = [ASCellNode new]; ASTestDisplayNode *node = [ASTestDisplayNode new]; [cellNode addSubnode:node]; [cellNode setHierarchyState:ASHierarchyStateRangeManaged]; - // Simulate enter range, fetch data, exit range + // Simulate enter range, preload, exit range [cellNode enterInterfaceState:ASInterfaceStatePreload]; [cellNode exitInterfaceState:ASInterfaceStatePreload]; - node.hasFetchedData = NO; + node.hasPreloaded = NO; [cellNode enterInterfaceState:ASInterfaceStatePreload]; - XCTAssert(node.hasFetchedData); + XCTAssert(node.hasPreloaded); } - (void)testInitWithViewClass @@ -2070,8 +2064,8 @@ static bool stringContainsPointer(NSString *description, id p) { XCTAssertTrue((node.interfaceState & ASInterfaceStatePreload) == ASInterfaceStatePreload); XCTAssertTrue((subnode.interfaceState & ASInterfaceStatePreload) == ASInterfaceStatePreload); - XCTAssertTrue(node.hasFetchedData); - XCTAssertTrue(subnode.hasFetchedData); + XCTAssertTrue(node.hasPreloaded); + XCTAssertTrue(subnode.hasPreloaded); } // FIXME @@ -2196,8 +2190,10 @@ static bool stringContainsPointer(NSString *description, id p) { // The inset spec here is crucial. If the nodes themselves are children, it passed before the fix. return [ASOverlayLayoutSpec overlayLayoutSpecWithChild:[ASInsetLayoutSpec insetLayoutSpecWithInsets:UIEdgeInsetsZero child:underlay] overlay:overlay]; }; - [node layoutThatFits:ASSizeRangeMake(CGSizeMake(100, 100))]; - node.frame = (CGRect){ .size = node.calculatedSize }; + + ASDisplayNodeSizeToFitSize(node, CGSizeMake(100, 100)); + [node.view layoutIfNeeded]; + NSInteger underlayIndex = [node.subnodes indexOfObjectIdenticalTo:underlay]; NSInteger overlayIndex = [node.subnodes indexOfObjectIdenticalTo:overlay]; XCTAssertLessThan(underlayIndex, overlayIndex); @@ -2215,8 +2211,10 @@ static bool stringContainsPointer(NSString *description, id p) { // The inset spec here is crucial. If the nodes themselves are children, it passed before the fix. return [ASBackgroundLayoutSpec backgroundLayoutSpecWithChild:overlay background:[ASInsetLayoutSpec insetLayoutSpecWithInsets:UIEdgeInsetsZero child:underlay]]; }; - [node layoutThatFits:ASSizeRangeMake(CGSizeMake(100, 100))]; - node.frame = (CGRect){ .size = node.calculatedSize }; + + ASDisplayNodeSizeToFitSize(node, CGSizeMake(100, 100)); + [node.view layoutIfNeeded]; + NSInteger underlayIndex = [node.subnodes indexOfObjectIdenticalTo:underlay]; NSInteger overlayIndex = [node.subnodes indexOfObjectIdenticalTo:overlay]; XCTAssertLessThan(underlayIndex, overlayIndex); diff --git a/AsyncDisplayKitTests/ASDisplayNodeTestsHelper.h b/AsyncDisplayKitTests/ASDisplayNodeTestsHelper.h index 7f1ad0b383..664bf30b77 100644 --- a/AsyncDisplayKitTests/ASDisplayNodeTestsHelper.h +++ b/AsyncDisplayKitTests/ASDisplayNodeTestsHelper.h @@ -9,7 +9,13 @@ // #import +#import "ASDimension.h" + +@class ASDisplayNode; typedef BOOL (^as_condition_block_t)(void); BOOL ASDisplayNodeRunRunLoopUntilBlockIsTrue(as_condition_block_t block); + +void ASDisplayNodeSizeToFitSize(ASDisplayNode *node, CGSize size); +void ASDisplayNodeSizeToFitSizeRange(ASDisplayNode *node, ASSizeRange sizeRange); diff --git a/AsyncDisplayKitTests/ASDisplayNodeTestsHelper.m b/AsyncDisplayKitTests/ASDisplayNodeTestsHelper.m index 728ffec66f..d1721cef51 100644 --- a/AsyncDisplayKitTests/ASDisplayNodeTestsHelper.m +++ b/AsyncDisplayKitTests/ASDisplayNodeTestsHelper.m @@ -9,6 +9,8 @@ // #import "ASDisplayNodeTestsHelper.h" +#import "ASDisplayNode.h" +#import "ASLayout.h" #import @@ -41,3 +43,15 @@ BOOL ASDisplayNodeRunRunLoopUntilBlockIsTrue(as_condition_block_t block) } return passed; } + +void ASDisplayNodeSizeToFitSize(ASDisplayNode *node, CGSize size) +{ + CGSize sizeThatFits = [node layoutThatFits:ASSizeRangeMake(size)].size; + node.bounds = (CGRect){.origin = CGPointZero, .size = sizeThatFits}; +} + +void ASDisplayNodeSizeToFitSizeRange(ASDisplayNode *node, ASSizeRange sizeRange) +{ + CGSize sizeThatFits = [node layoutThatFits:sizeRange].size; + node.bounds = (CGRect){.origin = CGPointZero, .size = sizeThatFits}; +} diff --git a/AsyncDisplayKitTests/ASImageNodeSnapshotTests.m b/AsyncDisplayKitTests/ASImageNodeSnapshotTests.m index 55706583a5..9ebb36ce30 100644 --- a/AsyncDisplayKitTests/ASImageNodeSnapshotTests.m +++ b/AsyncDisplayKitTests/ASImageNodeSnapshotTests.m @@ -30,7 +30,7 @@ // trivial test case to ensure ASSnapshotTestCase works ASImageNode *imageNode = [[ASImageNode alloc] init]; imageNode.image = [self testImage]; - [imageNode layoutThatFits:ASSizeRangeMake(CGSizeZero, CGSizeMake(100, 100))]; + ASDisplayNodeSizeToFitSize(imageNode, CGSizeMake(100, 100)); ASSnapshotVerifyNode(imageNode, nil); } @@ -46,13 +46,12 @@ // Snapshot testing requires that node is formally laid out. imageNode.style.width = ASDimensionMake(forcedImageSize.width); imageNode.style.height = ASDimensionMake(forcedImageSize.height); - [imageNode layoutThatFits:ASSizeRangeMake(CGSizeZero, forcedImageSize)]; + ASDisplayNodeSizeToFitSize(imageNode, forcedImageSize); ASSnapshotVerifyNode(imageNode, @"first"); imageNode.style.width = ASDimensionMake(200); imageNode.style.height = ASDimensionMake(200); - [imageNode layoutThatFits:ASSizeRangeMake(CGSizeZero, CGSizeMake(200, 200))]; - + ASDisplayNodeSizeToFitSize(imageNode, CGSizeMake(200, 200)); ASSnapshotVerifyNode(imageNode, @"second"); XCTAssert(CGImageGetWidth((CGImageRef)imageNode.contents) == forcedImageSize.width * imageNode.contentsScale && @@ -66,7 +65,7 @@ UIImage *tinted = ASImageNodeTintColorModificationBlock([UIColor redColor])(test); ASImageNode *node = [[ASImageNode alloc] init]; node.image = tinted; - [node layoutThatFits:ASSizeRangeMake(test.size)]; + ASDisplayNodeSizeToFitSize(node, test.size); ASSnapshotVerifyNode(node, nil); } @@ -81,7 +80,7 @@ UIImage *rounded = ASImageNodeRoundBorderModificationBlock(2, [UIColor redColor])(result); ASImageNode *node = [[ASImageNode alloc] init]; node.image = rounded; - [node layoutThatFits:ASSizeRangeMake(rounded.size)]; + ASDisplayNodeSizeToFitSize(node, rounded.size); ASSnapshotVerifyNode(node, nil); } diff --git a/AsyncDisplayKitTests/ASLayoutSpecSnapshotTestsHelper.m b/AsyncDisplayKitTests/ASLayoutSpecSnapshotTestsHelper.m index e2d908d012..5d555bdde4 100644 --- a/AsyncDisplayKitTests/ASLayoutSpecSnapshotTestsHelper.m +++ b/AsyncDisplayKitTests/ASLayoutSpecSnapshotTestsHelper.m @@ -39,7 +39,7 @@ node.layoutSpecUnderTest = layoutSpec; - [node layoutThatFits:sizeRange]; + ASDisplayNodeSizeToFitSizeRange(node, sizeRange); ASSnapshotVerifyNode(node, identifier); } diff --git a/AsyncDisplayKitTests/ASSnapshotTestCase.h b/AsyncDisplayKitTests/ASSnapshotTestCase.h index c89fc79af4..d3f6791d86 100644 --- a/AsyncDisplayKitTests/ASSnapshotTestCase.h +++ b/AsyncDisplayKitTests/ASSnapshotTestCase.h @@ -9,6 +9,7 @@ // #import +#import "ASDisplayNodeTestsHelper.h" @class ASDisplayNode; diff --git a/AsyncDisplayKitTests/ASSnapshotTestCase.m b/AsyncDisplayKitTests/ASSnapshotTestCase.m index 3bcfb67293..301ed923fe 100644 --- a/AsyncDisplayKitTests/ASSnapshotTestCase.m +++ b/AsyncDisplayKitTests/ASSnapshotTestCase.m @@ -37,8 +37,6 @@ NSOrderedSet *ASSnapshotTestCaseDefaultSuffixes(void) + (void)hackilySynchronouslyRecursivelyRenderNode:(ASDisplayNode *)node { - ASDisplayNodeAssertNotNil(node.calculatedLayout, @"Node %@ must be measured before it is rendered.", node); - node.bounds = (CGRect) { .size = node.calculatedSize }; ASDisplayNodePerformBlockOnEveryNode(nil, node, YES, ^(ASDisplayNode * _Nonnull node) { [node.layer setNeedsDisplay]; }); diff --git a/AsyncDisplayKitTests/ASStackLayoutSpecSnapshotTests.mm b/AsyncDisplayKitTests/ASStackLayoutSpecSnapshotTests.mm index a23edd0ff9..e82fbe7f12 100644 --- a/AsyncDisplayKitTests/ASStackLayoutSpecSnapshotTests.mm +++ b/AsyncDisplayKitTests/ASStackLayoutSpecSnapshotTests.mm @@ -340,14 +340,14 @@ static void setCGSizeToNode(CGSize size, ASDisplayNode *node) - (void)testJustifiedSpaceBetweenWithRemainingSpace { - // width 301px; height 0-300px; 1px remaining + // width 301px; height 0-300px; static ASSizeRange kSize = {{301, 0}, {301, 300}}; [self testStackLayoutSpecWithJustify:ASStackLayoutJustifyContentSpaceBetween flexFactor:0 sizeRange:kSize identifier:nil]; } - (void)testJustifiedSpaceAroundWithRemainingSpace { - // width 305px; height 0-300px; 5px remaining + // width 305px; height 0-300px; static ASSizeRange kSize = {{305, 0}, {305, 300}}; [self testStackLayoutSpecWithJustify:ASStackLayoutJustifyContentSpaceAround flexFactor:0 sizeRange:kSize identifier:nil]; } diff --git a/AsyncDisplayKitTests/ASTableViewTests.m b/AsyncDisplayKitTests/ASTableViewTests.mm similarity index 84% rename from AsyncDisplayKitTests/ASTableViewTests.m rename to AsyncDisplayKitTests/ASTableViewTests.mm index 14a62bb0f5..6f62bd1a24 100644 --- a/AsyncDisplayKitTests/ASTableViewTests.m +++ b/AsyncDisplayKitTests/ASTableViewTests.mm @@ -21,7 +21,6 @@ #import "ASXCTExtensions.h" #define NumberOfSections 10 -#define NumberOfRowsPerSection 20 #define NumberOfReloadIterations 50 @interface ASTestDataController : ASChangeSetDataController @@ -73,6 +72,8 @@ @interface ASTableViewTestDelegate : NSObject @property (nonatomic, copy) void (^willDeallocBlock)(ASTableViewTestDelegate *delegate); +@property (nonatomic) CGFloat headerHeight; +@property (nonatomic) CGFloat footerHeight; @end @implementation ASTableViewTestDelegate @@ -92,6 +93,16 @@ return nil; } +- (CGFloat)tableView:(UITableView *)tableView heightForFooterInSection:(NSInteger)section +{ + return _footerHeight; +} + +- (CGFloat)tableView:(UITableView *)tableView heightForHeaderInSection:(NSInteger)section +{ + return _headerHeight; +} + - (void)dealloc { if (_willDeallocBlock) { @@ -120,10 +131,21 @@ @interface ASTableViewFilledDataSource : NSObject @property (nonatomic) BOOL usesSectionIndex; +@property (nonatomic) NSInteger rowsPerSection; +@property (nonatomic, nullable, copy) ASCellNodeBlock(^nodeBlockForItem)(NSIndexPath *); @end @implementation ASTableViewFilledDataSource +- (instancetype)init +{ + self = [super init]; + if (self != nil) { + _rowsPerSection = 20; + } + return self; +} + - (BOOL)respondsToSelector:(SEL)aSelector { if (aSelector == @selector(sectionIndexTitlesForTableView:) || aSelector == @selector(tableView:sectionForSectionIndexTitle:atIndex:)) { @@ -140,7 +162,7 @@ - (NSInteger)tableView:(UITableView *)tableView numberOfRowsInSection:(NSInteger)section { - return NumberOfRowsPerSection; + return _rowsPerSection; } - (ASCellNode *)tableView:(ASTableView *)tableView nodeForRowAtIndexPath:(NSIndexPath *)indexPath @@ -153,9 +175,14 @@ - (ASCellNodeBlock)tableView:(ASTableView *)tableView nodeBlockForRowAtIndexPath:(NSIndexPath *)indexPath { + if (_nodeBlockForItem) { + return _nodeBlockForItem(indexPath); + } + return ^{ ASTestTextCellNode *textCellNode = [ASTestTextCellNode new]; - textCellNode.text = indexPath.description; + textCellNode.text = [NSString stringWithFormat:@"{%d, %d}", (int)indexPath.section, (int)indexPath.row]; + textCellNode.backgroundColor = [UIColor whiteColor]; return textCellNode; }; } @@ -223,7 +250,7 @@ [tableView layoutIfNeeded]; for (int section = 0; section < NumberOfSections; section++) { - for (int row = 0; row < NumberOfRowsPerSection; row++) { + for (int row = 0; row < [tableView numberOfRowsInSection:section]; row++) { NSIndexPath *indexPath = [NSIndexPath indexPathForRow:row inSection:section]; CGRect rect = [tableView rectForRowAtIndexPath:indexPath]; XCTAssertEqual(rect.size.width, 100); // specified width should be ignored for table @@ -267,13 +294,12 @@ return [NSIndexSet indexSetWithIndexesInRange:NSMakeRange(MIN(randA, randB), MAX(randA, randB) - MIN(randA, randB))]; } -- (NSArray *)randomIndexPathsExisting:(BOOL)existing +- (NSArray *)randomIndexPathsExisting:(BOOL)existing rowCount:(NSInteger)rowCount { NSMutableArray *indexPaths = [NSMutableArray array]; [[self randomIndexSet] enumerateIndexesUsingBlock:^(NSUInteger idx, BOOL *stop) { - NSUInteger rowNum = NumberOfRowsPerSection; NSIndexPath *sectionIndex = [[NSIndexPath alloc] initWithIndex:idx]; - for (NSUInteger i = (existing ? 0 : rowNum); i < (existing ? rowNum : rowNum * 2); i++) { + for (NSUInteger i = (existing ? 0 : rowCount); i < (existing ? rowCount : rowCount * 2); i++) { // Maximize evility by sporadically skipping indicies 1/3rd of the time, but only if reloading existing rows if (existing && arc4random_uniform(2) == 0) { continue; @@ -325,7 +351,7 @@ } if (reloadRowsInsteadOfSections) { - NSArray *indexPaths = [self randomIndexPathsExisting:YES]; + NSArray *indexPaths = [self randomIndexPathsExisting:YES rowCount:dataSource.rowsPerSection]; //NSLog(@"reloading rows: %@", indexPaths); [tableView reloadRowsAtIndexPaths:indexPaths withRowAnimation:rowAnimation]; } else { @@ -396,7 +422,15 @@ ASTestTableView *tableView = [[ASTestTableView alloc] __initWithFrame:CGRectMake(0, 0, tableViewSize.width, tableViewSize.height) style:UITableViewStylePlain]; ASTableViewFilledDataSource *dataSource = [ASTableViewFilledDataSource new]; - + // Currently this test requires that the text in the cell node fills the + // visible width, so we use the long description for the index path. + dataSource.nodeBlockForItem = ^(NSIndexPath *indexPath) { + return (ASCellNodeBlock)^{ + ASTestTextCellNode *textCellNode = [[ASTestTextCellNode alloc] init]; + textCellNode.text = indexPath.description; + return textCellNode; + }; + }; tableView.asyncDelegate = dataSource; tableView.asyncDataSource = dataSource; @@ -413,7 +447,7 @@ [tableView setEditing:YES]; [tableView endUpdatesAnimated:YES completion:^(BOOL completed) { for (int section = 0; section < NumberOfSections; section++) { - for (int row = 0; row < NumberOfRowsPerSection; row++) { + for (int row = 0; row < dataSource.rowsPerSection; row++) { NSIndexPath *indexPath = [NSIndexPath indexPathForRow:row inSection:section]; ASTestTextCellNode *node = (ASTestTextCellNode *)[tableView nodeForRowAtIndexPath:indexPath]; if ([visibleNodes containsObject:node]) { @@ -441,7 +475,7 @@ [tableView setEditing:NO]; [tableView endUpdatesAnimated:YES completion:^(BOOL completed) { for (int section = 0; section < NumberOfSections; section++) { - for (int row = 0; row < NumberOfRowsPerSection; row++) { + for (int row = 0; row < dataSource.rowsPerSection; row++) { NSIndexPath *indexPath = [NSIndexPath indexPathForRow:row inSection:section]; ASTestTextCellNode *node = (ASTestTextCellNode *)[tableView nodeForRowAtIndexPath:indexPath]; BOOL visible = [visibleNodes containsObject:node]; @@ -472,7 +506,7 @@ // Cause table view to enter editing mode and then scroll to the bottom. // The last node should be re-measured on main thread with the new (smaller) content view width. - NSIndexPath *lastRowIndexPath = [NSIndexPath indexPathForRow:(NumberOfRowsPerSection - 1) inSection:(NumberOfSections - 1)]; + NSIndexPath *lastRowIndexPath = [NSIndexPath indexPathForRow:(dataSource.rowsPerSection - 1) inSection:(NumberOfSections - 1)]; XCTestExpectation *relayoutExpectation = [self expectationWithDescription:@"relayout"]; [tableView beginUpdates]; [tableView setEditing:YES]; @@ -502,7 +536,7 @@ [tableView reloadDataWithCompletion:^{ for (NSUInteger i = 0; i < NumberOfSections; i++) { - for (NSUInteger j = 0; j < NumberOfRowsPerSection; j++) { + for (NSUInteger j = 0; j < dataSource.rowsPerSection; j++) { NSIndexPath *indexPath = [NSIndexPath indexPathForRow:j inSection:i]; ASCellNode *cellNode = [tableView nodeForRowAtIndexPath:indexPath]; NSIndexPath *reportedIndexPath = [tableView indexPathForNode:cellNode]; @@ -517,7 +551,7 @@ XCTestExpectation *reloadDataExpectation = [self expectationWithDescription:@"reloadData"]; [tableView reloadDataWithCompletion:^{ for (int section = 0; section < NumberOfSections; section++) { - for (int row = 0; row < NumberOfRowsPerSection; row++) { + for (int row = 0; row < [tableView numberOfRowsInSection:section]; row++) { NSIndexPath *indexPath = [NSIndexPath indexPathForRow:row inSection:section]; ASTestTextCellNode *node = (ASTestTextCellNode *)[tableView nodeForRowAtIndexPath:indexPath]; XCTAssertEqual(node.numberOfLayoutsOnMainThread, 0); @@ -548,7 +582,7 @@ XCTAssertEqual(tableView.testDataController.numberOfAllNodesRelayouts, 1); for (int section = 0; section < NumberOfSections; section++) { - for (int row = 0; row < NumberOfRowsPerSection; row++) { + for (int row = 0; row < [tableView numberOfRowsInSection:section]; row++) { NSIndexPath *indexPath = [NSIndexPath indexPathForRow:row inSection:section]; ASTestTextCellNode *node = (ASTestTextCellNode *)[tableView nodeForRowAtIndexPath:indexPath]; XCTAssertLessThanOrEqual(node.numberOfLayoutsOnMainThread, 1); @@ -716,6 +750,74 @@ [node performBatchAnimated:NO updates:nil completion:nil]; } +// https://github.com/facebook/AsyncDisplayKit/issues/2252#issuecomment-263689979 +- (void)testIssue2252 +{ + // Hard-code an iPhone 7 screen. There's something particular about this geometry that causes the issue to repro. + UIWindow *window = [[UIWindow alloc] initWithFrame:CGRectMake(0, 0, 375, 667)]; + + ASTableNode *node = [[ASTableNode alloc] initWithStyle:UITableViewStyleGrouped]; + node.frame = window.bounds; + ASTableViewTestDelegate *del = [[ASTableViewTestDelegate alloc] init]; + del.headerHeight = 32; + del.footerHeight = 0.01; + node.delegate = del; + ASTableViewFilledDataSource *ds = [[ASTableViewFilledDataSource alloc] init]; + ds.rowsPerSection = 1; + node.dataSource = ds; + ASViewController *vc = [[ASViewController alloc] initWithNode:node]; + UITabBarController *tabCtrl = [[UITabBarController alloc] init]; + tabCtrl.viewControllers = @[ vc ]; + tabCtrl.tabBar.translucent = NO; + window.rootViewController = tabCtrl; + [window makeKeyAndVisible]; + + [window layoutIfNeeded]; + [node waitUntilAllUpdatesAreCommitted]; + XCTAssertEqual(node.view.numberOfSections, NumberOfSections); + ASXCTAssertEqualRects(CGRectMake(0, 32, 375, 43.5), [node rectForRowAtIndexPath:[NSIndexPath indexPathForItem:0 inSection:0]], @"This text requires very specific geometry. The rect for the first row should match up."); + + __unused XCTestExpectation *e = [self expectationWithDescription:@"Did a bunch of rounds of updates."]; + NSInteger totalCount = 20; + __block NSInteger count = 0; + dispatch_source_t timer = dispatch_source_create(DISPATCH_SOURCE_TYPE_TIMER, 0, 0, dispatch_get_main_queue()); + dispatch_source_set_timer(timer, DISPATCH_TIME_NOW, 0.2 * NSEC_PER_SEC, 0.01 * NSEC_PER_SEC); + dispatch_source_set_event_handler(timer, ^{ + [node performBatchUpdates:^{ + [node reloadSections:[NSIndexSet indexSetWithIndexesInRange:NSMakeRange(0, NumberOfSections)] withRowAnimation:UITableViewRowAnimationNone]; + } completion:^(BOOL finished) { + if (++count == totalCount) { + dispatch_cancel(timer); + dispatch_after(dispatch_time(DISPATCH_TIME_NOW, (int64_t)(2 * NSEC_PER_SEC)), dispatch_get_main_queue(), ^{ + [e fulfill]; + }); + } + }]; + }); + dispatch_resume(timer); + [self waitForExpectationsWithTimeout:60 handler:nil]; +} + +- (void)testThatInvalidUpdateExceptionReasonContainsDataSourceClassName +{ + ASTableNode *node = [[ASTableNode alloc] initWithStyle:UITableViewStyleGrouped]; + node.bounds = CGRectMake(0, 0, 100, 100); + ASTableViewFilledDataSource *ds = [[ASTableViewFilledDataSource alloc] init]; + node.dataSource = ds; + + // Force node to load initial data. + [node.view layoutIfNeeded]; + + // Submit an invalid update, ensure exception name matches and that data source is included in the reason. + @try { + [node deleteSections:[NSIndexSet indexSetWithIndex:1000] withRowAnimation:UITableViewRowAnimationNone]; + XCTFail(@"Expected validation to fail."); + } @catch (NSException *e) { + XCTAssertEqual(e.name, ASCollectionInvalidUpdateException); + XCTAssert([e.reason rangeOfString:NSStringFromClass([ds class])].location != NSNotFound, @"Expected validation reason to contain the data source class name. Got:\n%@", e.reason); + } +} + @end @implementation UITableView (Testing) diff --git a/AsyncDisplayKitTests/ASTextNodeSnapshotTests.m b/AsyncDisplayKitTests/ASTextNodeSnapshotTests.m index 1dac0c198c..2a2a1b704d 100644 --- a/AsyncDisplayKitTests/ASTextNodeSnapshotTests.m +++ b/AsyncDisplayKitTests/ASTextNodeSnapshotTests.m @@ -11,7 +11,6 @@ #import "ASSnapshotTestCase.h" #import -#import "ASLayout.h" @interface ASTextNodeSnapshotTests : ASSnapshotTestCase @@ -25,8 +24,8 @@ ASTextNode *textNode = [[ASTextNode alloc] init]; textNode.attributedText = [[NSAttributedString alloc] initWithString:@"judar" attributes:@{NSFontAttributeName : [UIFont italicSystemFontOfSize:24]}]; - [textNode layoutThatFits:ASSizeRangeMake(CGSizeZero, CGSizeMake(CGFLOAT_MAX, CGFLOAT_MAX))]; textNode.textContainerInset = UIEdgeInsetsMake(0, 2, 0, 2); + ASDisplayNodeSizeToFitSizeRange(textNode, ASSizeRangeMake(CGSizeZero, CGSizeMake(CGFLOAT_MAX, CGFLOAT_MAX))); ASSnapshotVerifyNode(textNode, nil); } @@ -40,13 +39,14 @@ textNode.attributedText = [[NSAttributedString alloc] initWithString:@"judar judar judar judar judar judar" attributes:@{ NSFontAttributeName : [UIFont systemFontOfSize:30] }]; - [textNode layoutThatFits:ASSizeRangeMake(CGSizeZero, CGSizeMake(100, 80))]; - textNode.frame = CGRectMake(50, 50, textNode.calculatedSize.width, textNode.calculatedSize.height); textNode.textContainerInset = UIEdgeInsetsMake(10, 10, 10, 10); + + ASLayout *layout = [textNode layoutThatFits:ASSizeRangeMake(CGSizeZero, CGSizeMake(100, 80))]; + textNode.frame = CGRectMake(50, 50, layout.size.width, layout.size.height); [backgroundView addSubview:textNode.view]; backgroundView.frame = UIEdgeInsetsInsetRect(textNode.bounds, UIEdgeInsetsMake(-50, -50, -50, -50)); - + textNode.highlightRange = NSMakeRange(0, textNode.attributedText.length); [ASSnapshotTestCase hackilySynchronouslyRecursivelyRenderNode:textNode]; @@ -62,9 +62,9 @@ textNode.attributedText = [[NSAttributedString alloc] initWithString:@"yolo" attributes:@{ NSFontAttributeName : [UIFont systemFontOfSize:30] }]; - [textNode layoutThatFits:ASSizeRangeMake(CGSizeZero, CGSizeMake(CGFLOAT_MAX, CGFLOAT_MAX))]; - textNode.frame = CGRectMake(50, 50, textNode.calculatedSize.width, textNode.calculatedSize.height); textNode.textContainerInset = UIEdgeInsetsMake(5, 10, 10, 5); + ASLayout *layout = [textNode layoutThatFits:ASSizeRangeMake(CGSizeZero, CGSizeMake(INFINITY, INFINITY))]; + textNode.frame = CGRectMake(50, 50, layout.size.width, layout.size.height); [backgroundView addSubview:textNode.view]; backgroundView.frame = UIEdgeInsetsInsetRect(textNode.bounds, UIEdgeInsetsMake(-50, -50, -50, -50)); @@ -90,7 +90,7 @@ textNode.attributedText = [[NSAttributedString alloc] initWithString:@"Quality is Important" attributes:@{ NSForegroundColorAttributeName: [UIColor blueColor], NSFontAttributeName: [UIFont italicSystemFontOfSize:24] }]; // Set exclusion paths to trigger slow path textNode.exclusionPaths = @[ [UIBezierPath bezierPath] ]; - [textNode layoutThatFits:ASSizeRangeMake(CGSizeZero, CGSizeMake(100, 50))]; + ASDisplayNodeSizeToFitSizeRange(textNode, ASSizeRangeMake(CGSizeZero, CGSizeMake(100, 50))); ASSnapshotVerifyNode(textNode, nil); } @@ -102,7 +102,7 @@ textNode.shadowOpacity = 0.3; textNode.shadowRadius = 3; textNode.shadowOffset = CGSizeMake(0, 1); - [textNode layoutThatFits:ASSizeRangeMake(CGSizeZero, CGSizeMake(CGFLOAT_MAX, CGFLOAT_MAX))]; + ASDisplayNodeSizeToFitSizeRange(textNode, ASSizeRangeMake(CGSizeZero, CGSizeMake(INFINITY, INFINITY))); ASSnapshotVerifyNode(textNode, nil); } diff --git a/AsyncDisplayKitTests/ASVideoNodeTests.m b/AsyncDisplayKitTests/ASVideoNodeTests.m index c7a0919e66..edbc57ece1 100644 --- a/AsyncDisplayKitTests/ASVideoNodeTests.m +++ b/AsyncDisplayKitTests/ASVideoNodeTests.m @@ -133,7 +133,7 @@ XCTAssertNil(_videoNode.player); } -- (void)testPlayerIsCreatedAsynchronouslyInFetchData +- (void)testPlayerIsCreatedAsynchronouslyInPreload { AVAsset *asset = _firstAsset; @@ -151,7 +151,7 @@ XCTAssertNotNil(_videoNode.player); } -- (void)testPlayerIsCreatedAsynchronouslyInFetchDataWithURL +- (void)testPlayerIsCreatedAsynchronouslyInPreloadWithURL { AVAsset *asset = [AVAsset assetWithURL:_url]; @@ -387,7 +387,7 @@ XCTAssertNotEqual(firstImage, _videoNode.image); } -- (void)testClearingFetchedContentShouldClearAssetData +- (void)testClearingPreloadedContentShouldClearAssetData { AVAsset *asset = _firstAsset; @@ -398,7 +398,7 @@ [[[videoNodeMock expect] andForwardToRealObject] prepareToPlayAsset:assetMock withKeys:_requestedKeys]; _videoNode.asset = assetMock; - [_videoNode fetchData]; + [_videoNode didEnterPreloadState]; [_videoNode setVideoPlaceholderImage:[[UIImage alloc] init]]; [videoNodeMock verifyWithDelay:1.0f]; @@ -407,7 +407,7 @@ XCTAssertNotNil(_videoNode.currentItem); XCTAssertNotNil(_videoNode.image); - [_videoNode clearFetchedData]; + [_videoNode didExitPreloadState]; XCTAssertNil(_videoNode.player); XCTAssertNil(_videoNode.currentItem); XCTAssertNil(_videoNode.image); diff --git a/AsyncDisplayKitTests/ReferenceImages_64/ASStackLayoutSpecSnapshotTests/testJustifiedSpaceAroundWithRemainingSpace@2x.png b/AsyncDisplayKitTests/ReferenceImages_64/ASStackLayoutSpecSnapshotTests/testJustifiedSpaceAroundWithRemainingSpace@2x.png index 088b49a293..6de6f28efc 100644 Binary files a/AsyncDisplayKitTests/ReferenceImages_64/ASStackLayoutSpecSnapshotTests/testJustifiedSpaceAroundWithRemainingSpace@2x.png and b/AsyncDisplayKitTests/ReferenceImages_64/ASStackLayoutSpecSnapshotTests/testJustifiedSpaceAroundWithRemainingSpace@2x.png differ diff --git a/AsyncDisplayKitTests/ReferenceImages_64/ASStackLayoutSpecSnapshotTests/testJustifiedSpaceBetweenWithRemainingSpace@2x.png b/AsyncDisplayKitTests/ReferenceImages_64/ASStackLayoutSpecSnapshotTests/testJustifiedSpaceBetweenWithRemainingSpace@2x.png index 4f85986130..8ee77c8de5 100644 Binary files a/AsyncDisplayKitTests/ReferenceImages_64/ASStackLayoutSpecSnapshotTests/testJustifiedSpaceBetweenWithRemainingSpace@2x.png and b/AsyncDisplayKitTests/ReferenceImages_64/ASStackLayoutSpecSnapshotTests/testJustifiedSpaceBetweenWithRemainingSpace@2x.png differ diff --git a/examples/ASCollectionView/Sample.xcworkspace/contents.xcworkspacedata b/examples/ASCollectionView/Sample.xcworkspace/contents.xcworkspacedata deleted file mode 100644 index 7b5a2f3050..0000000000 --- a/examples/ASCollectionView/Sample.xcworkspace/contents.xcworkspacedata +++ /dev/null @@ -1,10 +0,0 @@ - - - - - - - diff --git a/examples/ASDKLayoutTransition/Sample/ViewController.m b/examples/ASDKLayoutTransition/Sample/ViewController.m index 28f08b251c..789d5029c4 100644 --- a/examples/ASDKLayoutTransition/Sample/ViewController.m +++ b/examples/ASDKLayoutTransition/Sample/ViewController.m @@ -61,11 +61,7 @@ _buttonNode = [[ASButtonNode alloc] init]; [_buttonNode setTitle:buttonTitle withFont:buttonFont withColor:buttonColor forState:ASControlStateNormal]; - - // Note: Currently we have to set all the button properties to the same one as for ASControlStateNormal. Otherwise - // if the button is involved in the layout transition it would break the transition as it does a layout pass - // while changing the title. This needs and will be fixed in the future! - [_buttonNode setTitle:buttonTitle withFont:buttonFont withColor:buttonColor forState:ASControlStateHighlighted]; + [_buttonNode setTitle:buttonTitle withFont:buttonFont withColor:[buttonColor colorWithAlphaComponent:0.5] forState:ASControlStateHighlighted]; // Some debug colors @@ -80,7 +76,7 @@ { [super didLoad]; - [self.buttonNode addTarget:self action:@selector(buttonPressed:) forControlEvents:ASControlNodeEventTouchDown]; + [self.buttonNode addTarget:self action:@selector(buttonPressed:) forControlEvents:ASControlNodeEventTouchUpInside]; } #pragma mark - Actions @@ -88,7 +84,6 @@ - (void)buttonPressed:(id)sender { self.enabled = !self.enabled; - [self transitionLayoutWithAnimation:YES shouldMeasureAsync:NO measurementCompletion:nil]; } diff --git a/examples/ASDKTube/Sample.xcworkspace/contents.xcworkspacedata b/examples/ASDKTube/Sample.xcworkspace/contents.xcworkspacedata deleted file mode 100644 index 7b5a2f3050..0000000000 --- a/examples/ASDKTube/Sample.xcworkspace/contents.xcworkspacedata +++ /dev/null @@ -1,10 +0,0 @@ - - - - - - - diff --git a/examples/ASDKgram/Sample.xcworkspace/contents.xcworkspacedata b/examples/ASDKgram/Sample.xcworkspace/contents.xcworkspacedata deleted file mode 100644 index 7b5a2f3050..0000000000 --- a/examples/ASDKgram/Sample.xcworkspace/contents.xcworkspacedata +++ /dev/null @@ -1,10 +0,0 @@ - - - - - - - diff --git a/examples/ASMapNode/Sample.xcworkspace/contents.xcworkspacedata b/examples/ASMapNode/Sample.xcworkspace/contents.xcworkspacedata deleted file mode 100644 index 7b5a2f3050..0000000000 --- a/examples/ASMapNode/Sample.xcworkspace/contents.xcworkspacedata +++ /dev/null @@ -1,10 +0,0 @@ - - - - - - - diff --git a/examples/ASViewController/Sample/DetailViewController.m b/examples/ASViewController/Sample/DetailViewController.m index 6b6b1a902b..494efd4ffd 100644 --- a/examples/ASViewController/Sample/DetailViewController.m +++ b/examples/ASViewController/Sample/DetailViewController.m @@ -30,4 +30,5 @@ [self.node.collectionNode.view.collectionViewLayout invalidateLayout]; } + @end diff --git a/examples/CustomCollectionView-Swift/Podfile b/examples/CustomCollectionView-Swift/Podfile new file mode 100644 index 0000000000..33cfc86257 --- /dev/null +++ b/examples/CustomCollectionView-Swift/Podfile @@ -0,0 +1,8 @@ +source 'https://github.com/CocoaPods/Specs.git' +platform :ios, '7.0' + +use_frameworks! + +target 'Sample' do + pod 'AsyncDisplayKit', :path => '../..' +end \ No newline at end of file diff --git a/examples/CustomCollectionView-Swift/Sample.xcodeproj/project.pbxproj b/examples/CustomCollectionView-Swift/Sample.xcodeproj/project.pbxproj new file mode 100644 index 0000000000..621297c9c2 --- /dev/null +++ b/examples/CustomCollectionView-Swift/Sample.xcodeproj/project.pbxproj @@ -0,0 +1,787 @@ + + + + + archiveVersion + 1 + classes + + objectVersion + 46 + objects + + 27F2D2683285DCB73EE734BB + + fileRef + F7DA4A9952245B7E9BA8201F + isa + PBXBuildFile + + 3A21D910663270E99063573E + + children + + A9E9A143FF858FD89A482A84 + 73F1C0E45062A0A8CDC033A1 + + isa + PBXGroup + name + Pods + sourceTree + <group> + + 5D823AC81DD3B7760075E14A + + children + + 5D823AD31DD3B7770075E14A + 5D823AD21DD3B7770075E14A + 3A21D910663270E99063573E + 728C7877715727493EFEE42D + + isa + PBXGroup + sourceTree + <group> + + 5D823AC91DD3B7760075E14A + + attributes + + LastSwiftUpdateCheck + 0810 + LastUpgradeCheck + 0810 + ORGANIZATIONNAME + AsyncDisplayKit + TargetAttributes + + 5D823AD01DD3B7770075E14A + + CreatedOnToolsVersion + 8.1 + DevelopmentTeam + 888KTQ92ZP + ProvisioningStyle + Automatic + + + + buildConfigurationList + 5D823ACC1DD3B7760075E14A + compatibilityVersion + Xcode 3.2 + developmentRegion + English + hasScannedForEncodings + 0 + isa + PBXProject + knownRegions + + en + Base + + mainGroup + 5D823AC81DD3B7760075E14A + productRefGroup + 5D823AD21DD3B7770075E14A + projectDirPath + + projectReferences + + projectRoot + + targets + + 5D823AD01DD3B7770075E14A + + + 5D823ACC1DD3B7760075E14A + + buildConfigurations + + 5D823AE11DD3B7770075E14A + 5D823AE21DD3B7770075E14A + + defaultConfigurationIsVisible + 0 + defaultConfigurationName + Release + isa + XCConfigurationList + + 5D823ACD1DD3B7770075E14A + + buildActionMask + 2147483647 + files + + 5D823AE71DD3B7D30075E14A + 5D823AD71DD3B7770075E14A + 5D823AD51DD3B7770075E14A + 5D823AE91DD3B7D70075E14A + + isa + PBXSourcesBuildPhase + runOnlyForDeploymentPostprocessing + 0 + + 5D823ACE1DD3B7770075E14A + + buildActionMask + 2147483647 + files + + 27F2D2683285DCB73EE734BB + + isa + PBXFrameworksBuildPhase + runOnlyForDeploymentPostprocessing + 0 + + 5D823ACF1DD3B7770075E14A + + buildActionMask + 2147483647 + files + + 5D823ADF1DD3B7770075E14A + 5D823ADC1DD3B7770075E14A + 5D823ADA1DD3B7770075E14A + + isa + PBXResourcesBuildPhase + runOnlyForDeploymentPostprocessing + 0 + + 5D823AD01DD3B7770075E14A + + buildConfigurationList + 5D823AE31DD3B7770075E14A + buildPhases + + BAA73690D42731AA5D8001CF + 5D823ACD1DD3B7770075E14A + 5D823ACE1DD3B7770075E14A + 5D823ACF1DD3B7770075E14A + 8293091514A70C5E7E487A36 + 641DF857294FFEAA1878D05C + + buildRules + + dependencies + + isa + PBXNativeTarget + name + Sample + productName + Sample + productReference + 5D823AD11DD3B7770075E14A + productType + com.apple.product-type.application + + 5D823AD11DD3B7770075E14A + + explicitFileType + wrapper.application + includeInIndex + 0 + isa + PBXFileReference + path + Sample.app + sourceTree + BUILT_PRODUCTS_DIR + + 5D823AD21DD3B7770075E14A + + children + + 5D823AD11DD3B7770075E14A + + isa + PBXGroup + name + Products + sourceTree + <group> + + 5D823AD31DD3B7770075E14A + + children + + 5D823AD41DD3B7770075E14A + 5D823AD61DD3B7770075E14A + 5D823AE81DD3B7D70075E14A + 5D823AE61DD3B7D30075E14A + 5D823AD81DD3B7770075E14A + 5D823ADB1DD3B7770075E14A + 5D823ADD1DD3B7770075E14A + 5D823AE01DD3B7770075E14A + + isa + PBXGroup + path + Sample + sourceTree + <group> + + 5D823AD41DD3B7770075E14A + + isa + PBXFileReference + lastKnownFileType + sourcecode.swift + path + AppDelegate.swift + sourceTree + <group> + + 5D823AD51DD3B7770075E14A + + fileRef + 5D823AD41DD3B7770075E14A + isa + PBXBuildFile + + 5D823AD61DD3B7770075E14A + + isa + PBXFileReference + lastKnownFileType + sourcecode.swift + path + ViewController.swift + sourceTree + <group> + + 5D823AD71DD3B7770075E14A + + fileRef + 5D823AD61DD3B7770075E14A + isa + PBXBuildFile + + 5D823AD81DD3B7770075E14A + + children + + 5D823AD91DD3B7770075E14A + + isa + PBXVariantGroup + name + Main.storyboard + sourceTree + <group> + + 5D823AD91DD3B7770075E14A + + isa + PBXFileReference + lastKnownFileType + file.storyboard + name + Base + path + Base.lproj/Main.storyboard + sourceTree + <group> + + 5D823ADA1DD3B7770075E14A + + fileRef + 5D823AD81DD3B7770075E14A + isa + PBXBuildFile + + 5D823ADB1DD3B7770075E14A + + isa + PBXFileReference + lastKnownFileType + folder.assetcatalog + path + Assets.xcassets + sourceTree + <group> + + 5D823ADC1DD3B7770075E14A + + fileRef + 5D823ADB1DD3B7770075E14A + isa + PBXBuildFile + + 5D823ADD1DD3B7770075E14A + + children + + 5D823ADE1DD3B7770075E14A + + isa + PBXVariantGroup + name + LaunchScreen.storyboard + sourceTree + <group> + + 5D823ADE1DD3B7770075E14A + + isa + PBXFileReference + lastKnownFileType + file.storyboard + name + Base + path + Base.lproj/LaunchScreen.storyboard + sourceTree + <group> + + 5D823ADF1DD3B7770075E14A + + fileRef + 5D823ADD1DD3B7770075E14A + isa + PBXBuildFile + + 5D823AE01DD3B7770075E14A + + isa + PBXFileReference + lastKnownFileType + text.plist.xml + path + Info.plist + sourceTree + <group> + + 5D823AE11DD3B7770075E14A + + buildSettings + + ALWAYS_SEARCH_USER_PATHS + NO + CLANG_ANALYZER_NONNULL + YES + CLANG_CXX_LANGUAGE_STANDARD + gnu++0x + CLANG_CXX_LIBRARY + libc++ + CLANG_ENABLE_MODULES + YES + CLANG_ENABLE_OBJC_ARC + YES + CLANG_WARN_BOOL_CONVERSION + YES + CLANG_WARN_CONSTANT_CONVERSION + YES + CLANG_WARN_DIRECT_OBJC_ISA_USAGE + YES_ERROR + CLANG_WARN_DOCUMENTATION_COMMENTS + YES + CLANG_WARN_EMPTY_BODY + YES + CLANG_WARN_ENUM_CONVERSION + YES + CLANG_WARN_INFINITE_RECURSION + YES + CLANG_WARN_INT_CONVERSION + YES + CLANG_WARN_OBJC_ROOT_CLASS + YES_ERROR + CLANG_WARN_SUSPICIOUS_MOVE + YES + CLANG_WARN_SUSPICIOUS_MOVES + YES + CLANG_WARN_UNREACHABLE_CODE + YES + CLANG_WARN__DUPLICATE_METHOD_MATCH + YES + CODE_SIGN_IDENTITY[sdk=iphoneos*] + iPhone Developer + COPY_PHASE_STRIP + NO + DEBUG_INFORMATION_FORMAT + dwarf + ENABLE_STRICT_OBJC_MSGSEND + YES + ENABLE_TESTABILITY + YES + GCC_C_LANGUAGE_STANDARD + gnu99 + GCC_DYNAMIC_NO_PIC + NO + GCC_NO_COMMON_BLOCKS + YES + GCC_OPTIMIZATION_LEVEL + 0 + GCC_PREPROCESSOR_DEFINITIONS + + DEBUG=1 + $(inherited) + + GCC_WARN_64_TO_32_BIT_CONVERSION + YES + GCC_WARN_ABOUT_RETURN_TYPE + YES_ERROR + GCC_WARN_UNDECLARED_SELECTOR + YES + GCC_WARN_UNINITIALIZED_AUTOS + YES_AGGRESSIVE + GCC_WARN_UNUSED_FUNCTION + YES + GCC_WARN_UNUSED_VARIABLE + YES + IPHONEOS_DEPLOYMENT_TARGET + 9.3 + MTL_ENABLE_DEBUG_INFO + YES + ONLY_ACTIVE_ARCH + YES + SDKROOT + iphoneos + SWIFT_ACTIVE_COMPILATION_CONDITIONS + DEBUG + SWIFT_OPTIMIZATION_LEVEL + -Onone + + isa + XCBuildConfiguration + name + Debug + + 5D823AE21DD3B7770075E14A + + buildSettings + + ALWAYS_SEARCH_USER_PATHS + NO + CLANG_ANALYZER_NONNULL + YES + CLANG_CXX_LANGUAGE_STANDARD + gnu++0x + CLANG_CXX_LIBRARY + libc++ + CLANG_ENABLE_MODULES + YES + CLANG_ENABLE_OBJC_ARC + YES + CLANG_WARN_BOOL_CONVERSION + YES + CLANG_WARN_CONSTANT_CONVERSION + YES + CLANG_WARN_DIRECT_OBJC_ISA_USAGE + YES_ERROR + CLANG_WARN_DOCUMENTATION_COMMENTS + YES + CLANG_WARN_EMPTY_BODY + YES + CLANG_WARN_ENUM_CONVERSION + YES + CLANG_WARN_INFINITE_RECURSION + YES + CLANG_WARN_INT_CONVERSION + YES + CLANG_WARN_OBJC_ROOT_CLASS + YES_ERROR + CLANG_WARN_SUSPICIOUS_MOVE + YES + CLANG_WARN_SUSPICIOUS_MOVES + YES + CLANG_WARN_UNREACHABLE_CODE + YES + CLANG_WARN__DUPLICATE_METHOD_MATCH + YES + CODE_SIGN_IDENTITY[sdk=iphoneos*] + iPhone Developer + COPY_PHASE_STRIP + NO + DEBUG_INFORMATION_FORMAT + dwarf-with-dsym + ENABLE_NS_ASSERTIONS + NO + ENABLE_STRICT_OBJC_MSGSEND + YES + GCC_C_LANGUAGE_STANDARD + gnu99 + GCC_NO_COMMON_BLOCKS + YES + GCC_WARN_64_TO_32_BIT_CONVERSION + YES + GCC_WARN_ABOUT_RETURN_TYPE + YES_ERROR + GCC_WARN_UNDECLARED_SELECTOR + YES + GCC_WARN_UNINITIALIZED_AUTOS + YES_AGGRESSIVE + GCC_WARN_UNUSED_FUNCTION + YES + GCC_WARN_UNUSED_VARIABLE + YES + IPHONEOS_DEPLOYMENT_TARGET + 9.3 + MTL_ENABLE_DEBUG_INFO + NO + SDKROOT + iphoneos + SWIFT_OPTIMIZATION_LEVEL + -Owholemodule + VALIDATE_PRODUCT + YES + + isa + XCBuildConfiguration + name + Release + + 5D823AE31DD3B7770075E14A + + buildConfigurations + + 5D823AE41DD3B7770075E14A + 5D823AE51DD3B7770075E14A + + defaultConfigurationIsVisible + 0 + defaultConfigurationName + Release + isa + XCConfigurationList + + 5D823AE41DD3B7770075E14A + + baseConfigurationReference + A9E9A143FF858FD89A482A84 + buildSettings + + ASSETCATALOG_COMPILER_APPICON_NAME + AppIcon + DEVELOPMENT_TEAM + 888KTQ92ZP + INFOPLIST_FILE + Sample/Info.plist + LD_RUNPATH_SEARCH_PATHS + $(inherited) @executable_path/Frameworks + PRODUCT_BUNDLE_IDENTIFIER + com.facebook.AsyncDisplayKit.Sample + PRODUCT_NAME + $(TARGET_NAME) + SWIFT_VERSION + 3.0 + + isa + XCBuildConfiguration + name + Debug + + 5D823AE51DD3B7770075E14A + + baseConfigurationReference + 73F1C0E45062A0A8CDC033A1 + buildSettings + + ASSETCATALOG_COMPILER_APPICON_NAME + AppIcon + DEVELOPMENT_TEAM + 888KTQ92ZP + INFOPLIST_FILE + Sample/Info.plist + LD_RUNPATH_SEARCH_PATHS + $(inherited) @executable_path/Frameworks + PRODUCT_BUNDLE_IDENTIFIER + com.facebook.AsyncDisplayKit.Sample + PRODUCT_NAME + $(TARGET_NAME) + SWIFT_VERSION + 3.0 + + isa + XCBuildConfiguration + name + Release + + 5D823AE61DD3B7D30075E14A + + fileEncoding + 4 + isa + PBXFileReference + lastKnownFileType + sourcecode.swift + path + MosaicCollectionViewLayout.swift + sourceTree + <group> + + 5D823AE71DD3B7D30075E14A + + fileRef + 5D823AE61DD3B7D30075E14A + isa + PBXBuildFile + + 5D823AE81DD3B7D70075E14A + + fileEncoding + 4 + isa + PBXFileReference + lastKnownFileType + sourcecode.swift + path + ImageCellNode.swift + sourceTree + <group> + + 5D823AE91DD3B7D70075E14A + + fileRef + 5D823AE81DD3B7D70075E14A + isa + PBXBuildFile + + 641DF857294FFEAA1878D05C + + buildActionMask + 2147483647 + files + + inputPaths + + isa + PBXShellScriptBuildPhase + name + [CP] Copy Pods Resources + outputPaths + + runOnlyForDeploymentPostprocessing + 0 + shellPath + /bin/sh + shellScript + "${SRCROOT}/Pods/Target Support Files/Pods-Sample/Pods-Sample-resources.sh" + + showEnvVarsInLog + 0 + + 728C7877715727493EFEE42D + + children + + F7DA4A9952245B7E9BA8201F + + isa + PBXGroup + name + Frameworks + sourceTree + <group> + + 73F1C0E45062A0A8CDC033A1 + + includeInIndex + 1 + isa + PBXFileReference + lastKnownFileType + text.xcconfig + name + Pods-Sample.release.xcconfig + path + Pods/Target Support Files/Pods-Sample/Pods-Sample.release.xcconfig + sourceTree + <group> + + 8293091514A70C5E7E487A36 + + buildActionMask + 2147483647 + files + + inputPaths + + isa + PBXShellScriptBuildPhase + name + [CP] Embed Pods Frameworks + outputPaths + + runOnlyForDeploymentPostprocessing + 0 + shellPath + /bin/sh + shellScript + "${SRCROOT}/Pods/Target Support Files/Pods-Sample/Pods-Sample-frameworks.sh" + + showEnvVarsInLog + 0 + + A9E9A143FF858FD89A482A84 + + includeInIndex + 1 + isa + PBXFileReference + lastKnownFileType + text.xcconfig + name + Pods-Sample.debug.xcconfig + path + Pods/Target Support Files/Pods-Sample/Pods-Sample.debug.xcconfig + sourceTree + <group> + + BAA73690D42731AA5D8001CF + + buildActionMask + 2147483647 + files + + inputPaths + + isa + PBXShellScriptBuildPhase + name + [CP] Check Pods Manifest.lock + outputPaths + + runOnlyForDeploymentPostprocessing + 0 + shellPath + /bin/sh + shellScript + diff "${PODS_ROOT}/../Podfile.lock" "${PODS_ROOT}/Manifest.lock" > /dev/null +if [[ $? != 0 ]] ; then + cat << EOM +error: The sandbox is not in sync with the Podfile.lock. Run 'pod install' or update your CocoaPods installation. +EOM + exit 1 +fi + + showEnvVarsInLog + 0 + + F7DA4A9952245B7E9BA8201F + + explicitFileType + wrapper.framework + includeInIndex + 0 + isa + PBXFileReference + path + Pods_Sample.framework + sourceTree + BUILT_PRODUCTS_DIR + + + rootObject + 5D823AC91DD3B7760075E14A + + diff --git a/examples/CustomCollectionView-Swift/Sample.xcodeproj/project.xcworkspace/contents.xcworkspacedata b/examples/CustomCollectionView-Swift/Sample.xcodeproj/project.xcworkspace/contents.xcworkspacedata new file mode 100644 index 0000000000..a80c038249 --- /dev/null +++ b/examples/CustomCollectionView-Swift/Sample.xcodeproj/project.xcworkspace/contents.xcworkspacedata @@ -0,0 +1,7 @@ + + + + + diff --git a/examples/CustomCollectionView-Swift/Sample.xcodeproj/xcshareddata/xcschemes/Sample.xcscheme b/examples/CustomCollectionView-Swift/Sample.xcodeproj/xcshareddata/xcschemes/Sample.xcscheme new file mode 100644 index 0000000000..1c12aaa4d4 --- /dev/null +++ b/examples/CustomCollectionView-Swift/Sample.xcodeproj/xcshareddata/xcschemes/Sample.xcscheme @@ -0,0 +1,91 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/examples/CustomCollectionView-Swift/Sample/AppDelegate.swift b/examples/CustomCollectionView-Swift/Sample/AppDelegate.swift new file mode 100644 index 0000000000..13e0670d1b --- /dev/null +++ b/examples/CustomCollectionView-Swift/Sample/AppDelegate.swift @@ -0,0 +1,57 @@ +// +// AppDelegate.swift +// Sample +// +// Created by Rajeev Gupta on 11/9/16. +// +// Copyright (c) 2014-present, Facebook, Inc. All rights reserved. +// This source code is licensed under the BSD-style license found in the +// LICENSE file in the root directory of this source tree. An additional grant +// of patent rights can be found in the PATENTS file in the same directory. +// +// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL +// FACEBOOK BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN +// ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN +// CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. +// + +import UIKit + +@UIApplicationMain +class AppDelegate: UIResponder, UIApplicationDelegate { + + var window: UIWindow? + + + func application(_ application: UIApplication, didFinishLaunchingWithOptions launchOptions: [UIApplicationLaunchOptionsKey: Any]?) -> Bool { + // Override point for customization after application launch. + return true + } + + func applicationWillResignActive(_ application: UIApplication) { + // Sent when the application is about to move from active to inactive state. This can occur for certain types of temporary interruptions (such as an incoming phone call or SMS message) or when the user quits the application and it begins the transition to the background state. + // Use this method to pause ongoing tasks, disable timers, and invalidate graphics rendering callbacks. Games should use this method to pause the game. + } + + func applicationDidEnterBackground(_ application: UIApplication) { + // Use this method to release shared resources, save user data, invalidate timers, and store enough application state information to restore your application to its current state in case it is terminated later. + // If your application supports background execution, this method is called instead of applicationWillTerminate: when the user quits. + } + + func applicationWillEnterForeground(_ application: UIApplication) { + // Called as part of the transition from the background to the active state; here you can undo many of the changes made on entering the background. + } + + func applicationDidBecomeActive(_ application: UIApplication) { + // Restart any tasks that were paused (or not yet started) while the application was inactive. If the application was previously in the background, optionally refresh the user interface. + } + + func applicationWillTerminate(_ application: UIApplication) { + // Called when the application is about to terminate. Save data if appropriate. See also applicationDidEnterBackground:. + } + + +} + diff --git a/examples/CustomCollectionView-Swift/Sample/Assets.xcassets/AppIcon.appiconset/Contents.json b/examples/CustomCollectionView-Swift/Sample/Assets.xcassets/AppIcon.appiconset/Contents.json new file mode 100644 index 0000000000..b8236c6534 --- /dev/null +++ b/examples/CustomCollectionView-Swift/Sample/Assets.xcassets/AppIcon.appiconset/Contents.json @@ -0,0 +1,48 @@ +{ + "images" : [ + { + "idiom" : "iphone", + "size" : "20x20", + "scale" : "2x" + }, + { + "idiom" : "iphone", + "size" : "20x20", + "scale" : "3x" + }, + { + "idiom" : "iphone", + "size" : "29x29", + "scale" : "2x" + }, + { + "idiom" : "iphone", + "size" : "29x29", + "scale" : "3x" + }, + { + "idiom" : "iphone", + "size" : "40x40", + "scale" : "2x" + }, + { + "idiom" : "iphone", + "size" : "40x40", + "scale" : "3x" + }, + { + "idiom" : "iphone", + "size" : "60x60", + "scale" : "2x" + }, + { + "idiom" : "iphone", + "size" : "60x60", + "scale" : "3x" + } + ], + "info" : { + "version" : 1, + "author" : "xcode" + } +} \ No newline at end of file diff --git a/examples/CustomCollectionView-Swift/Sample/Assets.xcassets/Contents.json b/examples/CustomCollectionView-Swift/Sample/Assets.xcassets/Contents.json new file mode 100644 index 0000000000..da4a164c91 --- /dev/null +++ b/examples/CustomCollectionView-Swift/Sample/Assets.xcassets/Contents.json @@ -0,0 +1,6 @@ +{ + "info" : { + "version" : 1, + "author" : "xcode" + } +} \ No newline at end of file diff --git a/examples/CustomCollectionView-Swift/Sample/Assets.xcassets/image_0.imageset/Contents.json b/examples/CustomCollectionView-Swift/Sample/Assets.xcassets/image_0.imageset/Contents.json new file mode 100644 index 0000000000..09ec0851ee --- /dev/null +++ b/examples/CustomCollectionView-Swift/Sample/Assets.xcassets/image_0.imageset/Contents.json @@ -0,0 +1,21 @@ +{ + "images" : [ + { + "idiom" : "universal", + "filename" : "image_0.jpg", + "scale" : "1x" + }, + { + "idiom" : "universal", + "scale" : "2x" + }, + { + "idiom" : "universal", + "scale" : "3x" + } + ], + "info" : { + "version" : 1, + "author" : "xcode" + } +} \ No newline at end of file diff --git a/examples/CustomCollectionView-Swift/Sample/Assets.xcassets/image_0.imageset/image_0.jpg b/examples/CustomCollectionView-Swift/Sample/Assets.xcassets/image_0.imageset/image_0.jpg new file mode 100644 index 0000000000..4a365897ea Binary files /dev/null and b/examples/CustomCollectionView-Swift/Sample/Assets.xcassets/image_0.imageset/image_0.jpg differ diff --git a/examples/CustomCollectionView-Swift/Sample/Assets.xcassets/image_1.imageset/Contents.json b/examples/CustomCollectionView-Swift/Sample/Assets.xcassets/image_1.imageset/Contents.json new file mode 100644 index 0000000000..6d2e9f5f7c --- /dev/null +++ b/examples/CustomCollectionView-Swift/Sample/Assets.xcassets/image_1.imageset/Contents.json @@ -0,0 +1,21 @@ +{ + "images" : [ + { + "idiom" : "universal", + "filename" : "image_1.jpg", + "scale" : "1x" + }, + { + "idiom" : "universal", + "scale" : "2x" + }, + { + "idiom" : "universal", + "scale" : "3x" + } + ], + "info" : { + "version" : 1, + "author" : "xcode" + } +} \ No newline at end of file diff --git a/examples/CustomCollectionView-Swift/Sample/Assets.xcassets/image_1.imageset/image_1.jpg b/examples/CustomCollectionView-Swift/Sample/Assets.xcassets/image_1.imageset/image_1.jpg new file mode 100644 index 0000000000..5cb4828f44 Binary files /dev/null and b/examples/CustomCollectionView-Swift/Sample/Assets.xcassets/image_1.imageset/image_1.jpg differ diff --git a/examples/CustomCollectionView-Swift/Sample/Assets.xcassets/image_10.imageset/Contents.json b/examples/CustomCollectionView-Swift/Sample/Assets.xcassets/image_10.imageset/Contents.json new file mode 100644 index 0000000000..ea10700189 --- /dev/null +++ b/examples/CustomCollectionView-Swift/Sample/Assets.xcassets/image_10.imageset/Contents.json @@ -0,0 +1,21 @@ +{ + "images" : [ + { + "idiom" : "universal", + "filename" : "image_10.jpg", + "scale" : "1x" + }, + { + "idiom" : "universal", + "scale" : "2x" + }, + { + "idiom" : "universal", + "scale" : "3x" + } + ], + "info" : { + "version" : 1, + "author" : "xcode" + } +} \ No newline at end of file diff --git a/examples/CustomCollectionView-Swift/Sample/Assets.xcassets/image_10.imageset/image_10.jpg b/examples/CustomCollectionView-Swift/Sample/Assets.xcassets/image_10.imageset/image_10.jpg new file mode 100644 index 0000000000..ea5cd6d268 Binary files /dev/null and b/examples/CustomCollectionView-Swift/Sample/Assets.xcassets/image_10.imageset/image_10.jpg differ diff --git a/examples/CustomCollectionView-Swift/Sample/Assets.xcassets/image_11.imageset/Contents.json b/examples/CustomCollectionView-Swift/Sample/Assets.xcassets/image_11.imageset/Contents.json new file mode 100644 index 0000000000..dc85469057 --- /dev/null +++ b/examples/CustomCollectionView-Swift/Sample/Assets.xcassets/image_11.imageset/Contents.json @@ -0,0 +1,21 @@ +{ + "images" : [ + { + "idiom" : "universal", + "filename" : "image_11.jpg", + "scale" : "1x" + }, + { + "idiom" : "universal", + "scale" : "2x" + }, + { + "idiom" : "universal", + "scale" : "3x" + } + ], + "info" : { + "version" : 1, + "author" : "xcode" + } +} \ No newline at end of file diff --git a/examples/CustomCollectionView-Swift/Sample/Assets.xcassets/image_11.imageset/image_11.jpg b/examples/CustomCollectionView-Swift/Sample/Assets.xcassets/image_11.imageset/image_11.jpg new file mode 100644 index 0000000000..e93c68e512 Binary files /dev/null and b/examples/CustomCollectionView-Swift/Sample/Assets.xcassets/image_11.imageset/image_11.jpg differ diff --git a/examples/CustomCollectionView-Swift/Sample/Assets.xcassets/image_12.imageset/Contents.json b/examples/CustomCollectionView-Swift/Sample/Assets.xcassets/image_12.imageset/Contents.json new file mode 100644 index 0000000000..a6d99003d1 --- /dev/null +++ b/examples/CustomCollectionView-Swift/Sample/Assets.xcassets/image_12.imageset/Contents.json @@ -0,0 +1,21 @@ +{ + "images" : [ + { + "idiom" : "universal", + "filename" : "image_12.jpg", + "scale" : "1x" + }, + { + "idiom" : "universal", + "scale" : "2x" + }, + { + "idiom" : "universal", + "scale" : "3x" + } + ], + "info" : { + "version" : 1, + "author" : "xcode" + } +} \ No newline at end of file diff --git a/examples/CustomCollectionView-Swift/Sample/Assets.xcassets/image_12.imageset/image_12.jpg b/examples/CustomCollectionView-Swift/Sample/Assets.xcassets/image_12.imageset/image_12.jpg new file mode 100644 index 0000000000..d520b6d80f Binary files /dev/null and b/examples/CustomCollectionView-Swift/Sample/Assets.xcassets/image_12.imageset/image_12.jpg differ diff --git a/examples/CustomCollectionView-Swift/Sample/Assets.xcassets/image_13.imageset/Contents.json b/examples/CustomCollectionView-Swift/Sample/Assets.xcassets/image_13.imageset/Contents.json new file mode 100644 index 0000000000..4eb6baad3b --- /dev/null +++ b/examples/CustomCollectionView-Swift/Sample/Assets.xcassets/image_13.imageset/Contents.json @@ -0,0 +1,21 @@ +{ + "images" : [ + { + "idiom" : "universal", + "filename" : "image_13.jpg", + "scale" : "1x" + }, + { + "idiom" : "universal", + "scale" : "2x" + }, + { + "idiom" : "universal", + "scale" : "3x" + } + ], + "info" : { + "version" : 1, + "author" : "xcode" + } +} \ No newline at end of file diff --git a/examples/CustomCollectionView-Swift/Sample/Assets.xcassets/image_13.imageset/image_13.jpg b/examples/CustomCollectionView-Swift/Sample/Assets.xcassets/image_13.imageset/image_13.jpg new file mode 100644 index 0000000000..c0232370cd Binary files /dev/null and b/examples/CustomCollectionView-Swift/Sample/Assets.xcassets/image_13.imageset/image_13.jpg differ diff --git a/examples/CustomCollectionView-Swift/Sample/Assets.xcassets/image_2.imageset/Contents.json b/examples/CustomCollectionView-Swift/Sample/Assets.xcassets/image_2.imageset/Contents.json new file mode 100644 index 0000000000..b2536e53de --- /dev/null +++ b/examples/CustomCollectionView-Swift/Sample/Assets.xcassets/image_2.imageset/Contents.json @@ -0,0 +1,21 @@ +{ + "images" : [ + { + "idiom" : "universal", + "filename" : "image_2.jpg", + "scale" : "1x" + }, + { + "idiom" : "universal", + "scale" : "2x" + }, + { + "idiom" : "universal", + "scale" : "3x" + } + ], + "info" : { + "version" : 1, + "author" : "xcode" + } +} \ No newline at end of file diff --git a/examples/CustomCollectionView-Swift/Sample/Assets.xcassets/image_2.imageset/image_2.jpg b/examples/CustomCollectionView-Swift/Sample/Assets.xcassets/image_2.imageset/image_2.jpg new file mode 100644 index 0000000000..175343454d Binary files /dev/null and b/examples/CustomCollectionView-Swift/Sample/Assets.xcassets/image_2.imageset/image_2.jpg differ diff --git a/examples/CustomCollectionView-Swift/Sample/Assets.xcassets/image_3.imageset/Contents.json b/examples/CustomCollectionView-Swift/Sample/Assets.xcassets/image_3.imageset/Contents.json new file mode 100644 index 0000000000..512e735090 --- /dev/null +++ b/examples/CustomCollectionView-Swift/Sample/Assets.xcassets/image_3.imageset/Contents.json @@ -0,0 +1,21 @@ +{ + "images" : [ + { + "idiom" : "universal", + "filename" : "image_3.jpg", + "scale" : "1x" + }, + { + "idiom" : "universal", + "scale" : "2x" + }, + { + "idiom" : "universal", + "scale" : "3x" + } + ], + "info" : { + "version" : 1, + "author" : "xcode" + } +} \ No newline at end of file diff --git a/examples/CustomCollectionView-Swift/Sample/Assets.xcassets/image_3.imageset/image_3.jpg b/examples/CustomCollectionView-Swift/Sample/Assets.xcassets/image_3.imageset/image_3.jpg new file mode 100644 index 0000000000..f5398cac79 Binary files /dev/null and b/examples/CustomCollectionView-Swift/Sample/Assets.xcassets/image_3.imageset/image_3.jpg differ diff --git a/examples/CustomCollectionView-Swift/Sample/Assets.xcassets/image_4.imageset/Contents.json b/examples/CustomCollectionView-Swift/Sample/Assets.xcassets/image_4.imageset/Contents.json new file mode 100644 index 0000000000..88b2b7b98a --- /dev/null +++ b/examples/CustomCollectionView-Swift/Sample/Assets.xcassets/image_4.imageset/Contents.json @@ -0,0 +1,21 @@ +{ + "images" : [ + { + "idiom" : "universal", + "filename" : "image_4.jpg", + "scale" : "1x" + }, + { + "idiom" : "universal", + "scale" : "2x" + }, + { + "idiom" : "universal", + "scale" : "3x" + } + ], + "info" : { + "version" : 1, + "author" : "xcode" + } +} \ No newline at end of file diff --git a/examples/CustomCollectionView-Swift/Sample/Assets.xcassets/image_4.imageset/image_4.jpg b/examples/CustomCollectionView-Swift/Sample/Assets.xcassets/image_4.imageset/image_4.jpg new file mode 100644 index 0000000000..2a6fe4c264 Binary files /dev/null and b/examples/CustomCollectionView-Swift/Sample/Assets.xcassets/image_4.imageset/image_4.jpg differ diff --git a/examples/CustomCollectionView-Swift/Sample/Assets.xcassets/image_5.imageset/Contents.json b/examples/CustomCollectionView-Swift/Sample/Assets.xcassets/image_5.imageset/Contents.json new file mode 100644 index 0000000000..1f24c086d9 --- /dev/null +++ b/examples/CustomCollectionView-Swift/Sample/Assets.xcassets/image_5.imageset/Contents.json @@ -0,0 +1,21 @@ +{ + "images" : [ + { + "idiom" : "universal", + "filename" : "image_5.jpg", + "scale" : "1x" + }, + { + "idiom" : "universal", + "scale" : "2x" + }, + { + "idiom" : "universal", + "scale" : "3x" + } + ], + "info" : { + "version" : 1, + "author" : "xcode" + } +} \ No newline at end of file diff --git a/examples/CustomCollectionView-Swift/Sample/Assets.xcassets/image_5.imageset/image_5.jpg b/examples/CustomCollectionView-Swift/Sample/Assets.xcassets/image_5.imageset/image_5.jpg new file mode 100644 index 0000000000..4e507b8064 Binary files /dev/null and b/examples/CustomCollectionView-Swift/Sample/Assets.xcassets/image_5.imageset/image_5.jpg differ diff --git a/examples/CustomCollectionView-Swift/Sample/Assets.xcassets/image_6.imageset/Contents.json b/examples/CustomCollectionView-Swift/Sample/Assets.xcassets/image_6.imageset/Contents.json new file mode 100644 index 0000000000..25f33f2acd --- /dev/null +++ b/examples/CustomCollectionView-Swift/Sample/Assets.xcassets/image_6.imageset/Contents.json @@ -0,0 +1,21 @@ +{ + "images" : [ + { + "idiom" : "universal", + "filename" : "image_6.jpg", + "scale" : "1x" + }, + { + "idiom" : "universal", + "scale" : "2x" + }, + { + "idiom" : "universal", + "scale" : "3x" + } + ], + "info" : { + "version" : 1, + "author" : "xcode" + } +} \ No newline at end of file diff --git a/examples/CustomCollectionView-Swift/Sample/Assets.xcassets/image_6.imageset/image_6.jpg b/examples/CustomCollectionView-Swift/Sample/Assets.xcassets/image_6.imageset/image_6.jpg new file mode 100644 index 0000000000..35fe778b3a Binary files /dev/null and b/examples/CustomCollectionView-Swift/Sample/Assets.xcassets/image_6.imageset/image_6.jpg differ diff --git a/examples/CustomCollectionView-Swift/Sample/Assets.xcassets/image_7.imageset/Contents.json b/examples/CustomCollectionView-Swift/Sample/Assets.xcassets/image_7.imageset/Contents.json new file mode 100644 index 0000000000..5fdd6ba2cf --- /dev/null +++ b/examples/CustomCollectionView-Swift/Sample/Assets.xcassets/image_7.imageset/Contents.json @@ -0,0 +1,21 @@ +{ + "images" : [ + { + "idiom" : "universal", + "filename" : "image_7.jpg", + "scale" : "1x" + }, + { + "idiom" : "universal", + "scale" : "2x" + }, + { + "idiom" : "universal", + "scale" : "3x" + } + ], + "info" : { + "version" : 1, + "author" : "xcode" + } +} \ No newline at end of file diff --git a/examples/CustomCollectionView-Swift/Sample/Assets.xcassets/image_7.imageset/image_7.jpg b/examples/CustomCollectionView-Swift/Sample/Assets.xcassets/image_7.imageset/image_7.jpg new file mode 100644 index 0000000000..8f5e037722 Binary files /dev/null and b/examples/CustomCollectionView-Swift/Sample/Assets.xcassets/image_7.imageset/image_7.jpg differ diff --git a/examples/CustomCollectionView-Swift/Sample/Assets.xcassets/image_8.imageset/Contents.json b/examples/CustomCollectionView-Swift/Sample/Assets.xcassets/image_8.imageset/Contents.json new file mode 100644 index 0000000000..563d5ba824 --- /dev/null +++ b/examples/CustomCollectionView-Swift/Sample/Assets.xcassets/image_8.imageset/Contents.json @@ -0,0 +1,21 @@ +{ + "images" : [ + { + "idiom" : "universal", + "filename" : "image_8.jpg", + "scale" : "1x" + }, + { + "idiom" : "universal", + "scale" : "2x" + }, + { + "idiom" : "universal", + "scale" : "3x" + } + ], + "info" : { + "version" : 1, + "author" : "xcode" + } +} \ No newline at end of file diff --git a/examples/CustomCollectionView-Swift/Sample/Assets.xcassets/image_8.imageset/image_8.jpg b/examples/CustomCollectionView-Swift/Sample/Assets.xcassets/image_8.imageset/image_8.jpg new file mode 100644 index 0000000000..5651436bb6 Binary files /dev/null and b/examples/CustomCollectionView-Swift/Sample/Assets.xcassets/image_8.imageset/image_8.jpg differ diff --git a/examples/CustomCollectionView-Swift/Sample/Assets.xcassets/image_9.imageset/Contents.json b/examples/CustomCollectionView-Swift/Sample/Assets.xcassets/image_9.imageset/Contents.json new file mode 100644 index 0000000000..66c1b859b1 --- /dev/null +++ b/examples/CustomCollectionView-Swift/Sample/Assets.xcassets/image_9.imageset/Contents.json @@ -0,0 +1,21 @@ +{ + "images" : [ + { + "idiom" : "universal", + "filename" : "image_9.jpg", + "scale" : "1x" + }, + { + "idiom" : "universal", + "scale" : "2x" + }, + { + "idiom" : "universal", + "scale" : "3x" + } + ], + "info" : { + "version" : 1, + "author" : "xcode" + } +} \ No newline at end of file diff --git a/examples/CustomCollectionView-Swift/Sample/Assets.xcassets/image_9.imageset/image_9.jpg b/examples/CustomCollectionView-Swift/Sample/Assets.xcassets/image_9.imageset/image_9.jpg new file mode 100644 index 0000000000..9fb6e47d3f Binary files /dev/null and b/examples/CustomCollectionView-Swift/Sample/Assets.xcassets/image_9.imageset/image_9.jpg differ diff --git a/examples/CustomCollectionView-Swift/Sample/Base.lproj/LaunchScreen.storyboard b/examples/CustomCollectionView-Swift/Sample/Base.lproj/LaunchScreen.storyboard new file mode 100644 index 0000000000..fdf3f97d1b --- /dev/null +++ b/examples/CustomCollectionView-Swift/Sample/Base.lproj/LaunchScreen.storyboard @@ -0,0 +1,27 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/examples/CustomCollectionView-Swift/Sample/Base.lproj/Main.storyboard b/examples/CustomCollectionView-Swift/Sample/Base.lproj/Main.storyboard new file mode 100644 index 0000000000..273375fc70 --- /dev/null +++ b/examples/CustomCollectionView-Swift/Sample/Base.lproj/Main.storyboard @@ -0,0 +1,26 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/examples/CustomCollectionView-Swift/Sample/ImageCellNode.swift b/examples/CustomCollectionView-Swift/Sample/ImageCellNode.swift new file mode 100644 index 0000000000..52f8fdb55f --- /dev/null +++ b/examples/CustomCollectionView-Swift/Sample/ImageCellNode.swift @@ -0,0 +1,51 @@ +// +// ImageCellNode.swift +// Sample +// +// Created by Rajeev Gupta on 11/9/16. +// +// Copyright (c) 2014-present, Facebook, Inc. All rights reserved. +// This source code is licensed under the BSD-style license found in the +// LICENSE file in the root directory of this source tree. An additional grant +// of patent rights can be found in the PATENTS file in the same directory. +// +// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL +// FACEBOOK BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN +// ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN +// CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. +// + +import UIKit +import AsyncDisplayKit + +class ImageCellNode: ASCellNode { + let imageNode = ASImageNode() + required init(with image : UIImage) { + super.init() + imageNode.image = image + self.addSubnode(self.imageNode) + } + + override func layoutSpecThatFits(_ constrainedSize: ASSizeRange) -> ASLayoutSpec { + let imageSize = imageNode.image?.size + print("imageNode= \(imageNode.bounds), image=\(imageSize)") + + var imageRatio: CGFloat = 0.5 + if imageNode.image != nil { + imageRatio = (imageNode.image?.size.height)! / (imageNode.image?.size.width)! + } + + let imagePlace = ASRatioLayoutSpec(ratio: imageRatio, child: imageNode) + + let stackLayout = ASStackLayoutSpec.horizontal() + stackLayout.justifyContent = .start + stackLayout.alignItems = .start + stackLayout.style.flexShrink = 1.0 + stackLayout.children = [imagePlace] + + return ASInsetLayoutSpec(insets: UIEdgeInsets.zero, child: stackLayout) + } + +} diff --git a/examples/CustomCollectionView-Swift/Sample/Info.plist b/examples/CustomCollectionView-Swift/Sample/Info.plist new file mode 100644 index 0000000000..38e98af23d --- /dev/null +++ b/examples/CustomCollectionView-Swift/Sample/Info.plist @@ -0,0 +1,38 @@ + + + + + CFBundleDevelopmentRegion + en + CFBundleExecutable + $(EXECUTABLE_NAME) + CFBundleIdentifier + $(PRODUCT_BUNDLE_IDENTIFIER) + CFBundleInfoDictionaryVersion + 6.0 + CFBundleName + $(PRODUCT_NAME) + CFBundlePackageType + APPL + CFBundleShortVersionString + 1.0 + CFBundleVersion + 1 + LSRequiresIPhoneOS + + UILaunchStoryboardName + LaunchScreen + UIMainStoryboardFile + Main + UIRequiredDeviceCapabilities + + armv7 + + UISupportedInterfaceOrientations + + UIInterfaceOrientationPortrait + UIInterfaceOrientationLandscapeLeft + UIInterfaceOrientationLandscapeRight + + + diff --git a/examples/CustomCollectionView-Swift/Sample/MosaicCollectionViewLayout.swift b/examples/CustomCollectionView-Swift/Sample/MosaicCollectionViewLayout.swift new file mode 100644 index 0000000000..a8770d5282 --- /dev/null +++ b/examples/CustomCollectionView-Swift/Sample/MosaicCollectionViewLayout.swift @@ -0,0 +1,256 @@ +// +// MosaicCollectionViewLayout +// Sample +// +// Created by Rajeev Gupta on 11/9/16. +// +// Copyright (c) 2014-present, Facebook, Inc. All rights reserved. +// This source code is licensed under the BSD-style license found in the +// LICENSE file in the root directory of this source tree. An additional grant +// of patent rights can be found in the PATENTS file in the same directory. +// +// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL +// FACEBOOK BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN +// ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN +// CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. +// + +import Foundation +import UIKit +import AsyncDisplayKit + +protocol MosaicCollectionViewLayoutDelegate: ASCollectionDelegate { + func collectionView(_ collectionView: UICollectionView, layout: MosaicCollectionViewLayout, originalItemSizeAtIndexPath: IndexPath) -> CGSize +} + +class MosaicCollectionViewLayout: UICollectionViewFlowLayout { + var numberOfColumns: Int + var columnSpacing: CGFloat + var _sectionInset: UIEdgeInsets + var interItemSpacing: UIEdgeInsets + var headerHeight: CGFloat + var _columnHeights: [[CGFloat]]? + var _itemAttributes = [[UICollectionViewLayoutAttributes]]() + var _headerAttributes = [UICollectionViewLayoutAttributes]() + var _allAttributes = [UICollectionViewLayoutAttributes]() + + required override init() { + self.numberOfColumns = 2 + self.columnSpacing = 10.0 + self.headerHeight = 44.0 //viewcontroller + self._sectionInset = UIEdgeInsetsMake(10.0, 10.0, 10.0, 10.0) + self.interItemSpacing = UIEdgeInsetsMake(10.0, 0, 10.0, 0) + super.init() + self.scrollDirection = .vertical + } + required init?(coder aDecoder: NSCoder) { + fatalError("init(coder:) has not been implemented") + } + + public var delegate : MosaicCollectionViewLayoutDelegate? + + override func prepare() { + super.prepare() + guard let collectionView = self.collectionView else { return } + + _itemAttributes = [] + _allAttributes = [] + _headerAttributes = [] + _columnHeights = [] + + var top: CGFloat = 0 + + let numberOfSections: NSInteger = collectionView.numberOfSections + + for section in 0 ..< numberOfSections { + let numberOfItems = collectionView.numberOfItems(inSection: section) + + top += _sectionInset.top + + if (headerHeight > 0) { + let headerSize: CGSize = self._headerSizeForSection(section: section) + + let attributes = UICollectionViewLayoutAttributes(forSupplementaryViewOfKind: UICollectionElementKindSectionHeader, with: NSIndexPath(row: 0, section: section) as IndexPath) + + attributes.frame = CGRect(x: _sectionInset.left, y: top, width: headerSize.width, height: headerSize.height) + _headerAttributes.append(attributes) + _allAttributes.append(attributes) + top = attributes.frame.maxY + } + + _columnHeights?.append([]) //Adding new Section + for _ in 0 ..< self.numberOfColumns { + self._columnHeights?[section].append(top) + } + + let columnWidth = self._columnWidthForSection(section: section) + _itemAttributes.append([]) + for idx in 0 ..< numberOfItems { + let columnIndex: Int = self._shortestColumnIndexInSection(section: section) + let indexPath = IndexPath(item: idx, section: section) + + let itemSize = self._itemSizeAtIndexPath(indexPath: indexPath); + let xOffset = _sectionInset.left + (columnWidth + columnSpacing) * CGFloat(columnIndex) + let yOffset = _columnHeights![section][columnIndex] + + let attributes = UICollectionViewLayoutAttributes(forCellWith: indexPath) + + attributes.frame = CGRect(x: xOffset, y: yOffset, width: itemSize.width, height: itemSize.height) + + _columnHeights?[section][columnIndex] = attributes.frame.maxY + interItemSpacing.bottom + + _itemAttributes[section].append(attributes) + _allAttributes.append(attributes) + } + + let columnIndex: Int = self._tallestColumnIndexInSection(section: section) + top = (_columnHeights?[section][columnIndex])! - interItemSpacing.bottom + _sectionInset.bottom + + for idx in 0 ..< _columnHeights![section].count { + _columnHeights![section][idx] = top + } + } + } + + override func layoutAttributesForElements(in rect: CGRect) -> [UICollectionViewLayoutAttributes]? + { + var includedAttributes: [UICollectionViewLayoutAttributes] = [] + // Slow search for small batches + for attribute in _allAttributes { + if (attribute.frame.intersects(rect)) { + includedAttributes.append(attribute) + } + } + return includedAttributes + } + + override func layoutAttributesForItem(at indexPath: IndexPath) -> UICollectionViewLayoutAttributes? + { + guard indexPath.section < _itemAttributes.count, + indexPath.item < _itemAttributes[indexPath.section].count + else { + return nil + } + return _itemAttributes[indexPath.section][indexPath.item] + } + + override func layoutAttributesForSupplementaryView(ofKind elementKind: String, at indexPath: IndexPath) -> UICollectionViewLayoutAttributes? + { + if (elementKind == UICollectionElementKindSectionHeader) { + return _headerAttributes[indexPath.section] + } + return nil + } + + override func shouldInvalidateLayout(forBoundsChange newBounds: CGRect) -> Bool { + if (!(self.collectionView?.bounds.size.equalTo(newBounds.size))!) { + return true; + } + return false; + } + + func _widthForSection (section: Int) -> CGFloat + { + return self.collectionView!.bounds.size.width - _sectionInset.left - _sectionInset.right; + } + + func _columnWidthForSection(section: Int) -> CGFloat + { + return (self._widthForSection(section: section) - ((CGFloat(numberOfColumns - 1)) * columnSpacing)) / CGFloat(numberOfColumns) + } + + func _itemSizeAtIndexPath(indexPath: IndexPath) -> CGSize + { + var size = CGSize(width: self._columnWidthForSection(section: indexPath.section), height: 0) + let originalSize = self.delegate!.collectionView(self.collectionView!, layout:self, originalItemSizeAtIndexPath:indexPath) + if (originalSize.height > 0 && originalSize.width > 0) { + size.height = originalSize.height / originalSize.width * size.width + } + return size + } + + func _headerSizeForSection(section: Int) -> CGSize + { + return CGSize(width: self._widthForSection(section: section), height: headerHeight) + } + + override var collectionViewContentSize: CGSize + { + var height: CGFloat = 0 + if ((_columnHeights?.count)! > 0) { + if (_columnHeights?[(_columnHeights?.count)!-1].count)! > 0 { + height = (_columnHeights?[(_columnHeights?.count)!-1][0])! + } + } + return CGSize(width: self.collectionView!.bounds.size.width, height: height) + } + + func _tallestColumnIndexInSection(section: Int) -> Int + { + var index: Int = 0; + var tallestHeight: CGFloat = 0; + _ = _columnHeights?[section].enumerated().map { (idx,height) in + if (height > tallestHeight) { + index = idx; + tallestHeight = height + } + } + return index + } + + func _shortestColumnIndexInSection(section: Int) -> Int + { + var index: Int = 0; + var shortestHeight: CGFloat = CGFloat.greatestFiniteMagnitude + _ = _columnHeights?[section].enumerated().map { (idx,height) in + if (height < shortestHeight) { + index = idx; + shortestHeight = height + } + } + return index + } + +} + +class MosaicCollectionViewLayoutInspector: NSObject, ASCollectionViewLayoutInspecting +{ + func collectionView(_ collectionView: ASCollectionView, constrainedSizeForNodeAt indexPath: IndexPath) -> ASSizeRange { + let layout = collectionView.collectionViewLayout as! MosaicCollectionViewLayout + return ASSizeRangeMake(CGSize.zero, layout._itemSizeAtIndexPath(indexPath: indexPath)) + } + + func collectionView(_ collectionView: ASCollectionView, constrainedSizeForSupplementaryNodeOfKind: String, at atIndexPath: IndexPath) -> ASSizeRange + { + let layout = collectionView.collectionViewLayout as! MosaicCollectionViewLayout + return ASSizeRange.init(min: CGSize.zero, max: layout._headerSizeForSection(section: atIndexPath.section)) + } + + /** + * Asks the inspector for the number of supplementary sections in the collection view for the given kind. + */ + func collectionView(_ collectionView: ASCollectionView, numberOfSectionsForSupplementaryNodeOfKind kind: String) -> UInt { + if (kind == UICollectionElementKindSectionHeader) { + return UInt((collectionView.dataSource?.numberOfSections!(in: collectionView))!) + } else { + return 0 + } + } + + /** + * Asks the inspector for the number of supplementary views for the given kind in the specified section. + */ + func collectionView(_ collectionView: ASCollectionView, supplementaryNodesOfKind kind: String, inSection section: UInt) -> UInt { + if (kind == UICollectionElementKindSectionHeader) { + return 1 + } else { + return 0 + } + } + + func scrollableDirections() -> ASScrollDirection { + return ASScrollDirectionVerticalDirections; + } +} diff --git a/examples/CustomCollectionView-Swift/Sample/ViewController.swift b/examples/CustomCollectionView-Swift/Sample/ViewController.swift new file mode 100644 index 0000000000..c325f8bc27 --- /dev/null +++ b/examples/CustomCollectionView-Swift/Sample/ViewController.swift @@ -0,0 +1,101 @@ +// +// ViewController.swift +// Sample +// +// Created by Rajeev Gupta on 11/9/16. +// +// Copyright (c) 2014-present, Facebook, Inc. All rights reserved. +// This source code is licensed under the BSD-style license found in the +// LICENSE file in the root directory of this source tree. An additional grant +// of patent rights can be found in the PATENTS file in the same directory. +// +// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL +// FACEBOOK BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN +// ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN +// CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. +// + +import UIKit +import AsyncDisplayKit + +class ViewController: UIViewController, MosaicCollectionViewLayoutDelegate, ASCollectionDataSource, ASCollectionDelegate { + + var _sections = [[UIImage]]() + let _collectionNode: ASCollectionNode! + let _layoutInspector = MosaicCollectionViewLayoutInspector() + let kNumberOfImages: UInt = 14 + + required init?(coder aDecoder: NSCoder) { + let layout = MosaicCollectionViewLayout() + layout.numberOfColumns = 3; + layout.headerHeight = 44; + _collectionNode = ASCollectionNode(frame: CGRect.zero, collectionViewLayout: layout) + super.init(coder: aDecoder) + layout.delegate = self + + _sections.append([]); + var section = 0 + for idx in 0 ..< kNumberOfImages { + let name = String(format: "image_%d.jpg", idx) + _sections[section].append(UIImage(named: name)!) + if ((idx + 1) % 5 == 0 && idx < kNumberOfImages - 1) { + section += 1 + _sections.append([]) + } + } + + _collectionNode.dataSource = self; + _collectionNode.delegate = self; + _collectionNode.view.layoutInspector = _layoutInspector + _collectionNode.backgroundColor = UIColor.white + _collectionNode.view.isScrollEnabled = true + _collectionNode.registerSupplementaryNode(ofKind: UICollectionElementKindSectionHeader) + } + + deinit { + _collectionNode.dataSource = nil; + _collectionNode.delegate = nil; + } + + override func viewDidLoad() { + super.viewDidLoad() + self.view.addSubnode(_collectionNode!) + } + + override func viewWillLayoutSubviews() { + _collectionNode.frame = self.view.bounds; + } + + func collectionNode(_ collectionNode: ASCollectionNode, nodeForItemAt indexPath: IndexPath) -> ASCellNode { + let image = _sections[indexPath.section][indexPath.item] + return ImageCellNode(with: image) + } + + + func collectionNode(_ collectionNode: ASCollectionNode, nodeForSupplementaryElementOfKind kind: String, at indexPath: IndexPath) -> ASCellNode { + let textAttributes : NSDictionary = [ + NSFontAttributeName: UIFont.preferredFont(forTextStyle: UIFontTextStyle.headline), + NSForegroundColorAttributeName: UIColor.gray + ] + let textInsets = UIEdgeInsets(top: 11, left: 0, bottom: 11, right: 0) + let textCellNode = ASTextCellNode(attributes: textAttributes as! [AnyHashable : Any], insets: textInsets) + textCellNode.text = String(format: "Section %zd", indexPath.section + 1) + return textCellNode; + } + + + func numberOfSections(in collectionNode: ASCollectionNode) -> Int { + return _sections.count + } + + func collectionNode(_ collectionNode: ASCollectionNode, numberOfItemsInSection section: Int) -> Int { + return _sections[section].count + } + + internal func collectionView(_ collectionView: UICollectionView, layout: MosaicCollectionViewLayout, originalItemSizeAtIndexPath: IndexPath) -> CGSize { + return _sections[originalItemSizeAtIndexPath.section][originalItemSizeAtIndexPath.item].size + } +} + diff --git a/examples/Kittens/Sample/ViewController.m b/examples/Kittens/Sample/ViewController.m index 6b2e4b5058..63a662327a 100644 --- a/examples/Kittens/Sample/ViewController.m +++ b/examples/Kittens/Sample/ViewController.m @@ -57,8 +57,6 @@ static const NSInteger kMaxLitterSize = 100; // max number of kitten cell if (!(self = [super initWithNode:_tableNode])) return nil; - - _tableNode.view.separatorStyle = UITableViewCellSeparatorStyleNone; // KittenNode has its own separator // populate our "data source" with some random kittens _kittenDataSource = [self createLitterWithSize:kLitterSize]; @@ -76,6 +74,7 @@ static const NSInteger kMaxLitterSize = 100; // max number of kitten cell { [super viewDidLoad]; + _tableNode.view.separatorStyle = UITableViewCellSeparatorStyleNone; // KittenNode has its own separator [self.node addSubnode:_tableNode]; } diff --git a/examples/LayoutSpecExamples/Sample.xcworkspace/contents.xcworkspacedata b/examples/LayoutSpecExamples/Sample.xcworkspace/contents.xcworkspacedata deleted file mode 100644 index 7b5a2f3050..0000000000 --- a/examples/LayoutSpecExamples/Sample.xcworkspace/contents.xcworkspacedata +++ /dev/null @@ -1,10 +0,0 @@ - - - - - - - diff --git a/examples/LayoutSpecPlayground/Sample.xcworkspace/contents.xcworkspacedata b/examples/LayoutSpecPlayground/Sample.xcworkspace/contents.xcworkspacedata deleted file mode 100644 index 7b5a2f3050..0000000000 --- a/examples/LayoutSpecPlayground/Sample.xcworkspace/contents.xcworkspacedata +++ /dev/null @@ -1,10 +0,0 @@ - - - - - - - diff --git a/examples/Videos/Sample.xcworkspace/contents.xcworkspacedata b/examples/Videos/Sample.xcworkspace/contents.xcworkspacedata deleted file mode 100644 index 7b5a2f3050..0000000000 --- a/examples/Videos/Sample.xcworkspace/contents.xcworkspacedata +++ /dev/null @@ -1,10 +0,0 @@ - - - - - - - diff --git a/examples/Videos/Sample/ViewController.m b/examples/Videos/Sample/ViewController.m index b372b41786..6b2bbc33fb 100644 --- a/examples/Videos/Sample/ViewController.m +++ b/examples/Videos/Sample/ViewController.m @@ -69,6 +69,12 @@ return [ASAbsoluteLayoutSpec absoluteLayoutSpecWithChildren:@[guitarVideoNode, nicCageVideoNode, simonVideoNode, hlsVideoNode]]; }; + // Delay setting video asset for testing that the transition between the placeholder and setting/playing the asset is seamless. + dispatch_after(dispatch_time(DISPATCH_TIME_NOW, (int64_t)(3 * NSEC_PER_SEC)), dispatch_get_main_queue(), ^{ + hlsVideoNode.asset = [AVAsset assetWithURL:[NSURL URLWithString:@"http://devimages.apple.com/iphone/samples/bipbop/gear1/prog_index.m3u8"]]; + [hlsVideoNode play]; + }); + [self.view addSubnode:_rootNode]; } @@ -124,9 +130,8 @@ ASVideoNode *hlsVideoNode = [[ASVideoNode alloc] init]; hlsVideoNode.delegate = self; - hlsVideoNode.asset = [AVAsset assetWithURL:[NSURL URLWithString:@"http://devimages.apple.com/iphone/samples/bipbop/gear1/prog_index.m3u8"]]; hlsVideoNode.gravity = AVLayerVideoGravityResize; - hlsVideoNode.backgroundColor = [UIColor lightGrayColor]; + hlsVideoNode.backgroundColor = [UIColor redColor]; // Should not be seen after placeholder image is loaded hlsVideoNode.shouldAutorepeat = YES; hlsVideoNode.shouldAutoplay = YES; hlsVideoNode.muted = YES;