iOS Programming · · 20 min read

Using AsyncDisplayKit to Develop Responsive UIs in iOS

Using AsyncDisplayKit to Develop Responsive UIs in iOS

Let’s get back to 2011, when I have seen this brilliant guy called Mike Matas on Ted introducing this new way of reading books interactively with a polished user interface as well as this astonishing user experience. The app was ridiculously fluid that you cannot believe it is run on a mobile device. Later that year, the company Push Pop Press behind this app was acquired by Facebook to take full advantage of the tools created on their behalf and let billions of users have this great experience.

I’ve always been curious to use and contribute to libraries maintained by big companies, which requires a decent amount of time and effort from developers working on the same project for a long period of time.

logo

What is AsyncDisplayKit?

AsyncDisplayKit is an iOS framework developed in order to optimize your app by making user interfaces thread safe, which means that you will be able to shift all your expensive views into background threads, preparing them before presenting. It’s a great way to make your app elegant, smooth and has a responsive user interface.

AsyncDisplayKit offers multiple components to serve the need of your app.

  • ASDisplayNode – Counterpart to UIView.
  • ASControlNode – Analogous to UIControl.
  • ASImageNode – Load images asynchronously.
  • ASNetworkImageNode – A node with which you provide an NSURL to loads an image.
  • ASMultiplexImageNode – Load images asynchronously with its multiple versions.
  • ASTextNode – A UITextView replacement that can be used for labels and buttons as well.
  • ASCollectionView and ASTableView — UICollectionView and UITableView subclasses that support ASCellNode subclasses.

asyncdisplaykit-api-flow

Take a Look at Our App

In this tutorial, we’ll create a simple app called “BrowseMeetup” that consumes Meetup’s public API. If you haven’t heard of Meetup, it is the world’s largest network of local groups. You’re free to use Meetup to organize a local group or find one of the thousands already meeting up face-to-face. Like other social networks, it provides open API for accessing its data from your own apps.

The BrowseMeetup app consumes the web service of Meetup to look up for groups nearby. The app gets the current location and then automatically loads the nearby Meetup groups in a new optimized and responsive version using AsyncDispatchKit. We will cover some of the basic components of AsyncDispatchKit and use them in a way to light weight our app.

AsyncDisplayKit

The App Structure

Before diving into coding, I suggest you download the final project of the demo app. This would help you easily follow the following sections.

Note: Before going through this tutorial, I would strongly recommend you to go over our old tutorial on How To Fetch and Parse JSON Using iOS SDK. I also assume that you are familiar with Swift. if not, please check out our Swift tutorials to get familiar with the language.

The following diagram shows the structure of the app:

diagram-to-show-app-structure

View Controller, Table Node, Delegate and Data Source

In UIKit, data is usually presented using a table view. For AsyncDisplayKit, the basic display unit is the node. It is an abstraction over UIView. ASTableNode is the equivalent version of views. Most methods have a node equivalent. If you understand how to use view or table views, you will know how to use nodes.

Table nodes are highly optimized for performance, they are easy to use and to implement. We will use a table node for the list of groups.

A table node is mostly represented by ASViewController, which is also the data source and the delegate for the table node. This behavior often leads to a massive view controller because it’s doing too many works from managing the presentation of the data, presenting the view, and the navigation to other view controllers.

It’s clean to split up the responsibility into several classes. Therefore, we will use a helper class to act as the data source for the table node. The communication between the view controller and the helper class will be defined using a protocol. It’s a good practice in case we need to replace an implementation with a better version.

Table node cells

By looking into our app, the group list have a photo, location, date, organizer’s avatar, and the organizer’s name. The table node cells should only show the set data. We will be to accomplish this by implementing our custom table node cell.

Model

The model of the application consists of the group, the organizer, interactor, data manager, which allows the search for nearby groups. Therefore, the controller will ask the interactor for the groups to present. The data manager will be responsible for using meetup service as well as creating groups from the JSON response.

Beginners often tend to manage the model objects within the controller. Then, the controller has a reference to a collection of groups. This is not recommended because if we decide to change the service, we would have to change those functinalities within the controller. It’s difficult to keep an overview of such a class, and because of this reason, it is a source of bugs.

It’s much more easier to have an interface between the interface and the model objects because if we need to change how the model objects are managed, the controller can stay the same. We could even replace the complete model layer if we just keep the inetrface the same.

