Asyncdisplaykit: SocialAppLayout tweaking

Created on 27 Jan 2016  路  11Comments  路  Source: facebookarchive/AsyncDisplayKit

You guys have been doing extraordinary work with this library. I have a question which I hope someone could point me in the right direction.

I have an ASTableView similar to SocialAppLayout using layoutSpecThatFits. I would like to make the table look a little bit nicer by making it look like the Facebook timeline using a "card" type of presentation for the cell. On a tableView with autolayout this is easy - add a UIView to the cell's contentView and add all ui components to this UIView, set the view's background color to white and the cell's background color to lightGray. Can even add a slight shadow for a raised look. Of course, without the performance of this library.

I am scratching my head on how to achieve this look with ASTableView using layoutSpecThatFits

Any help would be appreciated. Thank you..

Most helpful comment

I've managed this by altering the frame, but I suspect there must be a better way todo this.

Here's my code if it helps others, It would be great if there was more documentation on this.

This video also helped me, def worth a watch if your new to AsyncDisplayKit, these videos should be on the docs site.

//
//  CardCellNode.swift
//

import Foundation
import AsyncDisplayKit

class CardCellNode: ASCellNode {

    var cardBgNode:ASDisplayNode! // This is our white background card
    var titleNode:ASTextNode!     // The title displayed inside the card


    // Override init as we want to add a convience method
    override init() {
        super.init()

        // Give the cell (self) a bg colour of gray so we can see it
        self.backgroundColor = UIColor.lightGrayColor()

        // Card BG Node (White BG)
        self.cardBgNode = ASDisplayNode()
        self.cardBgNode.backgroundColor = UIColor.whiteColor()
        self.addSubnode(cardBgNode)

        // Title node (Yellow BG)
        self.titleNode = ASTextNode()
        self.titleNode.attributedString = NSAttributedString(string: "Title!")
        self.titleNode.backgroundColor = UIColor.yellowColor()
        self.addSubnode(titleNode)
    }

    override func layout() {
        super.layout()
        // There must be a better way to do this via Layouts?
        // Feels "Dirty"
        self.cardBgNode.frame = CGRectMake(10, 10, self.calculatedSize.width-20, self.calculatedSize.height-20)
    }

    // View: https://github.com/facebook/AsyncDisplayKit/issues/1254
    // View: https://github.com/facebook/AsyncDisplayKit/issues/1123
    override func layoutSpecThatFits(constrainedSize: ASSizeRange) -> ASLayoutSpec {
        let innerCardStack = ASStackLayoutSpec(direction: .Vertical, spacing: 8.0, justifyContent: ASStackLayoutJustifyContent.Center, alignItems: ASStackLayoutAlignItems.Center, children: [self.titleNode])

        // How can I center the card in the cell with some space on all edges
        let insetSpec = ASInsetLayoutSpec(insets: UIEdgeInsetsMake(10, 10, 10, 10), child: innerCardStack)

        let mainVerticalStack = ASStackLayoutSpec(direction: .Vertical, spacing: 8.0, justifyContent: ASStackLayoutJustifyContent.Center, alignItems: ASStackLayoutAlignItems.Center, children: [insetSpec])

        print(mainVerticalStack.asciiArtString())

        return mainVerticalStack
    }

} // End Class

All 11 comments

Hey @robmontesinos It's basically the same, you would create a new class that subclasses ASCellNode and apply the properties you mentioned to that node, then return this in your - (ASCellNode *)nodeForRowAtIndexPath:(NSIndexPath *)indexPath; call. That should do it.

In terms of using layoutSpecThatFits, you'll probably want to use ASStackLayoutSpec which lets you stack other nodes vertically & horizontally. Using a number of ASStackLayoutSpec's will let you achieve the look you are going for.

