The simplest example I could come up with that shows a infinite layout loop in Texture
I found a layout loop in Texture dealing with nodes in a UINavigationItem's titleView. The steps are rougly:
- Create a ASDisplayNode with a subnode (I used a button) and set it as a VC's navigationItem's titleView.
- Push another VC
- Cause the subnode in the titleView in the first VC to relayout.
- Pop the 2nd VC
I have put up a PR to fix the loop here: TextureGroup/Texture#1434
I tried to create a unit test to reproduce this, but didn't have any luck. This is what I came up with if it is useful:
#import <XCTest/XCTest.h>
#import <AsyncDisplayKit/AsyncDisplayKit.h>
@interface ASDisplayNode(Private)
- (void)__enterHierarchy;
@end
@interface NavigationHeaderNode : ASDisplayNode
@property (nonatomic, strong, nullable) ASControlNode *button;
@end
@implementation NavigationHeaderNode
- (void)setButton:(ASControlNode *)button
{
[_button removeFromSupernode];
_button = button;
[self addSubnode:button];
}
- (ASLayoutSpec *)layoutSpecThatFits:(ASSizeRange)constrainedSize
{
NSArray *children = self.button ? @[self.button] : @[];
return [ASAbsoluteLayoutSpec absoluteLayoutSpecWithSizing:ASAbsoluteLayoutSpecSizingSizeToFit children:children];
}
@end
@interface SecondViewController : ASViewController
@end
@interface FirstViewController : ASViewController
@property (strong) NavigationHeaderNode *headerNode;
@property (strong) ASButtonNode *navButton1;
@end
@implementation FirstViewController
- (instancetype)init
{
ASButtonNode *button = [[ASButtonNode alloc] init];
[button setTitle:@"Push VC" withFont:[UIFont systemFontOfSize:23] withColor:[UIColor darkGrayColor] forState:UIControlStateNormal];
ASDisplayNode *node = [[ASDisplayNode alloc] init];
node.automaticallyManagesSubnodes = YES;
node.layoutSpecBlock = ^ASLayoutSpec * _Nonnull(__kindof ASDisplayNode * _Nonnull node, ASSizeRange constrainedSize) {
return [ASCenterLayoutSpec centerLayoutSpecWithCenteringOptions:ASCenterLayoutSpecCenteringXY sizingOptions:ASCenterLayoutSpecSizingOptionDefault child:button];
};
self = [self initWithNode:node];
if (self) {
_navButton1 = [[ASButtonNode alloc] init];
[_navButton1 setTitle:@"Hello" withFont:[UIFont systemFontOfSize:23] withColor:[UIColor darkGrayColor] forState:UIControlStateNormal];
[_navButton1 setTitle:@"Hello!!" withFont:[UIFont systemFontOfSize:23] withColor:[UIColor darkGrayColor] forState:UIControlStateSelected];
[button addTarget:self action:@selector(buttonTapped:) forControlEvents:ASControlNodeEventTouchUpInside];
}
return self;
}
- (void)viewDidLoad
{
[super viewDidLoad];
NavigationHeaderNode *headerNode = [[NavigationHeaderNode alloc] init];
headerNode.button = self.navButton1;
self.headerNode = headerNode;
self.navigationItem.hidesBackButton = YES;
self.navigationItem.titleView = headerNode.view;
self.navigationItem.titleView.frame = CGRectMake(0, 0, 375, 44);
}
- (void)buttonTapped:(id)sender
{
SecondViewController *secondVC = [[SecondViewController alloc] init];
[self.navigationController pushViewController:secondVC animated:YES];
}
@end
@implementation SecondViewController
- (instancetype)init
{
return [self initWithNode:[[ASDisplayNode alloc] init]];
}
- (void)viewDidLoad
{
[super viewDidLoad];
self.navigationItem.hidesBackButton = NO;
}
- (void)viewDidAppear:(BOOL)animated
{
FirstViewController *firstViewController = (FirstViewController *)[self.navigationController.viewControllers firstObject];
ASControlNode *button = firstViewController.headerNode.button;
button.selected = !button.selected;
}
@end
@interface ASDisplayNodeRequestLayoutFromAboveTests : XCTestCase
@end
@implementation ASDisplayNodeRequestLayoutFromAboveTests
- (void)testRequestLayoutFromAbove
{
FirstViewController *firstViewController = [[FirstViewController alloc] init];
UINavigationController *navController = [[UINavigationController alloc] initWithRootViewController:firstViewController];
[firstViewController.view setNeedsLayout];
[firstViewController.view layoutIfNeeded];
UIWindow *window = [[UIWindow alloc] initWithFrame:[[UIScreen mainScreen] bounds]];
[window makeKeyAndVisible];
window.rootViewController = navController;
SecondViewController *secondViewController = [[SecondViewController alloc] init];
[navController pushViewController:secondViewController animated:NO];
[secondViewController.view setNeedsLayout];
[secondViewController.view layoutIfNeeded];
ASControlNode *button = firstViewController.headerNode.button;
button.selected = !button.selected;
[navController popViewControllerAnimated:NO];
[firstViewController.headerNode layoutIfNeeded];
firstViewController.headerNode.frame = CGRectMake(0, 0, 375, 0);
firstViewController.headerNode.button.frame = CGRectMake(0, 0, 51.5, 27.5);
[firstViewController.headerNode.button __enterHierarchy];
}