Asyncdisplaykit: Layout nodes using ASLayoutSpec

Created on 5 Oct 2015  Â·  30Comments  Â·  Source: facebookarchive/AsyncDisplayKit

I'm looking for a way to layout nodes without manually calculating and setting their frames.
screen shot 2015-10-05 at 9 55 17 pm

how can I layout this ASCellNode using ASLayoutSpec?
does ASLaoutSpec able to do this with these exact padding and margins?

I have come along a new powerful layout engine that is possible to layout views very easily and efficiently without using AutoLayout
Neon : UI layout framework
is it possible to use it along ASDisplayKit?

Most helpful comment

@hashemp206 you should be able to implement this with two ASStackLayoutSpecs.

  1. Create an ASDisplayNode subclass for the containing area, marked in blue in your diagram.
  2. Override the method -layoutSpecThatFits:
  3. Create an ASStackLayoutSpec that is horizontal (for the overall cell), and another one that is vertical (for the text labels)
  4. Set justifyContent on the horizontal stack to ASStackLayoutJustifyContentEnd, to have items push to the right.
  5. Use ASInsetLayoutSpec, if needed, to wrap the rounded rect node on the right hand side. If needed, you can also wrap the vertical layout spec using an inset.
  6. Lastly, the vertical layout spec should have each text node added to it. Set alignItems to ASStackLayoutAlignItemsEnd to make them right-aligned.

If you have any trouble, I can write a code sample for this. My apologies if I mis-typed or was unclear in any part of this description — let me know how it goes!

All 30 comments

@hashemp206 - Neon looks like a cool framework! However, it wouldn't integrate with ASDK without a fair amount of work. Also, it seems like many of the features are already supported natively by ASLayoutSpec. A small number of the features are things that would fit nicely within the mental model / implementation framework of ASLayoutSpec / ASLayoutable, so if there are any that jump out to you as particularly valuable let's get them written down as feature requests in the Issues section!

I'll write a note about how to lay out the example above as soon as I can (just in a meeting now). @rcancro or @nguyenhuy may get to it before me.

@hashemp206 you should be able to implement this with two ASStackLayoutSpecs.

  1. Create an ASDisplayNode subclass for the containing area, marked in blue in your diagram.
  2. Override the method -layoutSpecThatFits:
  3. Create an ASStackLayoutSpec that is horizontal (for the overall cell), and another one that is vertical (for the text labels)
  4. Set justifyContent on the horizontal stack to ASStackLayoutJustifyContentEnd, to have items push to the right.
  5. Use ASInsetLayoutSpec, if needed, to wrap the rounded rect node on the right hand side. If needed, you can also wrap the vertical layout spec using an inset.
  6. Lastly, the vertical layout spec should have each text node added to it. Set alignItems to ASStackLayoutAlignItemsEnd to make them right-aligned.

If you have any trouble, I can write a code sample for this. My apologies if I mis-typed or was unclear in any part of this description — let me know how it goes!

Thanks I tried but nothing appeared at all! just empty cell! I'm sure this is others question too
I have updated photo:
screen shot 2015-10-06 at 9 52 16 am

@hashemp206 could you share some of your code, then? Even just the -layoutSpecThatFits: implementation, or ideally the whole file (or project)? I'll give you free debugging help with high priority! :) I can guarantee you the problems can be solved, but it's impossible for me to fix them with the information that you've given (there are many types of implementation bugs that could cause stuff to not show up in an iOS app, even without ASDK :-P)

wow thanks for your kindness.

here is a sample project. cells should should stretch to screen width.
updated
https://www.dropbox.com/s/5p07a8iwzsgm4ln/layoutSpec.zip?dl=0

@hashemp206 no problem, I'm super glad you were able to share a test project! Now we can work quickly. I found these lines were missing from your -init, and adding them immediately improves the output:

  [self addSubnode:titleNode];
  [self addSubnode:hintNode];
  [self addSubnode:subHeadNode];
  [self addSubnode:subTitleNode];

I'm logging off for tonight, but do another iteration to see how far you can get now that the labels are showing up correctly. If you have any questions on justifyContent, alignItems, etc - Google those terms and you'll quickly find documentation related to CSS / "Flexbox", and it turns out the same layout rules & property naming scheme is used with AsyncDisplayKit. Just check out ASStackLayoutable for some additional options to control the behavior of individual items in a stack, in addition to the options on the stack itself (ASStackLayoutSpec).

@hashemp206 Does any of the text nodes grow to multiple lines if the text assigned to it is too long?

no it doesn't. imageView still doesn't appear.
screen shot 2015-10-07 at 5 16 21 pm