Thank you @aaronschubert0 - I was able to successfully modify the PostNode in the SocialAppLayout to simulate a "cardView" for each cellNode. I added 5 pixel height bands at top and bottom, and 10 pixel wide bands for left and right - I gave the four bands a lightGray background. I then moved the divider displayNode down to make it look like a shadow. I set the frames for the bands in "layout"
simulator screen shot jan 28 2016 11 24 53 am

I'm not all that pleased with the shadow as is and am curious, if I were to create a BezierPath to simulate a cardView shadow would it hurt scrolling performance? I can post the modified code if you want to take a look at it. Thank you

@robmontesinos Can you show your code for how you achieved that?

Basically, I want to do the same thing, I want to have a background (which is the cell - dark gray in your picture), and then a card on top of that.

I've managed this by altering the frame, but I suspect there must be a better way todo this.

Here's my code if it helps others, It would be great if there was more documentation on this.

This video also helped me, def worth a watch if your new to AsyncDisplayKit, these videos should be on the docs site.

//
//  CardCellNode.swift
//

import Foundation
import AsyncDisplayKit

class CardCellNode: ASCellNode {

    var cardBgNode:ASDisplayNode! // This is our white background card
    var titleNode:ASTextNode!     // The title displayed inside the card


    // Override init as we want to add a convience method
    override init() {
        super.init()

        // Give the cell (self) a bg colour of gray so we can see it
        self.backgroundColor = UIColor.lightGrayColor()

        // Card BG Node (White BG)
        self.cardBgNode = ASDisplayNode()
        self.cardBgNode.backgroundColor = UIColor.whiteColor()
        self.addSubnode(cardBgNode)

        // Title node (Yellow BG)
        self.titleNode = ASTextNode()
        self.titleNode.attributedString = NSAttributedString(string: "Title!")
        self.titleNode.backgroundColor = UIColor.yellowColor()
        self.addSubnode(titleNode)
    }

    override func layout() {
        super.layout()
        // There must be a better way to do this via Layouts?
        // Feels "Dirty"
        self.cardBgNode.frame = CGRectMake(10, 10, self.calculatedSize.width-20, self.calculatedSize.height-20)
    }

    // View: https://github.com/facebook/AsyncDisplayKit/issues/1254
    // View: https://github.com/facebook/AsyncDisplayKit/issues/1123
    override func layoutSpecThatFits(constrainedSize: ASSizeRange) -> ASLayoutSpec {
        let innerCardStack = ASStackLayoutSpec(direction: .Vertical, spacing: 8.0, justifyContent: ASStackLayoutJustifyContent.Center, alignItems: ASStackLayoutAlignItems.Center, children: [self.titleNode])

        // How can I center the card in the cell with some space on all edges
        let insetSpec = ASInsetLayoutSpec(insets: UIEdgeInsetsMake(10, 10, 10, 10), child: innerCardStack)

        let mainVerticalStack = ASStackLayoutSpec(direction: .Vertical, spacing: 8.0, justifyContent: ASStackLayoutJustifyContent.Center, alignItems: ASStackLayoutAlignItems.Center, children: [insetSpec])

        print(mainVerticalStack.asciiArtString())

        return mainVerticalStack
    }

} // End Class

Yes this is dirty, I have to now Inset my nodes in my layoutSpecThatFits to match (To make up the 10,10 we modify in the frame above), there must be a better way to do this, preferably not modifying the frame, but using the Layout methods provided by ADK.

Any ideas?

@lakhman nice work @robmontesinos trying to convert the "SocialAppLayout sample app" into swift. experiencing bugs...zzz @robmontesinos did you modify the PostNode in swift? if so how? would really appreciate it if I could see some of your code, could save me a good chunk of time. thx fingers crossed!

@alexrmacleod Unfortunately I got rid of that project a while back which was in Objective C. I'll post some code for you from my latest project using the same approach with a more complex hierarchy dealing with YouTube search results - I hope you can figure it out. Let me know if any problems reading my goobly-gook

