Using AsyncDisplayKit to Develop Responsive Response in iOS
In 2011, I met a very smart guy named Mike Matas on Ted. He describes a new way to enhance the user experience used in e-books, creating an amazing user experience. The smoothness of this App is incredible for a mobile App. Later that year, the company that owned the App was acquired by Facebook and used the technology in its products, enabling hundreds of millions of users to have this great experience.
I’ve always been curious about this library that is used and maintained by “big companies” and requires a lot of time and concerted effort from all the developers on the project.
What is AsyncDisplayKit?
AsyncDisplayKit is an iOS framework that aims to make the user interface of your app thread-safe, i.e. allow you to put all the “expensive” preparation of views into background threads before they are displayed. This will give your app a more elegant, smooth, and responsive UI.
The AsyncDisplayKit provides the following components for you to create apps.
- ASDisplayNode – similar to UIView.
- ASControlNode – Similar to UIControl.
- ASImageNode – Load images asynchronously.
- ASNetworkImageNode – a “node” object. Give it an NSURL and it will load the image for you.
- ASMultiplexImageNode – can load multiple versions of images asynchronously.
- ASTextNode – a replacement for UITextView, used for label and button.
- ASCollectionView and ASTableView – Subclasses of UICollectionView and UITableView, supporting subclasses of ASCellNode.
Sample App overview
In this tutorial, we will create a simple app called “BrowseMeetup” that will use Meetup’s public API. If you don’t know Meetup, all you need to know is that it’s the largest network of local people in the world. You can use Meetup for free to start a local group, or find one of the thousands of existing groups where you want to meet people face to face. Like other social networks, it provides a public API that can be accessed from within the app.
BrowseMeetup uses Meetup’s Web service to find the nearest group. The App will get the current coordinates and automatically load the nearby Meetup groups, and use AsyncDisplayKit for optimization and responsive design. We will introduce some basic concepts of AsyncDisplayKit and use these concepts to lightweight app design.
The structure of the App
Before you start coding, I recommend that you download the final project of your app. This will help you understand what follows.
[ecko_alert color= “gray”] Note: Before reading this tutorial, I strongly recommend that you take a look at one of our previous tutorials on how to grab and parse JSON data with the iOS SDK. I’m assuming you’re familiar with Swift. If not, read our Swift tutorial to familiarize yourself with the language. [/ecko_alert]
The following diagram shows the structure of the app:
View Controller, Table node, agent, and data source
In UIKit, data is often displayed using a Table View. For AsyncDisplayKit, the basic display unit is “node”. It’s an abstraction that sits on top of UIView. ASTableNode is like some kind of UIView. Most of its methods have a “version” of a node. If you’re familiar with UIView or Table View, you know how to use nodes.
The Table node is highly optimized for performance and is very easy to use and implement. We will use the Table node for the group list.
A Table node is usually used in conjunction with an ASViewController, which often acts as the data source and proxy for the former. This tends to cause the View Controller to swell because it has too many things to do, including presenting data, displaying views, and navigating to other View Controllers.
Obviously, this work should be divided among multiple classes. Therefore, we will use a helper class to take care of the data source for the Table node. The interaction between the View Controller and the helper class takes place through a protocol. This is good practice and maybe we’ll find a better way to implement it later.
The Table node cell
Take a look at our app. The group list has a picture, location, date, the organizer’s profile picture, and the organizer’s name. The cell of the Table node should only need to display this data. We’ll implement it with a custom Table node cell.
model
The App’s model includes groups, organizers, interaction objects, and a data manager that allows you to search nearby groups. At the same time, the controller queries the group’s interaction objects for display. The data manager is responsible for using the Meetup service while creating JSON objects as group objects.
Newbies always manage model objects in controllers. In this way, a group collection is referred to in the controller. This is not recommended because if we were to change the service, we would have to change the functionality in the controller. It’s hard to remember such classes, so this is a Bug trigger.
It would be easier to add a layer of interface between the interface and the model objects so that if we need to change the way the model objects are managed, the controller stays the same. You can even replace the entire model layer if the interface doesn’t need to change.
Our development strategy
In this tutorial, we create the app from the inside out. You start with the model, and then you write the network and the controller. Obviously that’s not the only way to write an app. We split the app by layer, not by function, because it’s easier to move on and always remember what you’re about to do. You’re more likely to recall the information you need when you need to refresh your memory later.
Starting from the Xcode
Now, we’re going to start our journey with a new project that we’re going to use AsyncDisplayKit for.
Open Xcode and create a new iOS project using the Single View Application template. In the options window, set Product Name to BrowseMeetup, language to Swift, Device to iPhone.
To configure the project to use AsyncDisplayKit, select Main.storyboard in the project navigator and remove it. In the project navigator, open AppDelegate.swift and replace the code with:
@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 simply initialize an AsyncDisplayKit container as our main View Controller, so that you don’t directly add nodes to the existing View tree. This is important because it allows the nodes to refresh at render time.
The code will not compile because Xcode does not yet know MeetupFeeController. You need to create this file by clicking the BrowseMeetup group in the project navigator. Points to open the menu File | New | File… File, select the iOS | Source | Swift template, then click next. In the Save As column, fill in the name of the class MeetupFeedViewController. Swift, click on the Create.
Open the MeetupFeedViewController. Swift to write 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 have a simpler way of initializing a View Controller. Our MeetupFeedViewController, for example, uses a designated initialization function that initializes an ASViewController and specifies its node as an ASTableNode. You probably don’t know what ASTableNode is, but just think of it as a UITableView of UIKit, just think of it as a UITableView.
Compile again. The compilation still fails because the AsyncDisplayKit has not been imported into your project.
Close Xcode and open terminal. We use CocoaPods to install. Switch to the iOS project directory and run pod init to create an empty Podfile.
$ cd path/to/project $ pod init
[ecko_alert class= “green”] Note: If you’re not familiar with CocoaPods, please read this guide. [/ecko_alert]
Now to write your Podfile, add the following to the file:
target 'BrowseMeetup' do use_frameworks! Pod 'AsyncDisplayKit', '2.0' end
Then, run Pod Install to install the framework. Installation can take a few minutes, depending on your Internet speed. When installed, open the newly generated Browsemeetup.xcWorkspace instead of browsemeetup.xcodeProj.
$ pod install $ open BrowseMeetup.xcworkspace
Use Meetup APIs
To use the Meetup API, you need to have a Meetup account. Go to APIs Doc, click on the “Request to Join Meetup Group” button, follow the on-screen prompts to register and join a group. Once registered, you can use that group to sandbox the API.
To be able to access the Meetup APIs, you need to have an API key. In Dashboard, click on the API TAB and you can click on the little lock icon to see your API key.
We will use a Meetup APIs (namely api.meetup.com/find/groups) near to search a coordinate Meetup group. To use it, the developer needs to specify a latitude and longitude coordinate. Meetup’s dashboard provides a Console that allows you to test the APIs on the Console. Click Console and type find/ Groups to try it out.
For example, if the request api.meetup.com/find/groups… , you get a response in JSON format:
[ { 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" } ] } ]
Implement Group structure
Our BrowserMeetup app needs a model for holding group information. This requires creating a new file for writing implementation code. Open the project navigator and add a Swift file called group.swift. As we know from the previous app screenshots, it needs to store the creation date, photo, city, country, and creator.
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) } }
In the structure definition, we added a helper method to convert the time interval to a human-readable date type via the date formatter, taking only the date part and ignoring the time part.
This code cannot compile because the Organizer type is unknown. To solve this problem, we need to create another Swift file named Organizer. Swift. And edit its contents into the following code.
struct Organizer { }
Now, you can compile without a hitch.
Implement the Organizer structure
In the previous section, we used a structure to hold creator information. Next you need to add some attributes to it. Open OpenOrganizer. Swift and edit to the following:
struct Organizer { let name: String! let thumbUrl: URL! }
Each creator has a name and avatar URL, so we created two properties to hold them.
Implement MeetupService class
As I said, we know how to use the Meetup API to find nearby groups. Namely the URL api.meetup.com/find/groups, it needs three parameters: latitude, longitude and keyword (you can use a key word in this case, your keyword can also be used to test your code). This API returns a JSON object that contains the information our app needs.
We’ll create a MeetupService class to connect to the API and do JSON parsing. Now add a new Swift file meetupservice.swift. Write the following code in it:
typealias JSONDictionary = Dictionary<String, Any>
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(a)
}
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 letunderlyingError { completion(nil, underlyingError); }})
}.resume(a)}}Copy the code
I won’t go into detail here, assuming you’re familiar with Web services and JSON parsing. Simply put, we use a URLSession to call the Web API and request data from the server. The returned JSON data is parsed using JSONSerialization and returned to the Completion block for processing.
Realize the LocationService class
To call the Meetup API we need to get a coordinate with CoreLocation. This section is a bit beyond the scope of this article, so we won’t cover it, but for How To use Core Location, you can refer To our tutorial How To Get the Current User Location or this book. Now, create a new Swift file called locationService. Swift and add the following code:
import Foundation import CoreLocation final class LocationService { var coordinate: CLLocationCoordinate2D? CLLocationCoordinate2D(latitude: 51.509980, longitude: -0.133700)}
Here we declare an instance of CLLocationCoordinate2D and set latitude and Longitude to some coordinate in London that is our point of interest. For demonstration purposes, we hard-coded this coordinate.
Realize the DataManager class
The MeetupBrowse app displays a list of nearby groups. This list is managed by the MeetupFeedDataManager class. To create a new Swift file, called MeetupFeedDataManager. Swift.
Edit this file as follows:
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 MeetupFeedDataManager, it is a good idea to provide an initialization method that accepts MeetupService and LocationService objects. This is called dependency injection, and this design pattern makes our classes easier to manage and test.
Extract data from JSON
SearchForGroupNearby method call MeetupService fetchMeetupGroupInLoaction method to get the latest group list. These results need to be converted from JSON format to some object in the APP domain, which is the model class we announced earlier.
To convert a JSON object into Group object, you need to write a groupItemFromJSONDictionary method, this method using a JSONDictionary object as parameters, and the values in the JSON object extraction to the Group in the object’s properties. In MeetupFeedDataManager. Swift to add the following code:
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) }
Here, each value in the JSONDictionary is used with nullable bindings and as? The mode of the conversion operation is extracted into the constant. These values are then used to create a Group object.
The above code can’t compile because organizerItemFromJSONDictionary method is unknown. To solve this problem, add the following code 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 decode JSON data and extract values using the Foundation API, without the need for any third-party frameworks or libraries.
Implement Interactor class
Now that we have implemented JSON data profiling, we need to create another class to handle business logic related to data (entities) or the network, such as creating new entity instances or fetching entities from the server.
Create a Swift file, called MeetupFeedInteractorIO. Swift, add the following code:
protocol MeetupFeedInteractorInput { func findGroupItemsNearby () } protocol MeetupFeedInteractorOutput { func foundGroupItems (_ groups: [Group]? , error: Error?) }
These protocols are used to process user input as well as what needs to be displayed. This separation is based on the principle of single responsibility. In our app this is mostly about displaying nearby groups. Below is the MeetupFeedInteractor. Swift implementation of:
final class MeetupFeedInteractor: MeetupFeedInteractorInput { var dataManager: MeetupFeedDataManager? var output: MeetupFeedInteractorOutput? func findGroupItemsNearby() { dataManager? .searchForGroupNearby(completion: output! .foundGroupItems) } }
Now, our class USES MeetupFeedInteractorInput agreement collecting input from the user interaction, so that when it after findGroupItemsNearby obtained results will be redrawn the UI.
Implement MeetupFeedViewController
Let’s go ahead and implement the MeetupFeedViewController class, which is responsible for displaying nearby groups. It is also the first view the user sees after the app launches.
In the project navigator to open the MeetupFeedViewController. Swift, amend the viewDidLoad method to 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. The size, width - _activityIndicatorView. Frame. The 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, you need to declare handler and _activityIndicatorView properties in the class:
var handler: MeetupFeedInteractorInput? var _activityIndicatorView: UIActivityIndicatorView!
We declare a new UIActivityIndicatorView object and add it to the SubView to display a rotating little chrysanthemum during data loading. Interaction is also disabled because the app only has one View Controller. Then, a handler (MeetupFeedInteractor) is used to find nearby groups.
Then, we’ll create a method to initialize the state of the View Controller. In this method, we define the controller’s data provider and data source. In the back, we will create a MeetupFeedTableDataProvider class to process the data. Now, let’s write this method:
func setupInitialState() { title = "Browse Meetup" _dataProvider = MeetupFeedTableDataProvider() _dataProvider._tableNode = _tableNode _tableNode.dataSource = _dataProvider }
We also need to declare a property to use as our data provider:
var _dataProvider: MeetupFeedTableDataProvider!
Remember we used to define a MeetupFeedInteractorOutput agreement? This protocol method is called in MeetupFeedInteractor:
func findGroupItemsNearby() { dataManager? .searchForGroupNearby(completion: output! .foundGroupItems) }
However, we haven’t implemented this method (foundGroupItems) yet. We implement it in the MeetupFeedViewController class. Therefore, change the definition of the class to:
final class MeetupFeedViewController: ASViewController, MeetupFeedInteractorOutput
Then implement this method:
func foundGroupItems(_ groups: [Group]? , error: Error?) { guard error == nil else { return } _dataProvider.insertNewGroupsInTableView(groups!) _activityIndicatorView.stopAnimating() }
When this method is called, we will process groups and insert it into the Table View. We use the data provider implementation insertNewGroupsInTableView method, this method will entity object access to Table node, explain later. We also need to stop the Activity Indicator because we don’t need it anymore.
Implement MeetupFeedTableDataProvider
In the previous section, we created a class that acts as a data source for the Table node. In this section, we implement its properties and methods.
Create a new file MeetupFeedTableDataProvider. Swift, modify its content as follows:
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 earlier, to use the Table View in AsyncDisplayKit, we must implement the ASTableDataSource protocol. Here, we create a new class that implements this protocol and serves as the data provider.
These methods are very similar to the UITableViewDataSource protocol methods you’re familiar with. In the first method, we return the total number of groups that need to be displayed in the Table View.
In the tableNode(_:nodeForRowAtIndexPath:) method, we get the Group object corresponding to the row specified by indepath.row. Then, a GroupCellNode is created, which is an abstraction of an ASCellNode. Finally, we bind the Group object to the cell and return it.
It is recommended that you use the node-block versions of these methods so that your collection node can prepare and display its cells synchronously. This also means that all child node initialization methods can be performed in the background.
In MeetupFeedTableDataProvider insertNewGroupsInTableView method, is used to group is inserted into the Table node. Here, we call the insertRows method on the Table node to insertRows. Note that this method must be called from the main thread.
Implement GroupCell
If you’ve ever customized a Table Cell, you know you need to create a custom class for the Table Cell. Similarly, when using AsyncDisplayKit, we need to create a custom Cell node and extend the ASCellNode to render custom data. Here, we create a GroupCellNode class that holds references to Label and Image.
Create a new file and name it GroupCellNode. Modify its contents to:
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 } }
This node downloads and displays thumbnails of Meetup groups. AsyncDisplay has a class called ASNetworkImageNode that downloads and displays remote images. All you need to do is set the URL of the image to its URL property. The image is loaded asynchronously and displayed synchronously.
For text, we use ASTextNode to display it. A literal node is similar to our usual UILabel. It adds rich text support and extends the ASControlNode class.
Helper method (namely createLayerBackedTextNode method) is used to create the Label when repeated program packed in a way.
The automatic layout of AsyncDisplayKit is based on the CSS box model. Compared to UIKit layout constraints, it is more efficient, easier to debug, cleaner, and structured to construct complex and reusable layouts.
Now to write the layout method:
override func layoutSpecThatFits(_ constrainedSize: ASSizeRange) - > ASLayoutSpec {_locationLabel. Style. FlexShrink = 1.0 _organizerNameLabel. Style.css. Let flexShrink = 1.0 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 uses a very powerful layout specification called ASStackLayoutSpec. It contains a number of properties that you can use to achieve any effect you want. We also used ASInsetLayoutSpec to add some padding.
In simple terms, this code creates a vertical Stack layout with two nodes and a horizontal Stack at the top, which itself contains three more nodes that display the creator’s image, creator name, and group location. Finally, we wrapped the entire node as a vertical ASStackLayoutSpec return.
ASLayoutSpec is used as a small interval.
[ecko_alert color= “green”] Note: You can refer to the official documentation for automatic layouts in AsyncDisplayKit. [/ecko_alert]
Final assembly
Previously, we have implemented each part of the app using AsyncDisplayKit. Now, let’s assemble them into a full app.
Open the project navigator and select Appdelegate.Swift. Modify the application (: didFinishLaunchingWithOptions:) method is:
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 added a few sentences, including initializing the Feed View Controller, data manager, and Interactor. Now you can run the app to test it. The app downloads and displays Meetup groups from meetup.com. However, the image cannot be displayed. If you take a look at the console, you will see that the error is reported:
App Transport Security has blocked a cleartext HTTP (http://)
App Transport Security (ATS), introduced in iOS 9, provides for apps to preferentially use HTTPS secure network connections. If your app accesses HTTP remote resources, you will see this error.
Close the ATS
To solve this problem, you can turn off the ATS in the info.plist file. Select info.plist in the project navigator and edit it here:
Setting the Allow Arbitrary Loads option to YES disables the ATS. Then run the app again. This time, you can see the group image.
The end of the
Congratulations, the app is complete! In this article, I walk you through the basics of AsyncDisplayKit. You now know how to create a responsive UI using AsyncDisplayKit in your own projects. For more information, I suggest you read the official documentation.
You can download the completed project from GitHub.
What do you think of this article and the AsyncDisplayKit framework? Let me know if you want to hear more about this amazing framework.
Introduction to the translator
Hongyan Yang, male, Chinese mainland, CSDN blog expert (personal blog blog.csdn.net/kmyhy). I began to learn Apple iOS development in 2009, proficient in O-C/Swift and Cocoa Touch frameworks, and developed several store applications and enterprise apps. Love writing, has written and translated a number of technical monographs, including: “Enterprise iOS Application Combat”, “iPhone & iPad enterprise mobile application Development Secrets”, “iOS8 Swift Programming Guide”, “Swift for busy People”, “iOS Swift Game development classic examples”