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

AsyncDisplayKit is an iOS framework that was originally designed for Facebook’s Paper. It makes it possible to achieve smoother and more responsive UI behavior than you can get with standard views.

You may have learned a bit about AsyncDisplayKit already in our beginning AsyncDisplayKit tutorial or your own studies; this tutorial will take your knowledge to the next level.

This tutorial will explain how you can make full use of the framework by exploring AsyncDisplayKit node hierarchies. By doing this, you get the benefits of smooth scrolling that AsyncDisplayKit is known for, at the same time as being able to build flexible and reusable UIs.

One of the key concepts of AsyncDisplayKit is the node. As you’ll learn, AsyncDisplayKit nodes are a thread-safe abstraction layer over UIView, which is (as you know) not thread safe. You can learn more about AsyncDisplayKit in AsyncDisplayKit’s Quick Start introduction.

The good news is that if you already know UIKit, then you’ll find that you already know the methods and properties in AsyncDisplayKit, because the APIs are almost identical.

By following along, you’ll learn:

  • How to build your own ASDisplayNode subclass.
  • How subclassing allows you to encapsulate node hierarchies into a single container node for organization and reuse.
  • How using node hierarchies can be superior to view hierarchies, because you automatically reduce the chance of stalling the main thread, keeping your user interface smooth and responsive.

Here’s how you’ll do it: You’ll build a container node that will hold two subnodes — one for the image and one for the title. You’ll see how containers measure their own size and how they lay out their subnodes. By the end, you’ll take your existing UIView container subclasses and convert them over to ASDisplayNode subclasses.

This is what you’re aiming towards:

ASDK_NodeHierarchies-7-e1434332214236

Cool stuff right? The smoother the UI, the better it is for all. With that said, it’s time to dive in!

Note: This is an intermediate tutorial tailored for engineers who have already dabbled a bit with AsyncDisplayKit and are familiar with the basics. If this is your first time using AsyncDisplayKit, first read through our beginning AsyncDisplayKit tutorial and check out AsyncDisplayKit’s Getting Started guide.

Getting Started

The app you’ll build presents a card that shows one of the wonders of the world, the Taj Mahal.

Download and open the starter project.

The project is a basic app with one view controller. Time to get acquainted with it!

The project uses CocoaPods to pull in AsyncDisplayKit. So, in usual CocoaPods style, go ahead and open Wonders.xcworkspace but NOT Wonders.xcodeproj.

Note: If you’re not familiar with CocoaPods, then that’s OK. But if you want to learn more about it then check out this Introduction to CocoaPods Tutorial.

Open ViewController.swift and take notice of the view controller’s constant property named card. It holds a data model value for the Taj Mahal, and you’ll use this model later to create a card node to display this wondrous structure to the user.

Build and run to make sure you have a working project. You should see an empty black screen — the digital equivalent of a blank canvas.

ASDK_NodeHierarchies - 1

Creating and Displaying a Container Node

Now you’re going to build your very first node hierarchy. It’s very similar to building a UIView hierarchy, which I’m sure you’re familiar with :].

Open Wonders-Bridging-Header.h, and add the following import statement:

#import <AsyncDisplayKit/ASDisplayNode+Subclasses.h>

ASDisplayNode+Subclasses.h exposes methods that are internal to ASDisplayNode. You need to import this header so you can override methods in ASDisplayNode subclasses, but it’s important to note that you should only call these methods within your ASDisplayNode subclasses. This is a similar pattern to UIGestureRecognizer which also has a header purely for subclasses.

Open CardNode.swift and add the following ASDisplayNode subclass implementation:

class CardNode: ASDisplayNode {}

This declares a new ASDisplayNode subclass that you’ll use as a container to hold the card’s user interface.

Open ViewController.swift and implement viewDidLoad():

override func viewDidLoad() {
  super.viewDidLoad()
  // Create, configure, and lay out container node
  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)
  // Create container node’s view and add to view hierarchy
  view.addSubview(cardNode.view)
}

This code creates a new card node with a hard-coded size. It will sit on the upper-left corner and will have a width and height of 100.

Don’t worry about the odd alignment at this pass. You’ll center the card nicely within the view controller very soon!

Build and run.

ASDK_NodeHierarchies - 2

Great! You have a custom node subclass that shows up on the screen. The next step is to give your node subclass, named CardNode, the ability to calculate its own size. This is required to be able to center it in the view. Before doing that, you should understand how the node layout engine works.

Node Layout Engine

The next task is to ask a node to calculate its own size by calling measure(constrainedSize:) on the node.

You’ll pass the constrainedSize argument into the method to tell the node to calculate a size that fits within constrainedSize.

In layman’s terms, this means the calculated size can be no larger than the constrained size provided.

For example, consider the following diagram:

ASDK_measure

This shows a constrained size with a certain width and height. The calculated size is equal in width, but smaller in height. It could have been equal on both width and height, or smaller on both width and height. But neither the width nor the height are allowed to be greater than the constrained size.

This works similarly to UIView’s sizeThatFits(size:). But the difference is that measure(constrainedSize:) holds on to the size it calculates, allowing you to access the cached value via the node’s calculatedSize property.

An example of when the calculated size is smaller in width and height than the constrained size is as follows:

ASDK_constrained2

Here the image’s size smaller than the constrained size, and without any sizing-to-fit logic, the calculated size is smaller than the constrained size.

The reason AsyncDisplayKit incorporates sizing into its API is because often, it may take a perceivable amount of time to calculate a size. Reading an image from disk to calculate the size can be very slow for example. By incorporating sizing into the node API, which remember is thread safe, means that sizing can all be performed on a background thread! Neat! It’s a sweet little feature that makes the UI smooth as butter, and the user has less of those awkward moments where he wonders if his phone broke.

A node will run size calculations if it has not already done so and has no cached value, or if the constrained size provided is different than the constrained size used to determine the cached calculated size.

In programmers’ terms, it works like this:

  • measure(constrainedSize:) either returns a cached size or runs a size calculation by calling calculateSizeThatFits(constrainedSize:).
  • You place all the size calculation logic inside of calculateSizeThatFits(constrainedSize:) within your ASDisplayNode subclass.

calculateSizeThatFits(constrainedSize:) is internal to ASDisplayNode, and you shouldn’t call it outside of your subclass.