diff --git a/CHANGELOG.md b/CHANGELOG.md index d326bdc5b..95c1e49b2 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,6 +1,7 @@ ## master * Add your own contributions to the next release on the line below this with your name. +- [ASDisplayNode+Layout] Add check for orphaned nodes after layout transition to clean up. #336. [Scott Goodson](https://github.com/appleguy) - Fixed an issue where GIFs with placeholders never had their placeholders uncover the GIF. [Garrett Moon](https://github.com/garrettmoon) - [Yoga] Implement ASYogaLayoutSpec, a simplified integration strategy for Yoga-powered layout calculation. [Scott Goodson](https://github.com/appleguy) - Fixed an issue where calls to setNeedsDisplay and setNeedsLayout would stop working on loaded nodes. [Garrett Moon](https://github.com/garrettmoon) diff --git a/Source/ASDisplayNode+Layout.mm b/Source/ASDisplayNode+Layout.mm index 6949199c0..e842973b7 100644 --- a/Source/ASDisplayNode+Layout.mm +++ b/Source/ASDisplayNode+Layout.mm @@ -817,11 +817,54 @@ - (void)_completeLayoutTransition:(ASLayoutTransition *)layoutTransition } } +- (void)_assertSubnodeState +{ + // Verify that any orphaned nodes are removed. + // This can occur in rare cases if main thread layout is flushed while a background layout is calculating. + + if (self.automaticallyManagesSubnodes == NO) { + return; + } + + NSArray *subnodes = [self subnodes]; + NSArray *sublayouts = _calculatedDisplayNodeLayout->layout.sublayouts; + + auto currentSubnodes = [[NSHashTable alloc] initWithOptions:NSHashTableObjectPointerPersonality + capacity:subnodes.count]; + auto layoutSubnodes = [[NSHashTable alloc] initWithOptions:NSHashTableObjectPointerPersonality + capacity:sublayouts.count];; + for (ASDisplayNode *subnode in subnodes) { + [currentSubnodes addObject:subnode]; + } + + for (ASLayout *sublayout in sublayouts) { + id layoutElement = sublayout.layoutElement; + ASDisplayNodeAssert([layoutElement isKindOfClass:[ASDisplayNode class]], + @"All calculatedLayouts should be flattened and only contain nodes!"); + [layoutSubnodes addObject:(ASDisplayNode *)layoutElement]; + } + + // Verify that all subnodes that occur in the current ASLayout tree are present in .subnodes array. + if ([layoutSubnodes isSubsetOfHashTable:currentSubnodes] == NO) { + // Note: This should be converted to an assertion after confirming it is rare. + NSLog(@"Warning: node's layout includes subnodes that have not been added: node = %@, subnodes = %@, subnodes in layout = %@", self, currentSubnodes, layoutSubnodes); + } + + // Verify that everything in the .subnodes array is present in the ASLayout tree (and correct it if not). + [currentSubnodes minusHashTable:layoutSubnodes]; + for (ASDisplayNode *orphanedSubnode in currentSubnodes) { + NSLog(@"Automatically removing orphaned subnode %@, from parent %@", orphanedSubnode, self); + [orphanedSubnode removeFromSupernode]; + } +} + - (void)_pendingLayoutTransitionDidComplete { + [self _assertSubnodeState]; + // Subclass hook [self calculatedLayoutDidChange]; - + // Grab lock after calling out to subclass ASDN::MutexLocker l(__instanceLock__);