what I have:
screen shot 2015-10-07 at 5 18 51 pm

still cell doesn't stretch enough to fill screen width. imageView doesn't appear at all

Yeah, the current layout spec needs some tweaks anyway. What I was wondering is your layout requirements-- i.e does any of the text nodes have multiple lines, or they always occupy 1 line.

Image node is actually there. Not sure why setting its placeholderColor in -init of the cell node doesn't take effect (while doing so in -layoutSpecThatFits does). I will take a look at this later on.

Regarding the cell width, if you want it to always fill the collection view's width, you need to provide a different constrained size, rather than using the default one defined here, by overriding the data source's -collectionView:constrainedSizeForNodeAtIndexPath:, like this.

If the text nodes always have the same height, I guess the easiest way to achieve your layout is to provide a fixed constrained height for each cell. Then the layout spec will deal with text alignments. Let's make those changes and we will iterate on top of that.

I'm proud of talking to you cooperative friends.
your hint helped and cell now stretches to fit screen width.
no, none of them are multiple lines but it will be good if you tell us.
I have set titleNode.flexShrink = YES; but title node goes out of cell bounds.
screen shot 2015-10-07 at 8 54 03 pm

Your labels are inside a vertical stack, so the stack dimension is vertical and enabling flexShrink to the titleNode means it can shrink vertically. If you want all text nodes to wrap their contents, enable flexShrink and flexGrow on the container object that is directly added to the horizontal stack instead. In this case, it is labelsStackInset. Enabling flexShrink and flexGrow to in labelsStackInset means it can shrink and/or grow to fit whatever horizontal space available (--i.e the remaining width of the cell after reserving enough space for the image and horizontal margins). Now that the vertical stack has a fixed width (and unconstrained height), text nodes inside it are forced to use that width and grow vertically if needed.

wow thanks it worked. so what about imageViewNode? it doesn't appear, even it has some reserved space!

Rather than setting the placeholderColor in -init, I guess a workaround would be to do it somewhere else, say -layoutSpecThatFits:. I think it's a bug of the framework and will take a look later on.

just use .backgroundColor if you want a rectangular, colored placeholder before the image loads. I'll need to check out the implementation of .placeholderColor, but it is not very well implemented...

yes, it seems assigning color doesn't work, but setting image works. thanks. I will check out then if no more question I will close the issue.

If you could post the latest update to your test project that demonstrates any aspect that isn’t working as you desire, I’m happy to take a look! If it is a framework bug, I’d really like to fix it. If it’s a small bug in your code, I think it’s a valuable opportunity to point it out so you know what went wrong!

On Oct 7, 2015, at 12:34 PM, hashemp206 [email protected] wrote:

yes, it seems assigning color doesn't work, but setting image works. thanks. I will check out then if no more question I will close the issue.

—
Reply to this email directly or view it on GitHub https://github.com/facebook/AsyncDisplayKit/issues/709#issuecomment-146304486.

Here is what I have:
screen shot 2015-10-09 at 9 37 41 am

problem is _title_ and _subtitle_ nodes are not aligned to the top and bottom of image!
what I'm missing?

- (ASLayoutSpec *)layoutSpecThatFits:(ASSizeRange)constrainedSize
{
    //space between right edge of labels stack and imageNode
    CGFloat labelsRightEdgeMargine = 12;
    //space between left Edge of labels stack and cell left edge
    CGFloat labelsLeftEdgeMargin = 9;
    CGFloat imageRightMargin = 12;
    CGFloat imageTopMargin = 15;
    CGFloat imageBottomMargin = 15;
    CGFloat hintRightEdgeMargin = 4;

    //creating a horizontal stack for hint and title label
    ASStackLayoutSpec *horizontalHintTitleStack = [ASStackLayoutSpec stackLayoutSpecWithDirection:ASStackLayoutDirectionHorizontal spacing:hintRightEdgeMargin justifyContent:ASStackLayoutJustifyContentEnd alignItems:ASStackLayoutAlignItemsCenter children:@[hintNode, titleNode]];
    // creating a vertical stack for labels. labelStack should align top and bottom of imageNode and subHeadNode should be align to the center of imageNode
    ASStackLayoutSpec *verticalLabelsStack = [ASStackLayoutSpec stackLayoutSpecWithDirection:ASStackLayoutDirectionVertical spacing:0 justifyContent:ASStackLayoutJustifyContentEnd alignItems:ASStackLayoutAlignItemsEnd children:@[horizontalHintTitleStack, subHeadNode, subTitleNode]];

    ASInsetLayoutSpec *labelsStackInset = [ASInsetLayoutSpec insetLayoutSpecWithInsets:UIEdgeInsetsMake(0, 0, 0, labelsRightEdgeMargine) child:verticalLabelsStack];
    labelsStackInset.flexShrink = YES;

    // I have added inset to labelsStack so I set spacing to 0
    ASStackLayoutSpec *horizontalCellSpec = [ASStackLayoutSpec stackLayoutSpecWithDirection:ASStackLayoutDirectionHorizontal spacing:0 justifyContent:ASStackLayoutJustifyContentEnd alignItems:ASStackLayoutAlignItemsCenter children:@[labelsStackInset, imageNode]];
    ASInsetLayoutSpec *cellStackInset = [ASInsetLayoutSpec insetLayoutSpecWithInsets:UIEdgeInsetsMake(imageTopMargin, labelsLeftEdgeMargin, imageBottomMargin, imageRightMargin) child:horizontalCellSpec];


       return cellStackInset;
}