- (void)buildUI {
// there's a bunch of other stuff here - I just included the "cardView makers"
// FlatBlack and other Flat colors from Chameleon library
    _divider = [[ASDisplayNode alloc] init];
    _divider.layerBacked = YES;
    _divider.backgroundColor = FlatBlack;
    _divider.alpha = 0.5;

    // padding
    _topNode = [[ASDisplayNode alloc] init];
    _topNode.backgroundColor = FlatWhiteDark;
    _topNode.layerBacked = YES;

    _bottomNode = [[ASDisplayNode alloc] init];
    _bottomNode.backgroundColor = FlatWhiteDark;
    _bottomNode.layerBacked = YES;
}

- (ASLayoutSpec *)layoutSpecThatFits:(ASSizeRange)constrainedSize {
    self.thumbnailImageNode.preferredFrameSize = CGSizeMake(148.0, 84.0);
    self.youTubeButtonNode.preferredFrameSize = CGSizeMake(24.0, 24.0);
    self.descriptionTextNode.flexShrink = YES;

    ASLayoutSpec *textSpacer = [ASLayoutSpec new];
    textSpacer.flexBasis = ASRelativeDimensionMakeWithPoints(2.0);

    ASLayoutSpec *horizontalSpacer = [ASLayoutSpec new];
    horizontalSpacer.flexGrow = YES;

    ASStackLayoutSpec *buttonStack = [[ASStackLayoutSpec alloc] init];
    buttonStack.children = @[horizontalSpacer, self.youTubeButtonNode];
    buttonStack.direction = ASStackLayoutDirectionHorizontal;
    buttonStack.alignSelf = ASStackLayoutAlignSelfStretch;
    buttonStack.flexGrow = YES;

    ASStackLayoutSpec *rightStack = [[ASStackLayoutSpec alloc] init];
    rightStack.children = @[self.titleTextNode, buttonStack, textSpacer];
    rightStack.direction = ASStackLayoutDirectionVertical;
    rightStack.flexShrink = YES;

    ASStackLayoutSpec *topStack = [[ASStackLayoutSpec alloc] init];
    topStack.direction = ASStackLayoutDirectionHorizontal;
    topStack.children = @[textSpacer, self.thumbnailImageNode, textSpacer, rightStack, textSpacer];
    topStack.spacing = kOuterPadding;

    ASStackLayoutSpec *infoStack = [[ASStackLayoutSpec alloc] init];
    infoStack.direction = ASStackLayoutDirectionHorizontal;
    infoStack.children = @[textSpacer, self.infoTextNode];
    infoStack.flexShrink = YES;
    infoStack.spacing = kOuterPadding;

    ASStackLayoutSpec *bottomStack = [[ASStackLayoutSpec alloc] init];
    bottomStack.direction = ASStackLayoutDirectionHorizontal;
    bottomStack.children = @[textSpacer, self.descriptionTextNode, textSpacer];
    bottomStack.spacing = kOuterPadding;

    NSArray *contentChildren = @[topStack, infoStack, bottomStack];

    ASStackLayoutSpec *contentSpec = [ASStackLayoutSpec stackLayoutSpecWithDirection:ASStackLayoutDirectionVertical spacing:paddingTop justifyContent:ASStackLayoutJustifyContentStart alignItems:ASStackLayoutAlignItemsStart children:contentChildren];

    return [ASInsetLayoutSpec insetLayoutSpecWithInsets:UIEdgeInsetsMake(paddingTop * 2, paddingTop, paddingTop * 2, paddingTop) child:contentSpec];
}

