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.
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.
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.
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.
The following diagram shows the structure of the app:
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
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.
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.
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.
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 = Dictionarylet 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.
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:
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.
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.