Your current layout means the labels stack is flexible and wraps its content. Its height doesn't depend on the image height. In many cases, it's actually good because the layout can grow to display all the texts (for example long texts and/or big fonts) and is still vertically aligned to the image node (center, top or bottom alignment can be easily set to the horizontal stack, using the appropriate align items value).

And that's why I asked you about flexibility of the labels. If you want all the constraints (tittle: top alignment, subhead: center alignment and subtitle: bottom alignment), then you can't have a flexible layout because the vertical stack needs to have the same size as the image. One way to achieve this layout is to have a fixed constrained height for every cell (which is a magic number derived from the requirements) and force nodes to fill it. The code would look like this:

- (ASSizeRange)collectionView:(ASCollectionView *)collectionView constrainedSizeForNodeAtIndexPath:(NSIndexPath *)indexPath
{
  CGFloat width = collectionView.bounds.size.width;
  return ASSizeRangeMake(CGSizeMake(width, 98), CGSizeMake(width, 98));
}

- (ASLayoutSpec *)layoutSpecThatFits:(ASSizeRange)constrainedSize
{
  CGFloat labelsRightEdgeMargin = 12;
  CGFloat labelsLeftEdgeMargin = 9;
  CGFloat imageRightMargin = 12;
  CGFloat imageTopMargin = 15;
  CGFloat imageBottomMargin = 15;
  CGFloat hintRightEdgeMargin = 4;

  for (ASTextNode *node in @[hintNode, titleNode, subTitleNode, subHeadNode]) {
    node.backgroundColor = [UIColor redColor]; // For visual testing
    node.maximumLineCount = 1;
  }

  ASStackLayoutSpec *horizontalHintTitleStack = [ASStackLayoutSpec stackLayoutSpecWithDirection:ASStackLayoutDirectionHorizontal
                                                                                        spacing:hintRightEdgeMargin
                                                                                 justifyContent:ASStackLayoutJustifyContentEnd
                                                                                     alignItems:ASStackLayoutAlignItemsCenter
                                                                                       children:@[hintNode, titleNode]];

  // Flexible vertical  spacer between text nodes
  ASLayoutSpec *verticalSpacer = [ASLayoutSpec new];
  verticalSpacer.flexGrow = YES;
  verticalSpacer.flexShrink = YES;

  ASStackLayoutSpec *verticalLabelsStack = [ASStackLayoutSpec stackLayoutSpecWithDirection:ASStackLayoutDirectionVertical
                                                                                   spacing:0
                                                                            justifyContent:ASStackLayoutJustifyContentEnd
                                                                                alignItems:ASStackLayoutAlignItemsEnd
                                                                                  children:@[horizontalHintTitleStack, verticalSpacer, subHeadNode, verticalSpacer, subTitleNode]];
  verticalLabelsStack.flexGrow = YES;
  verticalLabelsStack.flexShrink = YES;

  ASRatioLayoutSpec *squareImageSpec = [ASRatioLayoutSpec ratioLayoutSpecWithRatio:1 child:imageNode];

  ASStackLayoutSpec *horizontalCellSpec = [ASStackLayoutSpec stackLayoutSpecWithDirection:ASStackLayoutDirectionHorizontal
                                                                                  spacing:labelsRightEdgeMargin
                                                                           justifyContent:ASStackLayoutJustifyContentCenter
                                                                               alignItems:ASStackLayoutAlignItemsStretch // Force children to expand and fill available height
                                                                                 children:@[verticalLabelsStack, squareImageSpec]];

  ASInsetLayoutSpec *cellStackInset = [ASInsetLayoutSpec insetLayoutSpecWithInsets:UIEdgeInsetsMake(imageTopMargin, labelsLeftEdgeMargin, imageBottomMargin, imageRightMargin)
                                                                             child:horizontalCellSpec];
  return cellStackInset;
}

