Facebook’s Paper team has come out with another great library: AsyncDisplayKit. This library enables super-responsive user interfaces by letting you perform image decoding, layout and rendering operations on background threads, so they don’t block user interaction. Learn about it in this AsyncDisplayKit tutorial.
For example, you can use AsyncDisplayKit to build silky-smooth, 60-frame-per-second scrolling experiences for very complex interfaces, where normal UIKit optimizations wouldn’t be enough to overcome performance challenges.
In this tutorial, you’ll take a starter project that has some major UICollectionView
scrolling issues and use AsyncDisplayKit to drastically improve its performance. Along the way, you’ll learn how to use AsyncDisplayKit in your existing projects.
Note: Before beginning this tutorial, you should be familiar with Swift, Core Animation and Core Graphics.
Check out the Swift tutorials here on this site, Core Animation Programming Guide and Quartz 2D Programming Guide if you want to brush up or dive into these topics. WWDC 2012’s session named “iOS App Performance: Graphics and Animations” is another great resource that I highly recommend—you can watch it multiple times and learn something new each time!
Getting Started
Before getting started, take a look at AsyncDisplayKit’s intro. This will give you a brief overview of what the library is, and what it solves.
When you’re ready, download the starter project. You’ll need Xcode 6.1 and the iOS 8.1 SDK to build the project.
Note: The code in this tutorial was written for AsyncDisplayKit 1.0. This version of the library is included in the starter project.
The app you’re going to be working with consists of a UICollectionView
of cards describing different animals found in a rainforest. Each rainforest info card includes a picture, name and description of a rainforest animal. The background of each card is a blurred version of the main picture. Visual design details on the card ensure the text is legible.
In Xcode, open Layers.xcworkspace from the downloaded starter project.
Throughout this tutorial, follow these guidelines to see the most dramatic benefit from using AsyncDisplayKit:
- Run the app on a real device. It will be hard to see performance improvements if you run on the iOS Simulator.
- The app is universal, but it looks best on an iPad.
- Finally, to really appreciate what this library can do for you, run the app on the oldest device you can find that runs iOS 8.1. A third-generation iPad is perfect, because it has a retina screen but is not particularly fast.
Once you’ve got your device of choice, build and run the project. You will see something that looks like this:
Scroll through the collection view and notice the poor frame rate. On a third-generation iPad, it’s about 15-20 FPS. The collection view is dropping a ton of frames. By the end of this tutorial, you’ll have it scrolling at (or extremely close to) 60 FPS!
Note: All the images you see inside the app are bundled in the asset catalog. None of them are retrieved from the network.
Measuring Responsiveness
Before using AsyncDisplayKit on an existing project, you should measure your UI’s performance with Instruments so that you have a baseline to measure against as you make changes.
Most importantly, you want to find out whether you’re CPU-bound or GPU-bound. This means, is it the CPU or the GPU that’s stopping your app from running with a higher frame rate. That information will tell you what AsyncDisplayKit features to leverage to optimize your app.
If you have some time on your hands, watch the WWDC 2012 session mentioned in the Getting Started section and/or time profile the starter project with Instruments using a real device. The scrolling performance is CPU-bound. Can you guess what’s causing the collection view to drop so many frames?
Preparing the Project for AsyncDisplayKit
Using AsyncDisplayKit on an existing project boils down to replacing view hierarchies and/or layer trees with display node hierarchies. Display nodes are the key tenant of AsyncDisplayKit. They sit on top of views and are thread-safe, meaning work that is traditionally performed on the main thread can now be performed off the main thread. This leaves your main thread free to perform other actions such as handling touch events or in the case of this app, handling scrolling of the collection view.
That means your first step in this tutorial is to remove the view hierarchy.
Removing the View Hierarchy
Open RainforestCardCell.swift and delete all the addSubview(...)
calls in awakeFromNib()
, so that you have the following:
override func awakeFromNib() { super.awakeFromNib() contentView.layer.borderColor = UIColor(hue: 0, saturation: 0, brightness: 0.85, alpha: 0.2).CGColor contentView.layer.borderWidth = 1 } |
Next, replace the contents of layoutSubviews()
with the following:
override func layoutSubviews() { super.layoutSubviews() } |
Now replace configureCellDisplayWithCardInfo(cardInfo:)
with the following:
func configureCellDisplayWithCardInfo(cardInfo: RainforestCardInfo) { //MARK: Image Size Section let image = UIImage(named: cardInfo.imageName)! featureImageSizeOptional = image.size } |
Delete all the view properties from RainforestCardCell
, so that the only remaining property is as follows:
class RainforestCardCell: UICollectionViewCell { var featureImageSizeOptional: CGSize? ... } |
Finally, build and run, and you’ll see a whole lot of nothing:
Now that the cells are empty, they scroll super-smoothly. Your goal is to keep them scrolling smoothly while adding all the content back with nodes instead of views.
Feel free to profile the app using Instruments’s Core Animation template every step of the way on a real device, to see how your changes affect the frame rate.
Adding a Placeholder
Moving forward in RainforestCardCell.swift, add a CALayer
variable stored property called placeholderLayer
to RainforestCardCell
. The type should be an implicitly unwrapped optional:
class RainforestCardCell: UICollectionViewCell { var featureImageSizeOptional: CGSize? var placeholderLayer: CALayer! ... } |
You need a placeholder because the display will be done asynchronously, and if that takes some time, the user will see empty cells—which isn’t pleasant. It’s the same as when you’re fetching an image from the network and you need to fill the cells with placeholders to let your users know the content isn’t ready yet. Except in this case, you’ll be drawing in the background instead of fetching from the network.
In awakeFromNib()
, delete the contentView
‘s border setup and create and configure a placeholderLayer
. Add it to the cell contentView
‘s layer. The method will then look like this:
override func awakeFromNib() { super.awakeFromNib() placeholderLayer = CALayer() placeholderLayer.contents = UIImage(named: "cardPlaceholder")!.CGImage placeholderLayer.contentsGravity = kCAGravityCenter placeholderLayer.contentsScale = UIScreen.mainScreen().scale placeholderLayer.backgroundColor = UIColor(hue: 0, saturation: 0, brightness: 0.85, alpha: 1).CGColor contentView.layer.addSublayer(placeholderLayer) } |
In layoutSubviews()
, you need to lay out the placeholderLayer
. Replace the method with the following:
override func layoutSubviews() { super.layoutSubviews() placeholderLayer?.frame = bounds } |
Build and run, and you’re back from the brink of nothingness:
Plain CALayers which are not backed by a UIView have implicit animations when they change frame by default. That’s why you see the layer scale up when it is laid out. To fix that, add change the implementation of layoutSubviews
as follows:
override func layoutSubviews() { super.layoutSubviews() CATransaction.begin() CATransaction.setValue(kCFBooleanTrue, forKey: kCATransactionDisableActions) placeholderLayer?.frame = bounds CATransaction.commit() } |
Build and run, and you’ll see you’ve solved the problem.
Now the placeholders are sitting still, no longer animating their frames.
Your First Node
The first step in rebuilding the app is to add a background image node to each UICollectionView
cell. In this section, you’ll learn how to:
- Create, lay out and add an image node to a
UICollectionView
cell; - Handle cell reuse with nodes and their layers; and
- Blur the image node.
But before you do anything else, open Layers-Bridging-Header.h and import AsyncDisplayKit:
#import <AsyncDisplayKit/AsyncDisplayKit.h> |
This makes AsyncDisplayKit’s classes available from all the Swift files.
Build to make sure everything compiles OK.
Orientation: Rainforest Collection View Mechanics
Now, let’s take a look at the components of the collection view:
- The View Controller: The
RainforestViewController
doesn’t do anything fancy. It simply gets an array of data for all the rainforest cards and implements the data source for theUICollectionView
. As a matter of fact, you won’t spend much time in the view controller. - The Data Source: You’ll spend most of your time in the cell class,
RainforestCardCell
. The view controller dequeues each cell and simply hands the rainforest card data to the cell by callingconfigureCellDisplayWithCardInfo(cardInfo:)
. The cell then uses the data to configure itself. - The Cell: In
configureCellDisplayWithCardInfo(cardInfo:)
, the cell creates, configures, lays out and add nodes to itself. This means every time the view controller dequeues a cell, the cell will create and add to itself a brand new node hierarchy.If you were using views instead of nodes, this wouldn’t be the best strategy for performance reasons. But because you can create, configure and lay out nodes asynchronously, and because nodes can draw asynchronously, it turns out this isn’t a problem. The trick is to cancel any ongoing asynchronous activity and to remove old nodes when cells are preparing for reuse.
Note: The strategy this tutorial uses to add nodes to cells is an OK one. It’s a good first step toward mastering AsyncDisplayKit.
However, in production, you would want to use ASRangeController
to cache your nodes so that you don’t have to re-create the cell’s node hierarchy on every reuse. ASRangeController
is beyond the scope of this introductory tutorial, but for more information, check out the header comments in ASRangeController.h.
One last note: Version 1.1 of AsyncDisplayKit (which has not been released as of this writing, but will be available soon thereafter) includes ASCollectionView
. Using ASCollectionView
would allow the entire collection view in this app to be handled by display nodes. Instead, in this tutorial, each cell will contain a display node hierarchy. As explained above, this works, but it would be better if the entire collection view were handled by nodes. Roll on ASCollectionView
!
OK, it’s time to get your hands dirty.
Adding the Background Image Node
Now you’re going to walk through configuring the cell using nodes, one step at a time.
Open RainforestCardCell.swift and replace configureCellDisplayWithCardInfo(cardInfo:)
with the following:
func configureCellDisplayWithCardInfo(cardInfo: RainforestCardInfo) { //MARK: Image Size Section let image = UIImage(named: cardInfo.imageName)! featureImageSizeOptional = image.size //MARK: Node Creation Section let backgroundImageNode = ASImageNode() backgroundImageNode.image = image backgroundImageNode.contentMode = .ScaleAspectFill } |
This creates and configures an ASImageNode
constant called backgroundImageNode
.
Note: Make sure to include the //MARK:
comments in your code. This will make it easier to follow along.
AsyncDisplayKit ships with several display node classes, including ASImageNode
, which you use when you need to display an image. It’s the equivalent of a UIImageView
, except that ASImageNode
decodes images asynchronously by default.
Add the following code at the end of configureCellDisplayWithCardInfo(cardInfo:):
backgroundImageNode.layerBacked = true |
This makes backgroundImageNode
a layer-backed node.
Nodes can be backed by either UIView
or CALayer
instances. You typically use a view-backed node when the node needs to handle events such as touch events. If you don’t need to handle events and simply need to display some content, then use a layer-backed node—it’s not as heavy, so you’ll get a small performance gain.
Because this tutorial’s app doesn’t require event handling, you’ll make all the nodes layer-backed. In the code above, since backgroundImageNode
is layer-backed, AsyncDisplayKit will create a CALayer
for the contents of the rainforest animal’s image.
Continuing in configureCellDisplayWithCardInfo(cardInfo:)
add the following code at the bottom of the method:
//MARK: Node Layout Section backgroundImageNode.frame = FrameCalculator.frameForContainer(featureImageSize: image.size) |
This lays out backgroundImageNode
with FrameCalculator
.
FrameCalculator
is a helper class that encapsulates the cell’s layout, returning frames for each node. Notice that everything is laid out manually, without Auto Layout constraints. If you need to build adaptive layouts or localized-driven layouts, tread carefully here, because you can’t add constraints to nodes.
Next, add the following to the bottom of configureCellDisplayWithCardInfo(cardInfo:):
//MARK: Node Layer and Wrap Up Section self.contentView.layer.addSublayer(backgroundImageNode.layer) |
This adds backgroundImageNode
’s layer to the cell contentView
’s layer.
As noted, AsyncDisplayKit will create a layer for backgroundImageNode
. However, you have to place the node inside of a layer tree for the node to show up onscreen. This node will draw asynchronously, so the contents of the node won’t show up until drawing is complete, even though the layer is in a layer tree.
From a technical perspective, the layer is there the entire time. But the rendering of the image is done asynchronously. The layer initially has no contents (i.e. transparent). Once rendering is complete, the layer’s contents is updated to contain the image contents.
At this point, the cell’s content view’s layer will contain two sublayers: the placeholder and the node’s layer. Only the placeholder will be visible until the node finishes drawing.
Notice that configureCellDisplayWithCardInfo(cardInfo:)
gets called every time a cell is dequeued. Every time a cell is recycled, this logic is adding a new sublayer to the cell’s contentView
layer. Don’t worry; you’ll address this soon.
Back at the top of RainforestCardCell.swift, add an ASImageNode
variable stored property called backgroundImageNode
to RainforestCardCell
, like so:
class RainforestCardCell: UICollectionViewCell { var featureImageSizeOptional: CGSize? var placeholderLayer: CALayer! var backgroundImageNode: ASImageNode? ///< ADD THIS LINE ... } |
You need this property because something has to hold onto the backgroundImageNode
reference, or else ARC will release it and nothing will show up. Nodes hold onto their layers, but layers don’t hold onto their nodes—so even though the node’s layer is in a layer tree, you still have to hold onto the node.
At the end of the Node Layer and Wrap Up Section in configureCellDisplayWithCardInfo(cardInfo:)
, set the cell’s new backgroundImageNode
property to the original backgroundImageNode
constant:
self.backgroundImageNode = backgroundImageNode |
Here’s the complete configureCellDisplayWithCardInfo(cardInfo:)
method:
func configureCellDisplayWithCardInfo(cardInfo: RainforestCardInfo) { //MARK: Image Size Section let image = UIImage(named: cardInfo.imageName)! featureImageSizeOptional = image.size //MARK: Node Creation Section let backgroundImageNode = ASImageNode() backgroundImageNode.image = image backgroundImageNode.contentMode = .ScaleAspectFill backgroundImageNode.layerBacked = true //MARK: Node Layout Section backgroundImageNode.frame = FrameCalculator.frameForContainer(featureImageSize: image.size) //MARK: Node Layer and Wrap Up Section self.contentView.layer.addSublayer(backgroundImageNode.layer) self.backgroundImageNode = backgroundImageNode } |
Build and run, and watch AsyncDisplayKit asynchronously set the layer’s contents with its image. This allows you to scroll while the CPU is still drawing the contents of the layers in the background.
If you’re running on an older device, notice how the images pop into place—it’s the popcorn effect, not always welcome! The last section of the tutorial addresses this unpleasant pop and shows you how to make the images fade in nicely, like a rock star.
As discussed earlier, a new node is created each time a cell is reused. This is not ideal because it means that a new layer is added to the cell each time the cell is reused.
If you’d like to see the sublayer pileup that’s happening here, scroll up and down multiple times, then breakpoint and print out the sublayers property of a cell’s content view layer. You should see a lot of layers, which isn’t ideal.
Handling Cell Reuse
Continuing in RainforestCardCell.swift, add a CALayer
variable stored property called contentLayer
to RainforestCardCell
. The property should be an optional type:
class RainforestCardCell: UICollectionViewCell { var featureImageSizeOptional: CGSize? var placeholderLayer: CALayer! var backgroundImageNode: ASImageNode? var contentLayer: CALayer? ///< ADD THIS LINE ... } |
You’ll end up using this property to remove the old node’s layer from the cell content view’s layer tree. You could simply hold onto the node and access its layer property, but the above solution is more explicit.
Add the following code at the end of configureCellDisplayWithCardInfo(cardInfo:)
:
self.contentLayer = backgroundImageNode.layer |
This assigns backgroundImageNode
’s layer to the new contentLayer
property.
Then replace the implementation of prepareForReuse()
with the following:
override func prepareForReuse() { super.prepareForReuse() backgroundImageNode?.preventOrCancelDisplay = true } |
Because AsyncDisplayKit can draw nodes asynchronously, nodes let you prevent drawing from starting or cancel any drawing that’s in progress. Whenever you need to prevent or cancel drawing, set preventOrCancelDisplay
to true
as you do above. In this case, you want to cancel any ongoing drawing activity before the cell gets reused.
Next, add the following to the end of prepareForReuse()
:
contentLayer?.removeFromSuperlayer() |
This removes contentLayer
from its superlayer — the contentView
’s layer.
Every time a cell is recycled, this code removes the old node’s layer that was added during dequeue, thereby solving the pileup issue. So at all times, you should have at most two sublayers: the placeholder and the node’s layer.
Next add the following code to the end of prepareForReuse()
:
contentLayer = nil backgroundImageNode = nil |
This ensures that the cell releases these references so ARC can clean up, if needed.
Build and run. This time, there’s no sublayer pileup and all unnecessary drawing gets cancelled.
It’s time to blur, baby, blur.
Blurring the Image
To blur the image, you’re going to add an extra step to the image node’s display process.
Continuing inside RainforestCardCell.swift, in configureCellDisplayWithCardInfo(cardInfo:)
, right after setting backgroundImageNode.layerBacked
, add the following code:
backgroundImageNode.imageModificationBlock = { input in if input == nil { return input } if let blurredImage = input.applyBlurWithRadius( 30, tintColor: UIColor(white: 0.5, alpha: 0.3), saturationDeltaFactor: 1.8, maskImage: nil, didCancel:{ return false }) { return blurredImage } else { return image } } |
ASImageNode
’s imageModificationBlock
gives you a chance to process the underlying image before displaying it. It’s a neat feature that allows you to do things like apply filters to image nodes.
In the code above, you use the imageModificationBlock
to apply a blur to the cell’s background image. The key point is that the image node will draw its contents and execute this closure in the background, keeping the main thread’s run loop running smoothly. The closure takes in the original UIImage
and returns a modified UIImage
.
This code uses a UIImage
blurring category, released by Apple at WWDC 2013, that uses the Accelerate framework to blur images on the CPU. Because blurring can take a lot of time and memory, this version of the category was modified to include a cancelation mechanism. The blurring method will periodically call the didCancel
closure to see whether it should stop.
For now, the above code simply returns false
for didCancel
. You’ll write the actual didCancel
closure further down.
Note: Remember how poorly the collection view scrolled when you ran the app for the first time? The blurring method was clogging the main thread. By moving the blur to the background using AsyncDisplayKit, you are drastically improving the scrolling performance of the collection view. It’s like night and day.
Build and run to see your blur effect:
Notice how smoothly you can scroll through the collection view.
A blur operation begins in the background when the collection view dequeues a cell. When the user scrolls quickly through the collection view, the collection view reuses each cell multiple times, starting many blur operations. The goal is to cancel an in-progress blur operation when the cell for which it’s working is preparing to be reused.
You’re already canceling the node’s drawing in prepareForReuse()
, but once control gets handed over to your image modification closure, it’s your responsibility to respond to the node’s preventOrCancelDisplay
flag, which you’ll add now.
Canceling the Blur
To cancel the blur while it’s in progress, you need to implement the didCancel
closure of the blur method.
Add a capture list to the imageModificationBlock
to capture a weak reference to backgroundImageNode
:
backgroundImageNode.imageModificationBlock = { [weak backgroundImageNode] input in ... } |
You need the weak reference so as to avoid a retain cycle between the closure and the image node. You’ll use the weak backgroundImageNode
to determine if you should cancel the blurring.
It’s time to build the blur cancellation closure. Add the code indicated below to the imageModificationBlock
:
backgroundImageNode.imageModificationBlock = { [weak backgroundImageNode] input in if input == nil { return input } // ADD FROM HERE... let didCancelBlur: () -> Bool = { var isCancelled = true // 1 if let strongBackgroundImageNode = backgroundImageNode { // 2 let isCancelledClosure = { isCancelled = strongBackgroundImageNode.preventOrCancelDisplay } // 3 if NSThread.isMainThread() { isCancelledClosure() } else { dispatch_sync(dispatch_get_main_queue(), isCancelledClosure) } } return isCancelled } // ...TO HERE ... } |
Here’s what this code you just added does:
- This grabs a strong reference to
backgroundImageNode
, ready for us to do some work with it. IfbackgroundImageNode
has gone away by the time this runs, thenisCancelled
will stay true, and the blur will be canceled. There’s no point continuing a blur when there’s no node to display it in! - Here you wrap the cancellation check in a closure because once a node creates its layer or view, you can only access node properties on the main thread. Since you need to access
preventOrCancelDisplay
, you have to run the check on the main thread. - Finally, here you make sure that
isCancelledClosure
is called on the main thread. Either directly if we’re already on the main thread, or through adispatch_sync
if not. It must be a synchronous dispatch because we need the closure to finish and setisCancelled
before thedidCancelBlur
closure can return it.
On the call to applyBlurWithRadius(...)
, change the argument you pass to the didCancel
parameter, replacing the closure that always returns false
with the closure you defined above and assigned to the didCancelBlur
constant:
if let blurredImage = input.applyBlurWithRadius( 30, tintColor: UIColor(white: 0.5, alpha: 0.3), saturationDeltaFactor: 1.8, maskImage: nil, didCancel: didCancelBlur) { ... } |
Build and run. You might not notice much difference, but now any blurs that haven’t finished by the time the cell has gone off the screen will be canceled. This will mean the device is doing less than it was before. You may see a slight performance improvement, especially on slower devices such as the third generation iPad.
Of course, these aren’t really backgrounds without something in the foreground! Your cards need content. Over the next four sections, you’ll learn how to:
- Create a container node that draws all of its subnodes into a single
CALayer
; - Build a node hierarchy;
- Create a custom
ASDisplayNode
subclass; and - Build and lay out node hierarchies in the background.
After you have finished this you will have an app that looked like it did before you added AsyncDisplayKit, but has buttery smooth scrolling.
Rasterized Container Node
Up to now, you’ve been working with a single node inside of a cell. Next, you’ll create a container node that will house all of a card’s content.
Adding a Container Node
Continuing inside RainforestCardCell.swift, in configureCellDisplayWithCardInfo(cardInfo:)
, after setting backgroundImageNode.imageModificationBlock
and before the Node Layout Section, add the following code:
//MARK: Container Node Creation Section let containerNode = ASDisplayNode() containerNode.layerBacked = true containerNode.shouldRasterizeDescendants = true containerNode.borderColor = UIColor(hue: 0, saturation: 0, brightness: 0.85, alpha: 0.2).CGColor containerNode.borderWidth = 1 |
This creates and configure an ASDisplayNode
constant called containerNode
. Notice the container’s shouldRasterizeDescendants
property. This is a hint about how nodes work and an opportunity to make them work better.
As the word “descendants” implies, you can create hierarchies or trees of AsyncDisplayKit nodes, just as you can create hierarchies of Core Animation layers. For instance, if you have a hierarchy of nodes that are all layer-backed, then AsyncDisplayKit will create a separate CALayer
for each node, and that layer hierarchy will mirror the node hierarchy.
This should sound familiar: It’s analogous to how the layer hierarchy mirrors the view hierarchy when you’re using regular UIKit. However, this stack of layers has a couple of consequences:
- First, with asynchronous rendering, you would see each layer show up one by one. As AsyncDisplayKit finishes drawing each layer, it immediately makes the layer display content. So if you have one layer that takes longer to draw than another, then it will display after the other. The user would see the piecemeal assembly of all the layers, a process that’s normally invisible since Core Animation redraws all needed layers at the turn of the run loop before displaying anything.
- Second, having many layers could cause performance issues. Every
CALayer
needs a backing store of memory for its pixel bitmap or contents. Also, Core Animation has to send each layer over XPC to the render server. Finally, the render server may need to redraw some of the layers to composite them, for instance when blending layers. In general, more layers means more work for Core Animation. There are many benefits to limiting the number of layers you use.
To address this, AsyncDisplayKit has a handy feature: It allows you to draw a node hierarchy into one single-layer container. That’s exactly what shouldRasterizeDescendants
does. When you set this, ASDisplayNode
won’t set the layer’s contents until it finishes drawing all of its subnodes.
So in the previous step, setting the container node’s shouldRasterizeDescendants
to true
has two benefits:
- It ensures that all the nodes in the card display at once, as with plain old synchronous drawing;
- And it improves efficiency by rasterizing the stack of layers into a single layer and minimizing future compositing.
The downside is that, since you are flattening all the layers into one bitmap, you cannot later animate the nodes individually.
For more information, check out the shouldRasterizeDescendants
header comments in ASDisplayNode.h.
Next, after the Container Node Creation Section, add backgroundImageNode
as a subnode of containerNode
:
//MARK: Node Hierarchy Section containerNode.addSubnode(backgroundImageNode) |
Note: The order in which you add subnodes matters, just as with subviews and sublayers. A node added before another is displayed below the other.
Replace the first line in the Node Layout Section with this:
//MARK: Node Layout Section containerNode.frame = FrameCalculator.frameForContainer(featureImageSize: image.size) |
Finally, lay out backgroundImageNode
using FrameCalculator
:
backgroundImageNode.frame = FrameCalculator.frameForBackgroundImage( containerBounds: containerNode.bounds) |
This sets up the backgroundImageNode
to fill the containerNode
.
You’re almost there with the new node hierarchy, but first you need to setup the layer hierarchy correctly since the container node is now the root.
Managing the Container Node’s Layer
In the Node Layer and Wrap Up Section, instead of adding backgroundImageNode
’s layer to the cell contentView
’s layer, add containerNode
’s layer:
// Replace the following line... // self.contentView.layer.addSublayer(backgroundImageNode.layer) // ...with this line: self.contentView.layer.addSublayer(containerNode.layer) |
Delete the following backgroundImageNode
property assignment:
self.backgroundImageNode = backgroundImageNode |
Because the cell simply needs to hold onto the container node alone, you’ll end up removing the backgroundImageNode
property.
Instead of setting the cell’s contentLayer
property to backgroundImageNode
’s layer, set it to containerNode
’s layer:
// Replace the following line... // self.contentLayer = backgroundImageNode.layer // ...with this line: self.contentLayer = containerNode.layer |
Add an optional ASDisplayNode
variable stored property called containerNode
to RainforestCardCell
:
class RainforestCardCell: UICollectionViewCell { var featureImageSizeOptional: CGSize? var placeholderLayer: CALayer! var backgroundImageNode: ASImageNode? var contentLayer: CALayer? var containerNode: ASDisplayNode? ///< ADD THIS LINE ... } |
Remember that you need to hold onto your nodes if you don’t them to deallocate them immediately.
Back in configureCellDisplayWithCardInfo(cardInfo:)
, at the end of the Node Layer and Wrap Up Section, assign the containerNode
property to the containerNode
constant:
self.containerNode = containerNode |
Build and run. The blurred images will display once again! But there’s one final thing to change now that there is a new node hierarchy. Recall that during cell reuse you made the image node stop displaying. Now you need to make the entire node hierarchy stop displaying.
Handling Cell Reuse With the New Node Hierarchy
Staying in RainforestCardCell.swift, in prepareForReuse()
, instead of setting backgroundImageNode.preventOrCancelDisplay
, call recursiveSetPreventOrCancelDisplay(...)
on containerNode
and pass true
:
override func prepareForReuse() { super.prepareForReuse() // Replace this line... // backgroundImageNode?.preventOrCancelDisplay = true // ...with this line: containerNode?.recursiveSetPreventOrCancelDisplay(true) contentLayer?.removeFromSuperlayer() ... } |
Use recursiveSetPreventOrCancelDisplay()
when you need to cancel drawing for an entire node hierarchy. This method will set the preventOrCancelDisplay
property of the node and all of its children to either true
or false
.
Next, still in prepareForReuse()
, replace the line that sets backgroundImageNode
to nil
with a line that sets containerNode
to nil
:
override func prepareForReuse() { ... contentLayer = nil // Replace this line... // backgroundImageNode = nil // ...with this line: containerNode = nil } |
Remove RainforestCardCell
’s backgroundImageNode
stored property:
class RainforestCardCell: UICollectionViewCell { var featureImageSizeOptional: CGSize? var placeholderLayer: CALayer! // var backgroundImageNode: ASImageNode? ///< REMOVE THIS LINE var contentLayer: CALayer? var containerNode: ASDisplayNode? ... } |
Build and run. The app will function as before, but now you have the image node inside a container node and reuse working as it should.
Cell Contents
So far you have a node hierarchy, but there’s just one node in the container — the image node. Now it’s time to set up the node hierarchy to replicate the view hierarchy that you had before adding AsyncDisplayKit. This mean adding the text and the non-blurred, feature, image.
Adding the Feature Image
Here you’re going to add the feature image, which is the non-blurred image that’s displayed at the top of the card.
Open RainforestCardCell.swift and find configureCellDisplayWithCardInfo(cardInfo:)
. At the bottom of the Node Creation Section add the following code:
let featureImageNode = ASImageNode() featureImageNode.layerBacked = true featureImageNode.contentMode = .ScaleAspectFit featureImageNode.image = image |
This creates and configures a ASImageNode
constant called featureImageNode
. This is set up to be layer backed, scale to fit, and set to display the image, unblurred this time.
At the end of the Node Hierarchy Section, add featureImageNode
to containerNode
as a subnode:
containerNode.addSubnode(featureImageNode) |
You’re starting to fill the container with more nodes!
In the Node Layout Section, lay out featureImageNode
using FrameCalculator
:
featureImageNode.frame = FrameCalculator.frameForFeatureImage( featureImageSize: image.size, containerFrameWidth: containerNode.frame.size.width) |
Build and run. You’ll now see the feature image at the top of the card, on top of the blurred image. Notice how both the feature image and blurred image pop in at the same time. This is your shouldRasterizeDescendants
in action which you added earlier.
Adding the Title Text
Next up is adding the text labels, to display the name of the animal and the description. First up, the name of the animal.
Staying in configureCellDisplayWithCardInfo(cardInfo:)
, find the Node Creation Section. Add the following code at the end of this section, just after the creation of featureImageNode
:
let titleTextNode = ASTextNode() titleTextNode.layerBacked = true titleTextNode.backgroundColor = UIColor.clearColor() titleTextNode.attributedString = NSAttributedString.attributedStringForTitleText(cardInfo.name) |
This creates and configures an ASTextNode
constant called titleTextNode
.
ASTextNode
is another node subclass that ships with AsyncDisplayKit and which you use to display text. It’s a node based UILabel
effectively. It takes an attributed string, is backed by TextKit and has a lot of features like text links. To learn more about what you can do with this node subclass, head over to ASTextNode.h.
The starter project includes an extension on NSAttributedString
that provides factory methods to generate attributed strings for the title and description text that show up in the rainforest info card. The code above uses attributedStringForTitleText(...)
from this extension.
Now, at the end of the Node Hierarchy Section, add the following code:
containerNode.addSubnode(titleTextNode) |
This adds the titleTextNode
to the node hierarchy. It will be on top of both the feature image and the background image since it’s added after them.
At the end of the Node Layout Section add the following code:
titleTextNode.frame = FrameCalculator.frameForTitleText( containerBounds: containerNode.bounds, featureImageFrame: featureImageNode.frame) |
This lays out titleTextNode
with FrameCalculator
, just as you did with backgroundImageNode
and featureImageNode
.
Build and run. You now have the title being displayed on top of the feature image. Once again, the label is only rendered once the entire cell is ready to render.
Adding the Description Text
Adding a node with description text is much like adding a node with title text.
Back in configureCellDisplayWithCardInfo(cardInfo:)
, at the end of Node Creation Section, add the following code. Add it just after the creation of titleTextNode
which you added previously.
let descriptionTextNode = ASTextNode() descriptionTextNode.layerBacked = true descriptionTextNode.backgroundColor = UIColor.clearColor() descriptionTextNode.attributedString = NSAttributedString.attributedStringForDescriptionText(cardInfo.description) |
This creates and configures an ASTextNode
constant called descriptionTextNode
.
At the end of the Node Hierarchy Section, add descriptionTextNode
to containerNode
:
containerNode.addSubnode(descriptionTextNode) |
At the end of the Node Layout Section, lay out descriptionTextNode
using FrameCalculator
:
descriptionTextNode.frame = FrameCalculator.frameForDescriptionText( containerBounds: containerNode.bounds, featureImageFrame: featureImageNode.frame) |
Build and run. You’ll now see the description text as well.
Custom Node Subclasses
So far you’ve made use of ASImageNode
and ASTextNode
. These will take you a long way, but sometimes you need your own nodes just like you sometimes need your own views in traditional UIKit programming.
Creating a Gradient Node Class
Next up, you’ll use the Core Graphics code that’s in GradientView.swift to build a custom gradient display node. This will be used to create a custom node that draws a gradient. The gradient will be displayed at the bottom of the feature image to make the title stand out more.
Open Layers-Bridging-Header.h and add the following code:
#import <AsyncDisplayKit/_ASDisplayLayer.h> |
This step is necessary because this class isn’t included in the library’s umbrella header. You need access to this class when subclassing any ASDisplayNode
class and when subclassing _ASDisplayLayer
.
Click File\New\File…. Select iOS\Source\Cocoa Touch Class. Call the class GradientNode and make it a subclass of ASDisplayNode. Select Swift as the language and then click Next. Save the file and then open GradientNode.swift.
Add the following method to the class:
class func drawRect(bounds: CGRect, withParameters parameters: NSObjectProtocol!, isCancelled isCancelledBlock: asdisplaynode_iscancelled_block_t!, isRasterizing: Bool) { } |
Just as with UIView
or CALayer
, you can subclass ASDisplayNode
to do custom drawing. You can then use that drawing code to draw into either a UIView
’s layer or a stand-alone CALayer
, depending on how the client code configures the node. Check out ASDisplayNode+Subclasses.h for more information about subclassing ASDisplayNode
.
In addition, the ASDisplayNode
‘s drawing method takes more parameters than those on UIView
and CALayer
, offering you ways to write the method to do less work and be more efficient.
To fill your custom display nodes with content, you need to implement either drawRect(...)
or displayWithParameters(...)
from the _ASDisplayLayerDelegate
protocol. Before continuing, head over to _ASDisplayLayer.h for information about these methods and their parameters; search for _ASDisplayLayerDelegate
. Pay close attention to the header comments for drawRect(...)
.
Because the gradient that sits on top of the feature image is drawn using Core Graphics, you need to use drawRect(...)
for this custom node.
Open GradientView.swift and copy the contents of drawRect(...)
into GradientNode
’s drawRect(...)
in GradientNode.swift. The method will look like this:
class func drawRect(bounds: CGRect, withParameters parameters: NSObjectProtocol!, isCancelled isCancelledBlock: asdisplaynode_iscancelled_block_t!, isRasterizing: Bool) { let myContext = UIGraphicsGetCurrentContext() CGContextSaveGState(myContext) CGContextClipToRect(myContext, bounds) let componentCount: UInt = 2 let locations: [CGFloat] = [0.0, 1.0] let components: [CGFloat] = [0.0, 0.0, 0.0, 1.0, 0.0, 0.0, 0.0, 0.0] let myColorSpace = CGColorSpaceCreateDeviceRGB() let myGradient = CGGradientCreateWithColorComponents(myColorSpace, components, locations, componentCount) let myStartPoint = CGPoint(x: bounds.midX, y: bounds.maxY) let myEndPoint = CGPoint(x: bounds.midX, y: bounds.midY) CGContextDrawLinearGradient(myContext, myGradient, myStartPoint, myEndPoint, UInt32(kCGGradientDrawsAfterEndLocation)) CGContextRestoreGState(myContext) } |
Now delete GradientView.swift, and build to make sure everything compiles.
Adding the Gradient Node
Open RainforestCardCell.swift and find configureCellDisplayWithCardInfo(cardInfo:)
. At the bottom of the Node Creation Section, add the following code, just after the creation of descriptionTextNode
that you added earlier:
let gradientNode = GradientNode() gradientNode.opaque = false gradientNode.layerBacked = true |
This creates and configures a GradientNode
constant called gradientNode
.
In the Node Hierarchy Section, right below the line that adds featureImageNode
, add gradientNode
to containerNode
:
//MARK: Node Hierarchy Section containerNode.addSubnode(backgroundImageNode) containerNode.addSubnode(featureImageNode) containerNode.addSubnode(gradientNode) ///< ADD THIS LINE containerNode.addSubnode(titleTextNode) containerNode.addSubnode(descriptionTextNode) |
The gradient node needs to go here so that it’s above the feature image, but below the title.
Then add the following code at the bottom of the Node Layout Section:
gradientNode.frame = FrameCalculator.frameForGradient( featureImageFrame: featureImageNode.frame) |
Build and run. You’ll now see the gradient at the bottom of the feature image. The title certainly stands out more now!
The Popcorn Effect
As mentioned before, the cell’s node contents “pop” in when it’s finished drawing. This isn’t ideal. So let’s go ahead and fix that now. First up, some more diving into AsyncDisplayKit and how it works.
In the Container Node Creation Section within configureCellDisplayWithCardInfo(cardInfo:)
, turn off shouldRasterizeDescendants
for the container node:
containerNode.shouldRasterizeDescendants = false |
Build and run. You’ll notice that now the different nodes in the container hierarchy pop in one by one. You’ll see the text pop in, then the feature image, then the blurred background image.
With shouldRasterizeDescendants
turned off, instead of drawing into one container layer, AsyncDisplayKit creates a layer tree that mirrors the info card’s node hierarchy. Remember that the popcorn effect exists because each layer shows up immediately after it’s drawn and some layers take longer to draw than others.
That’s not what we want, but it illustrates how AsyncDisplayKit works. We don’t want that behaviour, so turn shouldRasterizeDescendants
back on:
containerNode.shouldRasterizeDescendants = true |
Build and run again. Back to the entire cell popping in once it’s finished rendering.
Time to rethink how to get rid of this popcorn effect. First up, let’s take a look at how nodes can be constructed in the background.
Constructing Nodes in the Background
In addition to drawing asynchronously, with AsyncDisplayKit, you can also create, configure and lay out nodes asynchronously. Take a deep breath, because that’s what you’re going to do next.
Creating a Node Construction Operation
You’re going to wrap up the construction of the node hierarchy into an NSOperation. This is a nice way to do it, since that operation can then be easily performed on any operation queue, including a background queue.
Open RainforestCardCell.swift. Then add the following method to the class:
func nodeConstructionOperationWithCardInfo(cardInfo: RainforestCardInfo, image: UIImage) -> NSOperation { let nodeConstructionOperation = NSBlockOperation() nodeConstructionOperation.addExecutionBlock { // TODO: Add node hierarchy construction } return nodeConstructionOperation } |
Drawing isn’t the only thing that can slow down the main thread. For complex screens, layouts can sometimes be expensive to compute. So far, with the current state of the tutorial project, a slow node layout causes the collection view to drop frames.
60 FPS means you have ~17 ms to get your cells ready to display, otherwise one or more frames will be dropped. It’s very common with table views and collection views which have complex cells, for frames to be dropped while scrolling because of this.
AsyncDisplayKit to the rescue!
You’ll use the nodeConstructionOperation
above to move all the node hierarchy construction and layout out of the main thread and onto a background NSOperationQueue
, further ensuring that the collection view will scroll as close to 60 FPS as possible.
Warning: You can access and set node properties in the background, but only before the node’s layer or view has been created, which happens when you access the node’s layer or view property for the first time.
Once the node’s layer or view has been created, you must access and set node properties on the main thread, because the node will forward those calls to its layer or view. If you get a crash with a log that says, ‘Incorrect display node thread affinity,’ this means you are attempting to get/set a node property in the background after its layer or view has been created.
Change the contents of the nodeConstructionOperation
operation block to the following:
nodeConstructionOperation.addExecutionBlock { [weak self, unowned nodeConstructionOperation] in if nodeConstructionOperation.cancelled { return } if let strongSelf = self { // TODO: Add node hierarchy construction } } |
By the time this operation runs, the cell may have been deallocated. In that case, there’s no need to do any work. Similarly, if the operation is canceled, then there is no work to do either.
An unowned reference to nodeConstructionOperation
is required to avoid a retain cycle between the operation and the execution closure.
Now find configureCellDisplayWithCardInfo(cardInfo:)
. Move everything after the Image Size Section in configureCellDisplayWithCardInfo(cardInfo:)
into nodeConstructionOperation
’s execution closure. Place the code inside the strongSelf
optional binding statement, where the TODO is. The configureCellDisplayWithCardInfo(cardInfo:)
method will now look like this:
func configureCellDisplayWithCardInfo(cardInfo: RainforestCardInfo) { //MARK: Image Size Section let image = UIImage(named: cardInfo.imageName)! featureImageSizeOptional = image.size } |
You’ll have a few compiler errors now. This is because self
in the operation block is a weak reference and therefore an optional. But you have a strong reference to self because the code is inside the optional binding statement. So replace the lines where the errors are with the following:
strongSelf.contentView.layer.addSublayer(containerNode.layer) strongSelf.contentLayer = containerNode.layer strongSelf.containerNode = containerNode |
Finally, add the following code underneath those three lines you just changed:
containerNode.setNeedsDisplay() |
Build to make sure everything compiles. If you run it right now, then only the placeholder will display, because the node creation operation is not actually being used yet. Let’s now add that.
Using the Node Creation Operation
Open RainforestCardCell.swift and add the following property to the class:
class RainforestCardCell: UICollectionViewCell { var featureImageSizeOptional: CGSize? var placeholderLayer: CALayer! var backgroundImageNode: ASImageNode? var contentLayer: CALayer? var containerNode: ASDisplayNode? var nodeConstructionOperation: NSOperation? ///< ADD THIS LINE ... } |
This adds an NSOperation
optional variable stored property called nodeConstructionOperation
.
You’ll use this property to cancel node construction when the cell is preparing to be recycled. This can happen when the user scrolls very quickly through the collection view, especially if it takes a while to calculate the layout.
In prepareForReuse()
add the code indicated below:
override func prepareForReuse() { super.prepareForReuse() // ADD FROM HERE... if let operation = nodeConstructionOperation { operation.cancel() } // ...TO HERE containerNode?.recursiveSetPreventOrCancelDisplay(true) contentLayer?.removeFromSuperlayer() contentLayer = nil containerNode = nil } |
This cancels the operation when the cell is reused, so that if the node creation hasn’t been done yet, it won’t be done.
Now find configureCellDisplayWithCardInfo(cardInfo:)
and add the code indicated below:
func configureCellDisplayWithCardInfo(cardInfo: RainforestCardInfo) { // ADD FROM HERE... if let oldNodeConstructionOperation = nodeConstructionOperation { oldNodeConstructionOperation.cancel() } // ...TO HERE //MARK: Image Size Section let image = UIImage(named: cardInfo.imageName)! featureImageSizeOptional = image.size } |
The cell now cancels any in-flight node construction operations when it prepares for reuse and when it starts configuring. This makes sure that the operation is canceled even if the cell is reconfigured before it’s been made ready for reuse.
Build to make sure everything compiles.
Running on the Main Thread
AsyncDisplayKit allows you to do lots of work off the main thread. But when it comes to interfacing with UIKit and CoreAnimation, you need to do all of this on the main thread still. So far, you’ve moved all node creation off the main thread. But there’s one thing that needs to be put back on the main thread — the setting up of the CoreAnimation layer hierarchy.
In RainforestCardCell.swift, go back to nodeConstructionOperationWithCardInfo(cardInfo:image:)
and replace the Node Layer and Wrap Up Section with the following:
// 1 dispatch_async(dispatch_get_main_queue()) { [weak nodeConstructionOperation] in if let strongNodeConstructionOperation = nodeConstructionOperation { // 2 if strongNodeConstructionOperation.cancelled { return } // 3 if strongSelf.nodeConstructionOperation !== strongNodeConstructionOperation { return } // 4 if containerNode.preventOrCancelDisplay { return } // 5 //MARK: Node Layer and Wrap Up Section strongSelf.contentView.layer.addSublayer(containerNode.layer) containerNode.setNeedsDisplay() strongSelf.contentLayer = containerNode.layer strongSelf.containerNode = containerNode } } |
Here’s what that does:
- Recall that layers are created when the node’s layer property is accessed for the first time. This is why you must run the Node Layer and Wrap Up Section on the main queue, since the code accesses the node’s layer.
- The operation is checked to see if it’s canceled before actually adding the layer to the hierarchy. It could be that the cell has been reused or reconfigured before the operation has finished, in which case you don’t want to actually add the layer.
- As a safeguard, there is an identity check to make sure the cell’s current
nodeConstructionOperation
is the sameNSOperation
instance that dispatched this closure. - Fast return if
containerNode
’spreventOrCancel
istrue
. If the construction operation finishes, but the node’s draw has been cancelled, you still don’t want the node’s layer to show up in the cell. - Finally, add the node’s layer to the hierarchy, which will create the layer if required.
Build to make sure everything compiles.
Starting the Node Creation Operation
You’ve still not actually created and started the operation. Let’s do that now.
Continuing in RainforestCardCell.swift, change the method signature of configureCellDisplayWithCardInfo(cardInfo:)
to the following:
func configureCellDisplayWithCardInfo( cardInfo: RainforestCardInfo, nodeConstructionQueue: NSOperationQueue) |
This adds a new parameter, nodeConstructionQueue
. This is an NSOperationQueue
which will be used to enqueue a node creation operation.
At the end of configureCellDisplayWithCardInfo(cardInfo:nodeConstructionQueue:)
, add the following code:
let newNodeConstructionOperation = nodeConstructionOperationWithCardInfo(cardInfo, image: image) nodeConstructionOperation = newNodeConstructionOperation nodeConstructionQueue.addOperation(newNodeConstructionOperation) |
This creates a node construction operation, assigns it to the cell’s nodeConstructionOperation
property, and adds it to the passed-in queue.
Finally, open RainforestViewController.swift. Add an initialized constant stored property called nodeConstructionQueue
to RainforestViewController
, like so:
class RainforestViewController: UICollectionViewController { let rainforestCardsInfo = getAllCardInfo() let nodeConstructionQueue = NSOperationQueue() ///< ADD THIS LINE ... } |
Next, in collectionView(collectionView:cellForItemAtIndexPath indexPath:)
, pass the view controller’s nodeConstructionQueue
into configureCellDisplayWithCardInfo(cardInfo:nodeConstructionQueue:)
:
cell.configureCellDisplayWithCardInfo(cardInfo, nodeConstructionQueue: nodeConstructionQueue) |
The cell will now create a new node construction operation and add it to the view controller’s operation queue to run concurrently. Remember that a new node hierarchy gets created every time a cell is dequeued. It’s not ideal, but it’s good enough. Check out ASRangeController
if you’d like to cache the nodes to be reused.
Whew, OK, now build and run! You’ll see the same as you did before, but now layout and rendering are being performed off the main thread. Neat! I bet you never thought you’d ever see a day where you could do that! This is the power of AsyncDisplayKit. You can move more and more things off the main thread that don’t need to be there. This leaves the main thread free to handle user interaction, keeping your app’s interaction buttery smooth.
Fading in the Cells
Now for the fun stuff. In this short section, you’ll learn how to:
- Back nodes with custom display layer subclasses;
- Leverage implicit animations to animate node layers.
This will enable you to remove the popcorn effect and finally bring a nice fade animation to the party.
Creating a New Layer Subclass
Click File\New\File…. Select iOS\Source\Cocoa Touch Class and click Next. Call the class AnimatedContentsDisplayLayer and make it a subclass of _ASDisplayLayer. Choose Swift as the language and click Next. Finally, save it and open AnimatedContentsDisplayLayer.swift.
Now add the following method to the class:
override func actionForKey(event: String!) -> CAAction! { if let action = super.actionForKey(event) { return action } if event == "contents" && contents == nil { let transition = CATransition() transition.duration = 0.6 transition.type = kCATransitionFade return transition } return nil } |
Layers have a contents property which tells the system what to draw for that layer. AsyncDisplayKit works by rendering the contents in the background before finally setting the contents on the main thread.
This code will add a transition animation so that the contents fade into view. You can find more information about implicit layer animations and CAAction
in Apple’s Core Animation Programming Guide.
Build to make sure everything compiles.
Fading in the Container Node
You’ve made a layer that will fade in its contents when its set, but now you need to use that layer.
Open RainforestCardCell.swift. Inside nodeConstructionOperationWithCardInfo(cardInfo:image:)
, at the beginning of the Container Node Creation Section, change the following line:
// REPLACE THIS LINE... // let containerNode = ASDisplayNode() // ...WITH THIS LINE: let containerNode = ASDisplayNode(layerClass: AnimatedContentsDisplayLayer.self) |
This tells the container node to use an AnimatedContentsDisplayLayer
instance for its backing layer, therefore opting-in to the fade-in animation that you added.
Note: Only subclasses of _ASDisplayLayer
can be drawn asynchronously.
Build and run. You’ll now see the container node fading in once it’s drawn.
Where to Go from Here?
Congratulations! You now have another tool at your disposal for when you need to build high-performing, scrolling user interfaces.
In this tutorial, you took a poorly-performing collection view and significantly improved its scrolling by replacing the view hierarchy with a rasterized AsyncDisplayKit node hierarchy. Exciting!
This is just one example of what you can do. AsyncDisplayKit holds out the promise of reaching levels of UI performance that would be difficult or impossible to achieve via optimizations within ordinary UIKit.
Realistically, to make the most of AsyncDisplayKit, you need a solid understanding of where the true performance bottlenecks are in the standard UIKit. One great thing about AsyncDisplayKit is it provokes us to probe those issues and think about how fast and responsive it is physically possible for our apps to be.
AsyncDisplayKit is a powerful tool for exploring this performance frontier. Use it wisely. And welcome to the cutting edge—the bleeding edge?—of super-responsive UIs.
This is only the beginning for AsyncDisplayKit! The authors and contributors are building new exciting features every day. Keep an eye out for ASCollectionView
and ASMultiplexImageNode
in version 1.1. Straight from the header, “ASMultiplexImageNode is an image node that can load and display multiple versions of an image. For example, it can display a low-resolution version of an image while the high-resolution version is loading.” Pretty cool :]
You can download the final Xcode project here.
The AsyncDisplayKit guide is here and the AsyncDisplayKit Github repo is here.
The library authors are looking for API design feedback. Make sure to share your thoughts in the Paper Engineering Community group on Facebook or even better you can get involved with the development of AsyncDisplayKit by contributing through pull requests on GitHub.
AsyncDisplayKit Tutorial: Achieving 60 FPS scrolling is a post from: Ray Wenderlich
The post AsyncDisplayKit Tutorial: Achieving 60 FPS scrolling appeared first on Ray Wenderlich.