AsyncDisplayKit Tutorial: Node Hierarchies

This intermediate level AsyncDisplayKit tutorial will explain how you can make full use of the framework by exploring AsyncDisplayKit node hierarchies. By René Cacheaux.

Leave a rating/review
Save for later
Share
You are currently viewing page 2 of 3 of this article. Click here to view the first page.

Measuring a Node’s Size

Now that you understand the method to the madness here, it’s time to apply it and measure some node sizes for yourself.

Open CardNode.swift and replace the class there with the following:

class CardNode: ASDisplayNode {

  override func calculateSizeThatFits(constrainedSize: CGSize) -> CGSize {
    return CGSize(width: constrainedSize.width * 0.2, height: constrainedSize.height * 0.2)
  }

}

For now, this method returns a size that is 20 percent of the constrained size provided, hence, it takes up just 4 percent of the available area.

Open ViewController.swift, delete the viewDidLoad() implementation, and implement the following createCardNode(containerRect:) method:

/* Delete this method

override func viewDidLoad() {
  super.viewDidLoad()
  // 1
  let cardNode = CardNode()
  cardNode.backgroundColor = UIColor(white: 1.0, alpha: 0.27)
  let origin = CGPointZero
  let size = CGSize(width: 100, height: 100)
  cardNode.frame = CGRect(origin: origin, size: size)

  // 2
  view.addSubview(cardNode.view)
}
*/

func createCardNode(#containerRect: CGRect) -> CardNode {
  // 3
  let cardNode = CardNode()
  cardNode.backgroundColor = UIColor(white: 1.0, alpha: 0.27)
  cardNode.measure(containerRect.size)

  // 4
  let size = cardNode.calculatedSize
  let origin = containerRect.originForCenteredRectWithSize(size)
  cardNode.frame = CGRect(origin: origin, size: size)
  return cardNode
}

Here’s a section-by-section breakdown:

  1. Delete the old way of creating, configuring, and laying out container node.
  2. Delete the old way of creating the container node’s view and adding it to the view hierarchy
  3. createCardNode(containerRect:) creates a new card node with the same background color as the old container node, and it uses a provided container rect to constrain the size of the card node, so the card node cannot be any larger than containerRect’s size.
  4. Centers the card within the containerRect using the originForCenteredRectWithSize(size:) helper method. Note that the helper method is a custom method provided in the starter project that was added to CGRect instances via an extension.

Right below the createCardNode(containerRect:) method, re-implement viewDidLoad():

override func viewDidLoad() {
  super.viewDidLoad()
  let cardNode = createCardNode(containerRect: UIScreen.mainScreen().bounds)
  view.addSubview(cardNode.view)
}

When the view controller’s view loads, createCardNode(containerRect:) creates and sets up a new CardNode. The card node cannot be any larger than the main screen’s bounds size.

At this point in its lifecycle, the view controller’s view has not been laid out. Therefore, it’s not safe to use the view controller’s view’s bounds size, so you’re using the main screen’s bounds size to constrain the size of the card node.

This approach, albeit less than elegant, works for this view controller because it spans the entire screen. Later in this tutorial, you’ll move this logic to a more appropriate method, but for now, it works, so roll with it!

Build and run, and you’ll see your node properly centered.

ASDK_NodeHierarchies - 3

Asynchronous Node Setup and Layout

Sometimes it takes a human being a perceivable amount of time to lay out complex hierarchies, if that is happening on the main thread. This blocks UI interaction. You can’t have any perceivable wait times if you expect to please the user.

For this reason, you’ll create, set up and lay out nodes in the background so that you can avoid blocking the main UI thread.

Implement addCardViewAsynchronously(containerRect:) in between createCardNode(containerRect:) and viewDidLoad():

func addCardViewAsynchronously(#containerRect: CGRect) {
  dispatch_async(dispatch_get_global_queue(QOS_CLASS_BACKGROUND, 0)) {
    let cardNode = self.createCardNode(containerRect: containerRect)
    dispatch_async(dispatch_get_main_queue()) {
      self.view.addSubview(cardNode.view)
    }
  }
}

addCardViewAsynchronously(containerRect:) creates the CardNode on a background queue — which is fine because nodes are thread safe! After creating, configuring and framing the node, execution returns to the main queue in order to add the node’s view to the view controller’s view hierarchy — after all, UIKit isn’t thread safe. :]

Note: Once you create the node’s view, all access to the node occurs exclusively on the main thread.

Re-implement viewDidLoad() by using addCardViewAsynchronously(containerRect:):

override func viewDidLoad() {
  super.viewDidLoad()
  addCardViewAsynchronously(containerRect: UIScreen.mainScreen().bounds)
}

No more blocking the main thread, ensuring the user interface remains responsive!

Build and run. Same as before, but all the sizing of your node is now being done on a background thread! Neat! :]

ASDK_NodeHierarchies - 3

Constraining the Node Size to View Controller’s View Size

Remember I said that you’d use a more elegant solution to size the node than just relying on the screen size? Well, I’m delivering on that promise right now!

Open ViewController.swift. Add the following property at the top of the class:

var cardViewSetupStarted = false

Then replace viewDidLoad() with viewWillLayoutSubviews():

/* Delete this method
override func viewDidLoad() {
  super.viewDidLoad()
  addCardViewAsynchronously(containerRect: UIScreen.mainScreen().bounds)
}
*/

override func viewWillLayoutSubviews() {
  super.viewWillLayoutSubviews()
  if !cardViewSetupStarted {
    addCardViewAsynchronously(containerRect: view.bounds)
    cardViewSetupStarted = true
  }
}

Instead of using the main screen’s bounds size, the logic above uses the view controller’s view’s bounds size to constrain the size of the card node.

Now it’s safe to use the view controller’s views’ bound size since the logic is inside viewWillLayoutSubviews() instead of viewDidLoad(). By this time in its lifecycle, the view controller’s view will already have its size set.

This approach is superior because a view controller’s view can be any size, and you don’t want to depend on the fact that this view controller happens to span the entire screen.

Screen size's tyrannical reign is over

The view can be laid out multiple times. So viewWillLayoutSubviews() can be called multiple times. You only want to create the card node once, and that’s why you need the cardViewSetupStarted flag to prevent the view controller from creating the card node multiple times.

Build and run.

ASDK_NodeHierarchies - 3

The Node Hierarchy

Currently you have an empty container card node on screen. Now you want to display some content. The way to do this is to add subnodes to the card node. The following diagram describes the simple node hierarchy you’ll build.

ASDK_Nodes

The process of adding a subnode will look very familiar since the process is similar to how you add subviews within custom UIView subclasses.

The first step is to add the image node, but first, you should know how container nodes lay out their subnodes.

Subnode Layout

You now know how to measure the container node’s size and how to use that calculated size to lay out the container node’s view. That takes care of the container, but how does the container node lay out its subnodes?

It’s a two-step process:

  1. First, you measure each subnode’s size in calculateSizeThatFits(constrainedSize:). This ensures that each subnode caches a calculated size.
  2. During UIKit’s layout pass on the main thread, AsyncDisplayKit will call layout() on your custom ASDisplayNode subclass. layout() works just like UIView’s layoutSubviews(), except that layout() doesn’t have to calculate the sizes of all of its children. layout() simply queries each subnode’s calculated size.

Back to the UI. The Taj Mahal’s card size should equal the size of its image, and the title should then fit within that size. The easiest way to accomplish this is to measure the Taj Mahal image node’s size and use the result to constrain the title text node’s size, so that the text node fits within the size of the image.

And that is the logic you’ll use to lay out the card’s subnodes. Now you’re going to make it happen in code. :]