Note:

  • If title is long enough, hint can be pushed out of the screen.
  • Image node doesn't have a preferred size because its size is computed from the constrained height, top and bottom margins.
  • A flexible spacer is used between labels to distribute any negative violations in the vertical stack. This is a workaround for the lack of "space between" and "space around" options in ASStackLayoutJustifyContent. Implementing them should not be so difficult though (and the code can be contributed back to ComponentKit).

you are a great man. worked like a charm!
why still title pushed out from screen while you set it flexible shrink?
I suggest reference this issue in the api guide, the others can benefit from it.

why still title pushed out from screen while you set it flexible shrink?

Great question :) Currently, as long as one of the two labels is so long, the other will be pushed out. Enabling flexShrink to both labels doesn't fix this because, with the current implementation, the violation on stack dimension is distributed evenly to both of them. For example, the title label is 500 points long, the hint label is 100 points long and the constrained width is 300 points. The violation will be 300 points and each label will need to shrink 150 points. So, the hint label will shrink to 0! Worse yet, the title label will shrink to 350 points and it will still be big enough to cause a violation of 50.

So that's the current implementation. I'm not sure if the CSS flexbox spec solves discusses this case. Maybe there is a way to un-uniformly distribute a violation to among stack's flexible children. If there is then we may want to support that as well.

One way to fix this is to somehow tell the 2 labels that: you are both flexible, but only if the other doesn't push you out of the screen. The result is that both labels will be shown, but I haven't figured out a way to describe this constraint.

Another easier workaround would be to manually limit the length of the strings set to those labels.

I suggest reference this issue in the api guide, the others can benefit from it.

Yeah, many folks have been asking for a guide. I'm sure we have good materials scattered over many issues and PRs. We really need someone who can actively push the guide forward :(

@appleguy hi, i dont think implementing Neon would be such a pain. I think I just did it. Look at PR: https://github.com/mamaral/Neon/pull/9

I did not use Neon or ASDK so much, but I think thats just it since Neon is manipulating frames.

@zdenektopic thanks for taking a look! I believe you've bridged the main thread layout capability, but I don't see how it will participate in the asynchronous layout functionality that is driven by ASTableView / ASCollectionView. This would pretty seriously affect performance.

I am sorry. I thought ASDKs ASDisplayNode had method that executes subnodes
layout in the background thread so there I thought I would use Neon.

po 12. 10. 2015 v 18:37 odesílatel appleguy [email protected]
napsal:

@zdenektopic https://github.com/zdenektopic thanks for taking a look! I
believe you've bridged the main thread layout capability, but I don't see
how it will participate in the asynchronous layout functionality that is
driven by ASTableView / ASCollectionView. This would pretty seriously
affect performance.

—
Reply to this email directly or view it on GitHub
https://github.com/facebook/AsyncDisplayKit/issues/709#issuecomment-147455035
.

@zdenektopic no worries, it's an interesting experiment! ASDK's asynchronous layout is driven by a method called -measureWithSizeRange.

So positioning the nodes happens on the main thread then?

@zdenektopic hmm, no, not sure where you got that sense. If you use ASTableView or ASCollectionView, the measurement is automatically done in the background — and is in fact parallelized across each cell, doing layout independently across multiple cores. Check out ASDataController for some of the code (not important for clients of the framework, just implementation details).

You can call -measureWithSizeRange: yourself if you are not using those two classes. If you do it yourself, it's best to dispatch it to a background thread and then jump back to the main thread when done, to install the view objects. It really depends on what you need, though. If you are tapping a button and it's supposed to immediately show an object, then calling -measure... on the main thread is probably the right architecture (that way the button stays highlighted and nothing else happens until the user's action is serviced).

Hey appleguy could you help me out with a layout? I'm really struggling with it. If I could do multiple overlay's it would be easy but I think I have to combine multiple stacks and it makes it difficult to get all the items pushed to the edges of the screens properly. Please let me know if you can help out with it. I already have divided a lot of the parts into stacks.

I think flexshrink and flexgrow just solved my problem lol

@aliasnoa haha, awesome! Definitely come join us on Slack. If you draw a layout on a napkin or paper and take a photo / share it, we can help you with the layout specs.

I have a lot of the nodes in static specs because they won't seem to show up when I use insets. Not sure if this hurts performance but I just couldn't seem to get them working with inset spec. I will get slack I have not used it before downloading it now!

Was this page helpful?
0 / 5 - 0 ratings