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.mm b/AsyncDisplayKit/ASCellNode.mm index cad63a8230..7ddd50cab7 100644 --- a/AsyncDisplayKit/ASCellNode.mm +++ b/AsyncDisplayKit/ASCellNode.mm @@ -58,6 +58,7 @@ static NSMutableSet *__cellClassesForVisibilityNotifications = nil; // See +init // Use UITableViewCell defaults _selectionStyle = UITableViewCellSelectionStyleDefault; self.clipsToBounds = YES; + return self; } @@ -117,15 +118,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 diff --git a/AsyncDisplayKit/ASDisplayNode.h b/AsyncDisplayKit/ASDisplayNode.h index dddc908239..18751b1120 100644 --- a/AsyncDisplayKit/ASDisplayNode.h +++ b/AsyncDisplayKit/ASDisplayNode.h @@ -637,11 +637,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; @@ -796,7 +796,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 231d7e7acb..1bf8f53a15 100644 --- a/AsyncDisplayKit/ASDisplayNode.mm +++ b/AsyncDisplayKit/ASDisplayNode.mm @@ -315,6 +315,7 @@ static ASDisplayNodeMethodOverrides GetASDisplayNodeMethodOverrides(Class c) _environmentState = ASEnvironmentStateMakeDefault(); _calculatedDisplayNodeLayout = std::make_shared(); + _pendingDisplayNodeLayout = nullptr; _defaultLayoutTransitionDuration = 0.2; _defaultLayoutTransitionDelay = 0.0; @@ -703,6 +704,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 @@ -714,75 +779,27 @@ static ASDisplayNodeMethodOverrides GetASDisplayNodeMethodOverrides(Class c) - (ASLayout *)layoutThatFits:(ASSizeRange)constrainedSize parentSize:(CGSize)parentSize { ASDN::MutexLocker l(__instanceLock__); - - 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 multiple layout transitions are in progress it can happen that an invalid one is still trying to do a measurement + // before it get's cancelled. In this case we should not touch any layout and return a no op layout + if ([self _isLayoutTransitionInvalid]) { + return [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; - } + 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}]; } - // 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 @@ -815,14 +832,7 @@ 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; - } - - [self invalidateCalculatedLayout]; - [self transitionLayoutWithSizeRange:_calculatedDisplayNodeLayout->constrainedSize + [self transitionLayoutWithSizeRange:[self _locked_constrainedSizeForLayoutPass] animated:animated shouldMeasureAsync:shouldMeasureAsync measurementCompletion:completion]; @@ -834,9 +844,11 @@ 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) { + + // 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; } @@ -845,20 +857,26 @@ static ASDisplayNodeMethodOverrides GetASDisplayNodeMethodOverrides(Class c) ASDisplayNodeAssert(ASHierarchyStateIncludesLayoutPending(_hierarchyState) == NO, @"Can't start a transition when one of the supernodes is performing one."); } + // Invalidate the current layout to be able to measure a new layout based onthe given constrained size + [self setNeedsLayout]; + + // 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__); @@ -891,7 +909,7 @@ static ASDisplayNodeMethodOverrides GetASDisplayNodeMethodOverrides(Class c) return; } - // Update display node layout + // Update calculated layout auto previousLayout = _calculatedDisplayNodeLayout; auto pendingLayout = std::make_shared( newLayout, @@ -930,6 +948,7 @@ static ASDisplayNodeMethodOverrides GetASDisplayNodeMethodOverrides(Class c) }); }; + // Start transition based on flag on current or background thread if (shouldMeasureAsync) { ASPerformBlockOnBackgroundThread(transitionBlock); } else { @@ -957,6 +976,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 { @@ -1387,51 +1418,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 @@ -1450,58 +1453,152 @@ 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; } + + // 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; - // Handle placeholder layer creation in case the size of the node changed after the initial placeholder layer - // was created - if ([self _shouldHavePlaceholderLayer]) { - [self _setupPlaceholderLayerIfNeeded]; - } - _placeholderLayer.frame = bounds; + [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 @@ -2591,12 +2688,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; } @@ -2630,15 +2733,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__); @@ -3680,7 +3774,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 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 bdc2180db1..2678a7e8a9 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/ASTableView.mm b/AsyncDisplayKit/ASTableView.mm index 10c4853f34..aca1855e7b 100644 --- a/AsyncDisplayKit/ASTableView.mm +++ b/AsyncDisplayKit/ASTableView.mm @@ -1523,7 +1523,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; 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/ASViewController.mm b/AsyncDisplayKit/ASViewController.mm index dfb41bfc7f..606f667c10 100644 --- a/AsyncDisplayKit/ASViewController.mm +++ b/AsyncDisplayKit/ASViewController.mm @@ -135,10 +135,11 @@ 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. + // A measure as well as 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 layoutThatFits:[self nodeConstrainedSize]]; + [_node.view layoutIfNeeded]; [_node recursivelyFetchData]; 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/ASDisplayNode+FrameworkPrivate.h b/AsyncDisplayKit/Private/ASDisplayNode+FrameworkPrivate.h index 620d976044..a7025a2db2 100644 --- a/AsyncDisplayKit/Private/ASDisplayNode+FrameworkPrivate.h +++ b/AsyncDisplayKit/Private/ASDisplayNode+FrameworkPrivate.h @@ -175,6 +175,20 @@ __unused static NSString * _Nonnull NSStringFromASHierarchyState(ASHierarchyStat */ - (BOOL)shouldScheduleDisplayWithNewInterfaceState:(ASInterfaceState)newInterfaceState; +/** + * @abstract Informs the root node that the intrinsic size of the receiver is no longer valid. + * + * @discussion The size of a root node is determined by each subnode. Calling invalidateSize will let the root node know + * that the intrinsic size of the receiver node is no longer valid and a resizing of the root node needs to happen. + */ +- (void)setNeedsLayoutFromAbove; + +/** + * @abstract Subclass hook for nodes that are acting as root nodes. This method is called if one of the subnodes + * size is invalidated and may need to result in a different size as the current calculated size. + */ +- (void)_locked_displayNodeDidInvalidateSizeNewSize:(CGSize)newSize; + @end @interface UIView (ASDisplayNodeInternal) diff --git a/AsyncDisplayKit/Private/ASDisplayNodeInternal.h b/AsyncDisplayKit/Private/ASDisplayNodeInternal.h index 8aa400cb54..67bb2cd35c 100644 --- a/AsyncDisplayKit/Private/ASDisplayNodeInternal.h +++ b/AsyncDisplayKit/Private/ASDisplayNodeInternal.h @@ -137,6 +137,7 @@ FOUNDATION_EXPORT NSString * const ASRenderingEngineDidDisplayNodesScheduledBefo int32_t _pendingTransitionID; ASLayoutTransition *_pendingLayoutTransition; std::shared_ptr _calculatedDisplayNodeLayout; + std::shared_ptr _pendingDisplayNodeLayout; ASDisplayNodeViewBlock _viewBlock; ASDisplayNodeLayerBlock _layerBlock; @@ -188,12 +189,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 +203,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 +239,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 f145abac7c..7c565f1cbe 100644 --- a/AsyncDisplayKit/Private/ASInternalHelpers.h +++ b/AsyncDisplayKit/Private/ASInternalHelpers.h @@ -31,8 +31,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 c9dae927ef..04c9972b62 100644 --- a/AsyncDisplayKit/Private/ASInternalHelpers.m +++ b/AsyncDisplayKit/Private/ASInternalHelpers.m @@ -95,12 +95,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/AsyncDisplayKitTests/ASDisplayNodeImplicitHierarchyTests.m b/AsyncDisplayKitTests/ASDisplayNodeImplicitHierarchyTests.m index 6623c55205..4208310282 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); @@ -170,10 +175,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 +192,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 +240,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 +257,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 126e5c3806..fcfa42c138 100644 --- a/AsyncDisplayKitTests/ASDisplayNodeTests.m +++ b/AsyncDisplayKitTests/ASDisplayNodeTests.m @@ -2166,8 +2166,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); @@ -2185,8 +2187,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/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/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/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