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.
Sign up/Sign in
With a free Kodeco account you can download source code, track your progress, bookmark, personalise your learner profile and more!
Create accountAlready a member of Kodeco? Sign in
Sign up/Sign in
With a free Kodeco account you can download source code, track your progress, bookmark, personalise your learner profile and more!
Create accountAlready a member of Kodeco? Sign in
Contents
AsyncDisplayKit Tutorial: Node Hierarchies
25 mins
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:
- Delete the old way of creating, configuring, and laying out container node.
- Delete the old way of creating the container node’s view and adding it to the view hierarchy
-
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 thancontainerRect
’s size. - Centers the card within the
containerRect
using theoriginForCenteredRectWithSize(size:)
helper method. Note that the helper method is a custom method provided in the starter project that was added toCGRect
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.
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.
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! :]
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.
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.
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.
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:
- First, you measure each subnode’s size in
calculateSizeThatFits(constrainedSize:)
. This ensures that each subnode caches a calculated size. - During UIKit’s layout pass on the main thread, AsyncDisplayKit will call
layout()
on your customASDisplayNode
subclass.layout()
works just like UIView’slayoutSubviews()
, except thatlayout()
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. :]