Our Development Strategy

In this tutorial, we will build the app from the inside out. We will start with the model, and then build the networking and controller. Obviously, this is not the only way to build apps. But by separating on the basis of layers instead of features, it is easier to follow and keep an overview of what is happening. When you later need to refresh your memory, the relevant information you need is easier to find.

Getting started with Xcode

Now, let’s start our journey by creating a new project that we will implement using AsyncDisplayKit.

Open Xcode and create a new iOS project using the Single View Application template. In the options window, add BrowseMeetup as the product name, select Swift as language, choose iPhone in the Device option.

To configure our project to use AsyncDisplayKit, select the file Main.Storyboard in the project navigator, then delete it. In the project navigator, open the AppDelegate.swift and replace the code available with the one shown below:

@UIApplicationMain
class AppDelegate: UIResponder, UIApplicationDelegate {

    var window: UIWindow?
    
    func application(_ application: UIApplication, didFinishLaunchingWithOptions launchOptions: [UIApplicationLaunchOptionsKey: Any]?) -> Bool {
        let window                      = UIWindow(frame: UIScreen.main.bounds)
        window.backgroundColor          = UIColor.white
        
        let feedVC  = MeetupFeedViewController()
        let feedNavCtrl = UINavigationController(rootViewController: feedVC)
        window.rootViewController  = feedNavCtrl
        
        window.makeKeyAndVisible()
        self.window = window
        
        return true
    }
}

In this method, we are simply initializing our main view controller using one of AsyncDisplayKit contrainers to not occur the common mistake by adding nodes directly to an existing view hierarchy. Doing this will virtually guarantee that your nodes will flash as they are rendered.

You cannot compile this code because Xcode cannot find MeetupFeedController. To add the file, first click the BrowseMeetup group in the Project Navigator. Go to File | New | File… navigate to iOS | Source | Swift File template, and click on next. In the Save As field, add the name MeetupFeedViewController.swift, and click on Create.

Open MeetupFeedViewController.swift in the editor and add the following code:

import AsyncDisplayKit

final class MeetupFeedViewController: ASViewController {
    
    var _tableNode: ASTableNode
    
    init() {
        _tableNode = ASTableNode()
        super.init(node: _tableNode)
                setupInitialState()
    }
    
    required init?(coder aDecoder: NSCoder) {
        fatalError("init(coder:) has not been implemented")
    }
}

Sometimes it’s nice to initialize a view controller in a simpler way. In the case of our MeetupFeedViewController, it will be using a designated initializer to specify ASViewController‘s node which is an ASTableNode. You’re new to ASTableNode, but it is actually an equivalent version to UIKit’s UITableView and can be used in place of any UITableView.

Build again. The project still cannot compile because your project has not bundled the AsyncDisplayKit.

Now close your Xcode project and open terminal. We will use CocoaPods to install the kit. Navigate to the location of your iOS project and run pod init command to create an empty-ish Podfile.

$ cd path/to/project
$ pod init
Note: If you’re not familiar with CocoaPods, you can take a look at this beginner guide.

Now edit the Podfile and then replace the code with the one shown below:

target 'BrowseMeetup' do
  use_frameworks!

  pod 'AsyncDisplayKit', ' 2.0'
end

Finally, run pod install to install the library. It will take a bit of time, depending on your network speed. When finished, open the generated BrowseMeetup.xcworkspace instead of the previous BrowseMeetup.xcodeproj to continue.

$ pod install
$ open BrowseMeetup.xcworkspace

Working with the Meetup APIs

Before you can use the Meetup APIs, you will first need to create an account on Meetup. Go to APIs Doc and clicking on the button “Request to join Meetup group”, then follow the onscreen procedures to sign up and join the group. Once registered, you will be able to use that group as a sandbox to test features of the API.

meetup-api-doc-signup

To access the Meetup APIs, you will need to have an API key. In the dashboard, click the API tab and you can reveal your own API key by clicking the lock button.

meetup-api-key