- (void)layout {
    [super layout];

    CGFloat pixelHeight = 3.0f / [[UIScreen mainScreen] scale];
    _divider.frame = CGRectMake(0.0f, self.calculatedSize.height - 1.0, self.calculatedSize.width, pixelHeight);

    _topNode.frame = CGRectMake(0.0, 0.0, self.calculatedSize.width, paddingTop);
    _bottomNode.frame = CGRectMake(0.0, self.calculatedSize.height - paddingTop, self.calculatedSize.width, paddingTop);

    // calculate duration label
    CGFloat xPos = self.thumbnailImageNode.frame.size.width + self.thumbnailImageNode.frame.origin.x - durationTextNodeWidth - paddingTop;
    CGFloat yPos = self.thumbnailImageNode.frame.size.height + self.thumbnailImageNode.frame.origin.y - durationTextNodeHeight - paddingTop;

    self.durationTextNode.frame = CGRectMake(xPos, yPos, durationTextNodeWidth, durationTextNodeHeight);
    self.durationTextNode.alignSelf = ASStackLayoutAlignItemsCenter;
}

@robmontesinos thanks for the fast response. much appreciated! will message back here if I achieve any meaningful progress. #voteforbernie

SocialAppLayoutSwift.zip

@appleguy @yury @lakhman @aaronschubert0 @robmontesinos Attached is a working swift version of the SocialAppLayout. Add it to the -> https://github.com/facebook/AsyncDisplayKit/tree/master/examples for all the swift coders out there. @appleguy might need to look at the didselectrowatindexpath on the main viewcontroller something is funky when selecting the cell.

Questions:
Why is the SocialAppLayout built with UIViewcontroller and ASTableView unlike the Swift [Swift] example project which uses ASViewController and ASTableNode?

  • is it possible to achieve this social layout with ASTableNode?
  • should I try convert this SocialAppLayout swift version to use ASTableNode or would that be a waste of time?

Finally I noticed the SocialAppLayout uses ASControlNode for the buttons... why is that.... you guys built ASButtonNode? what are the benefits of using one over the other?

@alexrmacleod: Thanks for including the Swift example! We get quite a few asks for Swift examples and this could be our first ASDK Swift example project.

UIViewController + ASTableView is older ASDK code (many of our example projects our outdated). The newer style is ASViewController + ASTableNode as you mention.

Benefits of using ASViewController over UIViewController:

  • One of the main benefits to using an ASViewController is to save memory. An ASViewController that goes off screen will automatically reduce the size of the fetch data and display ranges of any of its children. This is key for memory management in large applications.
  • support the new "Visibility" feature. From the 1.9.8 Release Notes: _ASNavigationController and ASTabBarController, which implement a new protocol ASVisibility. These classes can be used even without ASDisplayNodes, making them suitable base classes for your inheritance hierarchy. For any child view controllers that are ASViewControllers, these classes know the exact number of user taps it would take to make the view controller visible (0 if currently visible)._
  • More features will be added over time, so it is a good idea to base your view controllers off of this class.

Your other questions:

  • Yes, it is possible to achieve the same layout with ASTableNode and this is now the recommend way of doing it rather than using the ASTableView.
  • Yes! If you would be willing to convert this Swift version to use ASTableNode, that would be even better to use in our examples directory. I'll wait to upload it until I hear back as to whether or not you would be willing to convert it.

ASControlNode is the ASDK equivalent of a UIControl. All ASControlNodes have the -addTarget:action:forControlEvents: and the hitTestSlop property (to easily add UIEdgeInsets to make your tappable area larger). If you just need a tappable area (with no image or text or state changing), this is a quick way to do it.

http://asyncdisplaykit.org/docs/control-node.html
http://asyncdisplaykit.org/docs/hit-test-slop.html
http://asyncdisplaykit.org/docs/debug-tool-hit-test-visualization.html

ASButtonNode is a subclass of ASControlNode and supports state-changing background images, state-changing titles, contentEdgeInsets and more. NOTE: the selected property logic of the ASButtonNode should be handled by the developer. Tapping the ASButtonNode does not automatically enable selected.

@alexrmacleod: I'm going to close this issue, but feel free to continue to ask questions here (or on Slack). Also, let me know if you'd be willing to update your Swift example for us before we upload it to examples.

Was this page helpful?
0 / 5 - 0 ratings