We’ll use one of the Meetup APIs (i.e. https://api.meetup.com/find/groups) for fetching the Meetup groups held at a certain location. The call allows developers to specify the location by using latitude and longitude. The Meetup dashboard provides you with a console for testing its APIs, just click Console and key in find/groups to have a test.

meetup-api-console

Here is a sample JSON response for the request (https://api.meetup.com/find/groups?&lat=51.509980&lon=-0.133700&page=1&key=1f5718c16a7fb3a5452f45193232):

[
    {
            score: 1,
            id: 10288002,
            name: "Virtual Java User Group",
            link: "https://www.meetup.com/virtualJUG/",
            urlname: "virtualJUG",
            description: "If you don't live near an active Java User Group, or just yearn for more high quality technical sessions, The Virtual JUG is for you! If you live on planet Earth you can join. Actually even if you don't you can still join! Our aim is to get the greatest minds and speakers of the Java industry giving talks and presentations for this community, in the form of webinars and JUG session streaming from JUG f2f meetups. If you're a Java enthusiast and you want to learn more about Java and surrounding technologies, join and see what we have to offer!

", created: 1379344850000, city: "London", country: "GB", localized_country_name: "United Kingdom", state: "17", join_mode: "open", visibility: "public", lat: 51.5, lon: -0.14, members: 10637, organizer: { id: 13374959, name: "Simon Maple", bio: "", photo: { id: 210505562, highres_link: "http://photos2.meetupstatic.com/photos/member/6/3/d/a/highres_210505562.jpeg", photo_link: "http://photos2.meetupstatic.com/photos/member/6/3/d/a/member_210505562.jpeg", thumb_link: "http://photos2.meetupstatic.com/photos/member/6/3/d/a/thumb_210505562.jpeg", type: "member", base_url: "http://photos2.meetupstatic.com" } }, who: "vJUGers", group_photo: { id: 454745514, highres_link: "http://photos4.meetupstatic.com/photos/event/1/5/8/a/highres_454745514.jpeg", photo_link: "http://photos4.meetupstatic.com/photos/event/1/5/8/a/600_454745514.jpeg", thumb_link: "http://photos4.meetupstatic.com/photos/event/1/5/8/a/thumb_454745514.jpeg", type: "event", base_url: "http://photos4.meetupstatic.com" }, key_photo: { id: 454577629, highres_link: "http://photos1.meetupstatic.com/photos/event/4/4/d/d/highres_454577629.jpeg", photo_link: "http://photos1.meetupstatic.com/photos/event/4/4/d/d/600_454577629.jpeg", thumb_link: "http://photos1.meetupstatic.com/photos/event/4/4/d/d/thumb_454577629.jpeg", type: "event", base_url: "http://photos1.meetupstatic.com" }, timezone: "Europe/London", next_event: { id: "235903314", name: "The JavaFX Ecosystem", yes_rsvp_count: 261, time: 1484154000000, utc_offset: 0 }, category: { id: 34, name: "Tech", shortname: "Tech", sort_name: "Tech" }, photos: [ { id: 454577629, highres_link: "http://photos1.meetupstatic.com/photos/event/4/4/d/d/highres_454577629.jpeg", photo_link: "http://photos1.meetupstatic.com/photos/event/4/4/d/d/600_454577629.jpeg", thumb_link: "http://photos1.meetupstatic.com/photos/event/4/4/d/d/thumb_454577629.jpeg", type: "event", base_url: "http://photos1.meetupstatic.com" }, { id: 454577652, highres_link: "http://photos1.meetupstatic.com/photos/event/4/4/f/4/highres_454577652.jpeg", photo_link: "http://photos1.meetupstatic.com/photos/event/4/4/f/4/600_454577652.jpeg", thumb_link: "http://photos1.meetupstatic.com/photos/event/4/4/f/4/thumb_454577652.jpeg", type: "event", base_url: "http://photos1.meetupstatic.com" }, { id: 454577660, highres_link: "http://photos1.meetupstatic.com/photos/event/4/4/f/c/highres_454577660.jpeg", photo_link: "http://photos1.meetupstatic.com/photos/event/4/4/f/c/600_454577660.jpeg", thumb_link: "http://photos1.meetupstatic.com/photos/event/4/4/f/c/thumb_454577660.jpeg", type: "event", base_url: "http://photos1.meetupstatic.com" }, { id: 454579647, highres_link: "http://photos4.meetupstatic.com/photos/event/4/c/b/f/highres_454579647.jpeg", photo_link: "http://photos2.meetupstatic.com/photos/event/4/c/b/f/600_454579647.jpeg", thumb_link: "http://photos2.meetupstatic.com/photos/event/4/c/b/f/thumb_454579647.jpeg", type: "event", base_url: "http://photos2.meetupstatic.com" } ] } ]

Implementing the Group Struct

Our BrowserMeetup app needs a model struct to store information for groups. To add a file for the implemetation code, open Project Navigator, add a Swift File with the name Group.swift. From the preview of our app shown previsously, we know that the date of creation, photo, city, country, and the organizer are required.

struct Group {
    let createdAt: Double!
    let photoUrl: URL!
    let city: String!
    let country: String!
    let organizer: Organizer!
    
    var timeInterval: String {
        let date = Date(timeIntervalSince1970: createdAt)
        let dateFormatter = DateFormatter()
        dateFormatter.dateStyle = .medium
        dateFormatter.timeStyle = .none
        
        return dateFormatter.string(from: date)
    }
}

Alongside with the structure, we are adding a helper method to format the time interval into a human readable date by creating a date formatter, and omitting the time.

This code does not compile because there is an unknown identifier called Organizer. To fix this issue, we will need to create an swift file with the name of Oganizer.swift and add the following the code to it.

struct Organizer {

}

Now, The code should compile without any issue.

Implemeting the Organizer Struct

In the previous section, we added a struct to hold information of the organizer. We will now add the required properties. OpenOrganizer.swift and update it like this:

struct Organizer {
    let name: String!
    let thumbUrl: URL!
}

Each organizer has a name and a URL of its thumbnail, so we create these properties to store the information.

Implemeting the MeetupService Class

As discussed earlier, we know how to use the Meetup API for finding nearby groups. The URL is https://api.meetup.com/find/groups and it takes three parameters: the latitude, longitude, and key (feel free to use the key illustrated in this example to test your code or use your own). The API returns a json response with all the neccessary information that we need for our app.

We will write a class called MeetupService to connect the API and handle the JSON parsing. Now, add a new Swift file to the project and name it MeetupService.swift. Insert the follwing code to it:

typealias JSONDictionary = Dictionary
let MEETUP_API_KEY = "1f5718c16a7fb3a5452f45193232"

final class MeetupService {
    
    var baseUrl: String = "https://api.meetup.com/"
    lazy var session: URLSession = URLSession.shared
    
    func fetchMeetupGroupInLocation(latitude: Double, longitude: Double, completion: @escaping (_ results: [JSONDictionary]?, _ error: Error?) -> ()) {
        guard let url = URL(string: "\(baseUrl)find/groups?&lat=\(latitude)&lon=\(longitude)&page=10&key=\(MEETUP_API_KEY)") else {
            fatalError()
        }
        
        session.dataTask(with: url) { (data, response, error) in
            DispatchQueue.main.async(execute: {
                do {
                    let results = try JSONSerialization.jsonObject(with: data!) as? [JSONDictionary]
                    completion(results, nil);
                    
                } catch let underlyingError {
                    completion(nil, underlyingError);
                }
            })
            }.resume()
    }
}

I will not go into the details of the code, as I assume you to have some experience with web service and JSON parsing. In brief, we utilize URLSession to make a call to the API and then send a request to the server. For the JSON response returned, we use JSONSerialization to parse the result, and finally pass it to the completion handler for further processing.

Implementing the LocationService Class

Using the meetup API requires us to work with CoreLocation for fetching coordinates. This is a little bit out of this scope of this tutorial so we won’t cover it but you can definitety check out this tutorial on How To Get the Current User Location or this book on how to work with Core Location. Now, add a new swift file called LocationService.swift and update it with the following code:

import Foundation
import CoreLocation

final class LocationService {
    
    var coordinate: CLLocationCoordinate2D? = CLLocationCoordinate2D(latitude: 51.509980, longitude: -0.133700)
}

Here we create an instance of CLLocationCoordinate2D, set up the latitude and longitude to specify London as our point of interest. For demo purpose, we just hardcode the coordinate.

Implemeting the DataManager Class

The MeetupBrowse app will show the nearby groups in a list. The list of groups will be managed by a class called MeetupFeedDataManager. Again, create a new swift file and let’s call it MeetupFeedDataManager.swift.

Add the following code to the file:

final class MeetupFeedDataManager {
    
    fileprivate var _meetupService: MeetupService?
    fileprivate var _locationService: LocationService?
    
    init(meetupService: MeetupService, locationService: LocationService) {
       _meetupService = meetupService
       _locationService = locationService
    }
    
    func searchForGroupNearby(completion: @escaping ( _ groups: [Group]?, _ error: Error?) -> ()) {
        let coordinate = _locationService?.coordinate
        
        _meetupService?.fetchMeetupGroupInLocation(latitude: coordinate!.latitude, longitude: coordinate!.longitude, completion: { (results, error) in
            guard error == nil else { completion(nil, error); return }
            
            let groups = results?.flatMap(self.groupItemFromJSONDictionary)
            completion(groups, nil)
        })
    }
}

In the case of MeetupFeedDataManager, it would be nice to provide an initializer that accepts a MeetupService and LocationService object so that we use dependency injection for better managed and more testable code.

Extracting Values from JSON

The searchForGroupNearby method provides the results of groups nearby by calling fetchMeetupGroupInLocation of our MeetupService class. For the result returned, we need to convert the JSON data to objects that are specific to the app’s domain, which is defined in the model classes we worked with earlier.

To convert from a JSON representation to a Group object, write a method named groupItemFromJSONDictionary that takes an JSONDictionary argument that extracts and transforms data from the JSON representation into properties. Add the following code to MeetupFeedDataManager.swift:

func groupItemFromJSONDictionary(_ entry: JSONDictionary) -> Group? {
        guard let created = entry["created"] as? Double, let city = entry["city"] as? String, let country = entry["country"] as? String, let keyPhoto = entry["key_photo"] as? JSONDictionary, let photoUrl = keyPhoto["photo_link"] as? String, let organizerJSON = entry["organizer"] as? JSONDictionary, let organizer = organizerItemFromJSONDictionary(organizerJSON) else {
            return nil
        }
        
        return Group(createdAt: created, photoUrl: URL(string: photoUrl), city: city, country: country, organizer: organizer)
    }

In the example above, each of the values are extracted into constants from the given JSON dictionary using optional binding and the as? type casting operator. And then we use the values to create the Group object.

The code demonsonstrated above isn’t compiling in reason of an unknown method called organizerItemFromJSONDictionary, which is used to extract the organizer item. To fix this issue, add the code below to the same class:

 
func organizerItemFromJSONDictionary(_ entry: JSONDictionary) -> Organizer? {
    guard let name = entry["name"] as? String, let photo = entry["photo"] as? JSONDictionary, let thumbUrl = photo["thumb_link"] as? String else {
        return nil
    }

    return Organizer(name: name, thumbUrl: URL(string: thumbUrl))
}

Swift’s built-in language features make it easy to safely extract and work with JSON data decoded with Foundation APIs without the need for an external library or framework.

Implementing the Interactor Class

Now that we have implemented the class for parsing JSON data, we will create another class for handling the business logic related to the data (Entities) or networking, like creating new instances of entities or fetching them from the server.

Create a new swift file called MeetupFeedInteractorIO.swift and add the following code:

protocol MeetupFeedInteractorInput {
    func findGroupItemsNearby ()
}

protocol MeetupFeedInteractorOutput {
    func foundGroupItems (_ groups: [Group]?, error: Error?)
}

These protocols are for handling the user input as well as for handing content for display. This separation also conforms to the Single Responsibility Principle. The primary use case for our sample app is to show the neaby groups. Below is the corresponding implementation for MeetupFeedInteractor.swift:

final class MeetupFeedInteractor: MeetupFeedInteractorInput {
    
    var dataManager: MeetupFeedDataManager?
    
    var output: MeetupFeedInteractorOutput?
    
    func findGroupItemsNearby() {
        dataManager?.searchForGroupNearby(completion: output!.foundGroupItems)
    }
}

Now, our class conforms to the protocol MeetupFeedInteractorInput to gather input from user interactions so it can update the UI from the result retrieved from the method findGroupItemsNearby.

Implementing MeetupFeedViewController

Let’s continue to implement the MeetupFeedViewController class which is used for displaying the nearby groups. It is the first view that a user sees when the app has started.

Select the file MeetupFeedViewController.swift from the Project Navigator and add the viewDidLoad method like this:

override func viewDidLoad() {
        super.viewDidLoad()
        _activityIndicatorView = UIActivityIndicatorView(activityIndicatorStyle: .gray)
        _activityIndicatorView.hidesWhenStopped = true
        _activityIndicatorView.sizeToFit()
        
        var refreshRect = _activityIndicatorView.frame
        refreshRect.origin = CGPoint(x: (view.bounds.size.width - _activityIndicatorView.frame.width) / 2.0, y: _activityIndicatorView.frame.midY)
        
        _activityIndicatorView.frame = refreshRect
        view.addSubview(_activityIndicatorView)
        
        _tableNode.view.allowsSelection = false
        _tableNode.view.separatorStyle = UITableViewCellSeparatorStyle.none
        
        _activityIndicatorView.startAnimating()
        handler?.findGroupItemsNearby()
    }

Also, declare both handler and _activityIndicatorView properties in the class:

var handler: MeetupFeedInteractorInput?
var _activityIndicatorView: UIActivityIndicatorView!

The code above create a new instance of UIActivityIndicatorView and add as a subview so as to show a spinner to table view while data is loading. We also disable the interaction for now as our app has just a single view controller. Then, we request the nearby groups by using the handler MeetupFeedInteractor.

Next, we will create a method to initialize the state of the view controller. In the method, we will define the data provider and data source of the controller. In the next step, we will create a class called MeetupFeedTableDataProvider for handling the data. For now, just add the method first:

    
func setupInitialState() {
    title = "Browse Meetup"
    
    _dataProvider = MeetupFeedTableDataProvider()
    _dataProvider._tableNode = _tableNode
    _tableNode.dataSource = _dataProvider
}

Also, declare a property for holding the data provider:

var _dataProvider: MeetupFeedTableDataProvider!

Recalled that we have declared a protocol called MeetupFeedInteractorOutput, its method is called in the MeetupFeedInteractor class:

func findGroupItemsNearby() {
    dataManager?.searchForGroupNearby(completion: output!.foundGroupItems)
}

However, we haven’t implemented the required method (i.e. foundGroupItems) so far. Now we will implement it in the MeetupFeedViewController class. Therefore, change the class declaration like this:

final class MeetupFeedViewController: ASViewController, MeetupFeedInteractorOutput

And implement the method like this:

func foundGroupItems(_ groups: [Group]?, error: Error?) {
    guard error == nil else { return }
    
    _dataProvider.insertNewGroupsInTableView(groups!)
    _activityIndicatorView.stopAnimating()
}

When the method is called, we process the groups and insert them into the table view. We are using the method insertNewGroupsInTableView implemented in the data provided class which we will explain in the upcoming sections to add the entries to the table node. Stopping the activity indicator is also a good move at this stage as we don’t need it anymore.

Implementing MeetupFeedTableDataProvider

In the previous section, we created a class to act as the data source for the group list table node. In this section we will implement its properties and methods.

Create a new file MeetupFeedTableDataProvider.swift and update it like this:

import Foundation
import AsyncDisplayKit

class MeetupFeedTableDataProvider: NSObject, ASTableDataSource {
    
    var _groups: [Group]?
    weak var _tableNode: ASTableNode?
    
    ///--------------------------------------
    // MARK - Table data source
    ///--------------------------------------
    
    func tableNode(_ tableNode: ASTableNode, numberOfRowsInSection section: Int) -> Int {
        return _groups?.count ?? 0
    }
    
    func tableNode(_ tableNode: ASTableNode, nodeBlockForRowAt indexPath: IndexPath) -> ASCellNodeBlock {
        
        let group = _groups![indexPath.row]
        let cellNodeBlock = { () -> ASCellNode in
            return GroupCellNode(group: group)
        }
        return cellNodeBlock
    }
    
    ///--------------------------------------
    // MARK - Helper Methods
    ///--------------------------------------
    
    func insertNewGroupsInTableView(_ groups: [Group]) {
        _groups = groups
        
        let section = 0
        var indexPaths = [IndexPath]()
        groups.enumerated().forEach { (row, group) in
            let path = IndexPath(row: row, section: section)
            indexPaths.append(path)
        }
        _tableNode?.insertRows(at: indexPaths, with: .none)
    }
}

As mentioned before, to work with table views in AsyncDisplayKit, we have to implement the ASTableDataSource protocol. Here we create a separate class that implements the protocol and serves as the data provider.

The methods are very similar to UITableViewDataSource if you’re familiar with it. In the first method, we return the total number of groups for display in the table view.

For the tableNode(_:nodeForRowAtIndexPath:) method, we obtain the specific group object for the given row, using indexPath.row. Then, we create a Group cell node which is an abstraction of ASCellNode. Finally, we hook the Group object with the cell and return it.

It is recommended that you use the node block version of these methods so that your collection node will be able to prepare and display all of its cells concurrently. This means that all subnode initialization methods can be run in the background.

The last thing we need to implement in MeetupFeedTableDataProvider is the insertNewGroupsInTableView method that is responsible to insert the groups into our table node. Here we insert the rows by calling the insertRows method of the table node. Please note that the method must be called from the main thread.

Implementing GroupCell

If you’ve worked with custom table cells before, you know we need to create a custom class for the table cell. Similarly, when using AsyncDisplayKit, we create a custom cell node that extends from ASCellNode for presenting custom data. Here we will create a custom class called GroupCellNode that holds both labels and images.

Again create a new file and name it GroupCellNode. Update its content like this:

import AsyncDisplayKit

fileprivate let SmallFontSize: CGFloat = 12
fileprivate let FontSize: CGFloat = 12
fileprivate let OrganizerImageSize: CGFloat = 30
fileprivate let HorizontalBuffer: CGFloat = 10

final class GroupCellNode: ASCellNode {
    
    fileprivate var _organizerAvatarImageView: ASNetworkImageNode!
    fileprivate var _organizerNameLabel: ASTextNode!
    fileprivate var _locationLabel: ASTextNode!
    fileprivate var _timeIntervalSincePostLabel: ASTextNode!
    fileprivate var _photoImageView: ASNetworkImageNode!
    
    init(group: Group) {
        super.init()
        
        _organizerAvatarImageView = ASNetworkImageNode()
        _organizerAvatarImageView.cornerRadius = OrganizerImageSize/2
        _organizerAvatarImageView.clipsToBounds = true
        _organizerAvatarImageView?.url = group.organizer.thumbUrl
        
        
        _organizerNameLabel = createLayerBackedTextNode(attributedString: NSAttributedString(string: group.organizer.name, attributes: [NSFontAttributeName: UIFont(name: "Avenir-Medium", size: FontSize)!, NSForegroundColorAttributeName: UIColor.darkGray]))
        
        let location = "\(group.city!), \(group.country!)"
        _locationLabel = createLayerBackedTextNode(attributedString: NSAttributedString(string: location, attributes: [NSFontAttributeName: UIFont(name: "Avenir-Medium", size: SmallFontSize)!, NSForegroundColorAttributeName: UIColor.blue]))
        
        _timeIntervalSincePostLabel = createLayerBackedTextNode(attributedString: NSAttributedString(string: group.timeInterval, attributes: [NSFontAttributeName: UIFont(name: "Avenir-Medium", size: FontSize)!, NSForegroundColorAttributeName: UIColor.lightGray]))
        
        _photoImageView = ASNetworkImageNode()
        _photoImageView?.url = group.photoUrl
        
        automaticallyManagesSubnodes = true
    }
    
    fileprivate func createLayerBackedTextNode(attributedString: NSAttributedString) -> ASTextNode {
        let textNode = ASTextNode()
        textNode.isLayerBacked = true
        textNode.attributedText = attributedString
        
        return textNode
    }
}

The node will download and display the thumbnail of the meetup group. AsyncDisplayKit comes with a class called ASNetworkImageNode, that can download and display a remote image. All you have to do is specify the url property with the appropriate URL of the image. And the image will be asynchonously loaded and concurrently rendered.

For text, we use ASTextNode to handle the rendering. A text node is very similar to UILabel you normally work with. It includes full rich text support and is a subclass of ASControlNode.

The helper method (i.e. createLayerBackedTextNode) is a way to remove duplicate code when creating labels in this example.

AsyncDispatchKit’s auto Layout is based on the CSS Box Model. Compared to UIKit’s layout constraints, it’s much more efficient, easier to debug, explicit, structured, compose complex and reusable layouts.

Now insert the layout method in the class:

override func layoutSpecThatFits(_ constrainedSize: ASSizeRange) -> ASLayoutSpec {
        
        _locationLabel.style.flexShrink = 1.0
        _organizerNameLabel.style.flexShrink = 1.0
        
        let headerSubStack = ASStackLayoutSpec.vertical()
        headerSubStack.children = [_organizerNameLabel, _locationLabel]
    
        _organizerAvatarImageView.style.preferredSize = CGSize(width: OrganizerImageSize, height: OrganizerImageSize)
        
        let spacer = ASLayoutSpec()
        spacer.style.flexGrow = 1.0
        
        let avatarInsets = UIEdgeInsets(top: HorizontalBuffer, left: 0, bottom: HorizontalBuffer, right: HorizontalBuffer)
        let avatarInset = ASInsetLayoutSpec(insets: avatarInsets, child: _organizerAvatarImageView)
        
        let headerStack = ASStackLayoutSpec.horizontal()
        headerStack.alignItems = ASStackLayoutAlignItems.center
        headerStack.justifyContent = ASStackLayoutJustifyContent.start
        headerStack.children = [avatarInset, headerSubStack, spacer, _timeIntervalSincePostLabel]
        
        let headerInsets = UIEdgeInsets(top: 0, left: HorizontalBuffer, bottom: 0, right: HorizontalBuffer)
        let headerWithInset = ASInsetLayoutSpec(insets: headerInsets, child: headerStack)
        
        let cellWidth = constrainedSize.max.width
        
        _photoImageView.style.preferredSize = CGSize(width: cellWidth, height: cellWidth)
        let photoImageViewAbsolute = ASAbsoluteLayoutSpec(children: [_photoImageView]) //ASStaticLayoutSpec(children: [_photoImageView])
        
        let verticalStack = ASStackLayoutSpec.vertical()
        verticalStack.alignItems = ASStackLayoutAlignItems.stretch
        verticalStack.children = [headerWithInset, photoImageViewAbsolute]
        
        return verticalStack
    }

The code above is using the most powerful layout spec called ASStackLayoutSpec. It includes tons of properties to fit your requirement and achieve the exact result wanted. In parallel, we use ASInsetLayoutSpec to add some padding.

In brief, the code above represents a Vertical Stack that contains two nodes and another Horizontal Stack at the top which itself holds another three nodes to represent the organizer’s photo, organizer’s name and location of the group. Finally, we wrap the entire nodes in a vertical ASStackLayoutSpec and return it.

ASLayoutSpec is used as a spacer.

Further information: You can refer to the official documentation to learn more about auto layout in AsyncDisplayKit.

Putting it all together

In the previous sections, we implemented the different parts of our app using AsyncDisplayKit. Now, it’s time to put them together to develop the complete app.

Go to project navigator and select AppDelegate.swift. Update application(:didFinishLaunchingWithOptions:) as shown below:

func application(_ application: UIApplication, didFinishLaunchingWithOptions launchOptions: [UIApplicationLaunchOptionsKey: Any]?) -> Bool {
    let window                      = UIWindow(frame: UIScreen.main.bounds)
    window.backgroundColor          = UIColor.white
    
    let feedVC  = MeetupFeedViewController()
    let locationService = LocationService()
    let meetupService = MeetupService()
    
    let dataManager = MeetupFeedDataManager(meetupService: meetupService, locationService: locationService)
    let interactor = MeetupFeedInteractor()
    interactor.dataManager = dataManager
    interactor.output = feedVC
    
    feedVC.handler = interactor
    
    let feedNavCtrl = UINavigationController(rootViewController: feedVC)
    window.rootViewController  = feedNavCtrl
    
    window.makeKeyAndVisible()
    self.window = window
    
    return true
}

We add a few lines of code to instantiate the feed view controller, data manager and interactor. Now you can run your app to have a test. Your app should be able to load the meetup groups from meetup.com. However, it fails to display the images. If you look into the console, you will see an error:

App Transport Security has blocked a cleartext HTTP (http://) 

App Transport Security (ATS) was first introduced in iOS 9 and it requires apps to use secure network connections over HTTPS by default. If your app tries to access a remote resource via HTTP, you will see the above error.

Disable ATS

To resolve the issue, you can disable ATS by editing the Info.plist file. Select Info.plist in project navigator and edit the file like this:

meetup-api-ats

You can set the Allow Arbitrary Loads option to YES to disable ATS. After that, you can test the app again. This time, you should see the group images.

meetup-api-demo

Summary

Congratulation, You’ve finished the app! In this tutorial, I have walked you the basics of AsyncDispatchKit. You should now know how to integrate AsyncDispatchKit in your app to have a responsive user interface. For further information, I encourage you to check out the official documentation.

For reference, you can download the complete project on GitHub.

What do you think about the tutorial and AsyncDispatchKit? Let me know if you would like me to cover more components about this astonishing framework.

Read next