AFNetworking is one of the most popular third-party libraries on iOS and OS X. It was awarded the 2012 Best iOS Library Award in our 2012 Reader’s Choice Awards, and is one of the most widely-used open source projects on Github with over 14K stars and 4K forks.
Recently the creator and maintainer of AFNetworking, Mattt Thompson, released a new networking library like AFNetworking, but designed specifically to use modern Swift conventions and language features: Alamofire.
The AF prefix in AFNetworking is short for Alamofire, so even the new library name is based on the new Swift conventions! :]
In Part 1 of this two part series, you will use Alamofire to build a photo gallery app that uses 500px.com as its feed. Along the way you’ll learn about Alamofire’s most important components and some other important aspects of handling network requests in your apps.
Part 2 will continue on from Part 1 and add some more functionality and polish to the app, learning about more advanced Alamofire features along the way.
This tutorial assumes you are familiar with Swift and iOS development – if you are not, please check out some of our other tutorials. Also, please use Xcode 6.1 for this tutorial (not Xcode 6.2 beta).
Note: If you’re familiar with the basics of Alamofire, you can jump straight to the second part of this tutorial. But make sure you obtain your consumer key first and replace it in the app as described below.
Getting Started
Download the starter project for this tutorial; it provides all UI code required to finish the project in this tutorial. This lets you focus on learning about Alamofire rather than mucking about with UI design.
Open up the project in Xcode and have a look at Main.storyboard:
The app uses a common UI pattern with a UITabBarController
as the root screen. The tab controller contains two tabs, each with its own UINavigationController
. The first tab lets users browse popular photos. The second tab allows users to browse photos they’ve already saved. Both tabs display photos to the user in a UICollectionViewController
. The storyboard also contains two standalone view controllers which will be used later in the tutorial.
Build and run your app; you’ll be greeted with a spinner that spins forever:
That’s not terribly cool — there’s not much to look at. But you’ll soon change that with some Alamofire code.
Note: If you’re familiar with AFNetworking, you might expect that the next section will discuss CocoaPods. Unfortunately, at the time of writing this tutorial there’s no straightforward way to integrate a Swift library via CocoaPods.
While a few people have found and posted solutions around the Internet, the steps below use the time-proven method of manually copying the code into your project.
To get the latest version of Alamofire, head to https://github.com/Alamofire/Alamofire and click the Download ZIP button found on the right hand side of the page. Open the starter project’s folder in Finder and drag the Alamofire-master folder into your main project folder.
Open the Alamofire-master folder (now in a subdirectory of your project folder) and drag Alamofire.xcodeproj — the file with a blue icon, not the white icon — straight into Xcode right below Photomania, like so:
Next, click on Photomania and ensure you’ve selected the General tab. Scroll down and click the + below Embedded Binaries, then choose Alamofire.framework and finally click on Add:
Build and run your project to make sure you have no errors, then move onto the next section.
Retreiving Data with Alamofire
You might be asking why you need Alamofire in the first place. Apple provides the NSURLSession
class and related classes for downloading content via HTTP, so why complicate things with another third party library?
The short answer is that Alamofire is based on NSURLSession
, but it frees you from writing boilerplate code and makes writing networking code much easier. You can access data on the Internet with very little effort, and your code will be much cleaner and easier to read.
To use Alamofire, you first need to import it. To do this, open PhotoBrowserCollectionViewController.swift and add the following line to the top of the file:
import Alamofire |
You’ll need to add this import
statement to every file that accesses Alamofire classes and functions.
Next, add the following code to viewDidLoad()
just below setupView()
:
Alamofire.request(.GET, "https://api.500px.com/v1/photos").responseJSON() { (_, _, data, _) in println(data) } |
I’ll explain this in a moment, but first build and run your app; you’ll see the following message appear in the console:
Optional({ error = "Consumer key missing."; status = 401; }) |
You might not realize it, but you just made your first network request with Alamofire. You requested a resource on the Internet and received a JSON response in return.
Here’s what’s going on under the hood:
Alamofire.request(_:_)
accepts two required parameters:method
, which is usually.GET
or.POST
; andURLString
, which is the URL of the content you wish to access. You then receive anAlamofire.Request
object in response.- Typically, you’ll simply chain the request object to a response method; for example, in the code above the request object simply calls
responseJSON()
.responseJSON
will call the supplied closure when the request completes. In this code, you simply output the parsed JSON to the console. - By calling
responseJSON
, you indicate that you expect a JSON response. In this case, Alamofire attempts to parse the response and return a JSON object. Alternatively, you could request a property list usingresponsePropertyList
or even a raw string usingresponseString
. You’ll learn more about these response serializers later in the tutorial.
As you can see from the response logged to the console, the server says that you need something called a consumer key. Before you can go any further, you’ll need a key for the 500px API.
Obtaining Your Consumer Key
Go to https://500px.com/signup and register for free using your email address, or by way of your Facebook, Twitter or Google login.
Once you’ve completed your registration, go to https://500px.com/settings/applications and click on “Register your application”.
You’ll see the following dialog box:
Those fields with big red arrows are all required. Add the Application Name as Alamofire Tutorial and the Description as iOS App. Your app doesn’t have an Application URL, but enter any valid URL to satisfy the Register Application requirements — perhaps raywenderlich.com will do! :]
Finally, enter your email address in the Developer’s Email field and click the checkbox to accept the Terms of Use.
Finally, click on the Register button; you’ll see a box like the following:
Click on the See application details link and it will expand to show you your consumer key as follows:
Copy the consumer key from this screen, then head back to Xcode and replace the first and only piece of code you’ve added so far with the following:
Alamofire.request(.GET, "https://api.500px.com/v1/photos", parameters: ["consumer_key": "PASTE_YOUR_CONSUMER_KEY_HERE"]).responseJSON() { (_, _, JSON, _) in println(JSON) } |
Make sure you replace PASTE_YOUR_CONSUMER_KEY_HERE
with the consumer key you copied above.
Build and run your app; this time, you’ll see a LOT more output in the console:
All the output above means you’ve successfully downloaded some JSON that contains information about some photos.
The JSON response contains a few properties about the set of images, some paging information, and an array of photos. Here’s the search result I got (yours may look slightly different):
{ "feature": "popular", "filters": { "category": false, "exclude": false }, "current_page": 1, "total_pages": 250, "total_items": 5000, "photos": [ { "id": 4910421, "name": "Orange or lemon", "description": "", . . . } }, { "id": 4905955, "name": "R E S I G N E D", "description": "From the past of Tagus River, we have History and memories, some of them abandoned and disclaimed in their margins ...", . . . } ] } |
Now that you have the JSON response you can do something useful with it.
Replace println(JSON)
in viewDidLoad()
with the following:
let photoInfos = (JSON!.valueForKey("photos") as [NSDictionary]).filter({ ($0["nsfw"] as Bool) == false }).map { PhotoInfo(id: $0["id"] as Int, url: $0["image_url"] as String) } self.photos.addObjectsFromArray(photoInfos) self.collectionView.reloadData() |
The above code translates the JSON response into an array of more manageable PhotoInfo
objects. These objects are just simple buckets for the photo’s ID and URL properties. You’ll also notice the code filters out pictures that…well…you might not want popping up unexpectedly! ;]
The above code also reloads the collection view. The sample code from the starter project creates cells in the collection view based on the photos
array you populated.
Build and run your app; this time the spinner disappears after a while and if you look closely, you’ll see a bunch of dark grey square cells:
You’re getting closer!
Still in PhotoBrowserCollectionViewController.swift, add the following code to collectionView(_: cellForItemAtIndexPath:)
just before return cell
:
let imageURL = (photos.objectAtIndex(indexPath.row) as PhotoInfo).url Alamofire.request(.GET, imageURL).response() { (_, _, data, _) in let image = UIImage(data: data! as NSData) cell.imageView.image = image } |
The above code creates another Alamofire request for an image from the photos
array. Because this is an image request, you’re using the simple request
method which returns the response in an NSData
blob. You then put the data directly into an instance of UIImage
and, in turn, put that into an image view that already exists in the sample project.
Build and run your app once again; a collection of images should appear, similar to the screenshot below:
You have your proof-of-concept Alamofire requests working just fine, but you don’t want to copy and paste the API address and add your consumer key every single time you request something from the server. Aside from it being ugly and cumbersome, imagine the code rework you’d face if the API address changed or you had to use a new consumer key!
Fortunately, Alamofire has a solution to that exact problem.
Creating a Request Router
Open Five100px.swift and find struct Five100px
, which defines enum ImageSize
. This is a very simple data structure based on the 500px.com API documentation.
Since you’re going to add some Alamofire code, add the following requisite import to the top of the file:
import Alamofire |
Now, add the following code inside struct Five100px
, just above enum ImageSize
:
enum Router: URLRequestConvertible { static let baseURLString = "https://api.500px.com/v1" static let consumerKey = "PASTE_YOUR_CONSUMER_KEY_HERE" case PopularPhotos(Int) case PhotoInfo(Int, ImageSize) case Comments(Int, Int) var URLRequest: NSURLRequest { let (path: String, parameters: [String: AnyObject]) = { switch self { case .PopularPhotos (let page): let params = ["consumer_key": Router.consumerKey, "page": "\(page)", "feature": "popular", "rpp": "50", "include_store": "store_download", "include_states": "votes"] return ("/photos", params) case .PhotoInfo(let photoID, let imageSize): var params = ["consumer_key": Router.consumerKey, "image_size": "\(imageSize.rawValue)"] return ("/photos/\(photoID)", params) case .Comments(let photoID, let commentsPage): var params = ["consumer_key": Router.consumerKey, "comments": "1", "comments_page": "\(commentsPage)"] return ("/photos/\(photoID)/comments", params) } }() let URL = NSURL(string: Router.baseURLString) let URLRequest = NSURLRequest(URL: URL!.URLByAppendingPathComponent(path)) let encoding = Alamofire.ParameterEncoding.URL return encoding.encode(URLRequest, parameters: parameters).0 } } |
This is your router, which creates appropriate instances of URLString
for your API calls. It’s a simple enum
that conforms to URLRequestConvertible
, which a protocol defined inside Alamofire. When an enum adopts this protocol, it must have a variable of type NSURLRequest
named URLRequest
.
Your router has two static constants: the baseURLString
of your API and your consumerKey
. For the last time (promise!), replace PASTE_YOUR_CONSUMER_KEY_HERE
with your own individual consumer key. From now on, the router will add your consumer key to the final URLString
as necessary.
Your app has three API endpoints: one to retrieve a list of popular photos, one to retrieve more information about a specific photo and one to retrieve for comments on a photo. Your router handles these three conditions with three corresponding case
statements, each of which accepts a parameter or two.
You’ve defined var URLRequest: NSURLRequest
as a computed property; this means that each time you use this enum
, it constructs the resulting URL on-the-fly based on the specific case
and its parameters.
Here’s an example snippet of code that illustrates the above logic:
Five100px.Router.PhotoInfo(10000, Five100px.ImageSize.Large) // URL: https://api.500px.com/v1/photos/10000?consumer_key=xxxxxx&image_size=4 // https://api.500px.com/v1 + /photos/10000 + ?consumer_key=xxxxxx&image_size=4 // = baseURLString + path + encoded parameters |
In the above example, the code routes through the photo info API endpoint looking for a large-sized photo with an ID of 10000. The commented lines break down the construction of the URL. In this case, the URL has three components: the baseURLString
, the path
(the part that comes before the “?”), and a [String: AnyObject]
dictionary of the parameters to pass to your API endpoint.
For path
, the first element of the return tuple, you’re using a simple Swift string interpolation as follows:
"/photos/\(photoID)" // "/photos/10000" |
Similar to response parsing, request parameters can be encoded as JSON, property lists, or strings. Typically, you’ll use simple string parameters as in the code above.
If you’re planning to use this router in your own projects, you’ll need to be extremely familiar with how it works. To that end, try figuring out how you’d construct the following URL:
https://api.foursquare.com/v2/users/{USER_ID}/lists?v=20131016&group=created
How did you do? If you weren’t 100% sure of the answer, take a bit of time and analyze the router code until you’re comfortable with how it works.
Loading More Photos
Your app currently shows only one page of photos. You’ll fix that so you can browse photos to your heart’s content. More is always better, right? :]
Open PhotoBrowserCollectionViewController.swift and add the following code below let refreshControl = UIRefreshControl()
:
var populatingPhotos = false var currentPage = 1 |
There are two variables to keep track of whether you’re currently populating photos, and what the current page of photos is.
Next, replace the current implementation of viewDidLoad()
with the following:
override func viewDidLoad() { super.viewDidLoad() setupView() populatePhotos() } |
Here you replace the Alamofire request you had previously with a call to populatePhotos()
, which you’ll write in a moment.
Still working in the same file, add the following two functions above handleRefresh()
:
// 1 override func scrollViewDidScroll(scrollView: UIScrollView) { if scrollView.contentOffset.y + view.frame.size.height > scrollView.contentSize.height * 0.8 { populatePhotos() } } func populatePhotos() { // 2 if populatingPhotos { return } populatingPhotos = true // 3 Alamofire.request(Five100px.Router.PopularPhotos(self.currentPage)).responseJSON() { (_, _, JSON, error) in if error == nil { // 4 dispatch_async(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_HIGH, 0)) { // 5, 6, 7 let photoInfos = ((JSON as NSDictionary).valueForKey("photos") as [NSDictionary]).filter({ ($0["nsfw"] as Bool) == false }).map { PhotoInfo(id: $0["id"] as Int, url: $0["image_url"] as String) } // 8 let lastItem = self.photos.count // 9 self.photos.addObjectsFromArray(photoInfos) // 10 let indexPaths = (lastItem..<self.photos.count).map { NSIndexPath(forItem: $0, inSection: 0) } // 11 dispatch_async(dispatch_get_main_queue()) { self.collectionView.insertItemsAtIndexPaths(indexPaths) } self.currentPage++ } } self.populatingPhotos = false } } |
There’s a lot going on here. Taking each commented section in turn:
scrollViewDidScroll()
loads more photos once you’ve scrolled through 80% of the view.populatePhotos()
loads photos in thecurrentPage
and usespopulatingPhotos
as a flag to avoid loading the next page while you’re still loading the current page.- You’re using your fancy router for the first time here. You simply pass in the page number and it constructs the URL string for that page. 500px.com returns at most 50 photos in each API call, so you’ll need to make another call for the next batch of photos.
- Make careful note that the completion handler — the trailing closure of
.responseJSON()
— must run on the main thread. If you’re performing any long-running operations, such as making an API call, you must use GCD to dispatch your code on another queue. In this case, you’re usingDISPATCH_QUEUE_PRIORITY_HIGH
to run this activity. - You’re interested in the
photos
key of the JSON response that includes an array of dictionaries. Each dictionary in the array contains information about one photo. - Here you use Swift’s
filter
function to filter out NSFW (Not Safe For Work) images. - The
map
function takes a closure and returns an array ofPhotoInfo
objects. This class is defined in Five100px.swift. If you look at the code of this class, you’ll see that it overrides bothisEqual
andhash
. Both of these methods use an integer for theid
property so ordering and uniquingPhotoInfo
objects will still be a relatively fast operation. - Next you store the current number of photos before you add the new batch; you’ll use this to update
collectionView
. - If someone uploaded new photos to 500px.com before you scrolled, the next batch of photos you get might contain a few photos that you’d already downloaded. That’s why you defined
var photos = NSMutableOrderedSet()
as a set; since all items in a set must be unique, this guarantees you won’t show a photo more than once. - Here you create an array of
NSIndexPath
objects to insert intocollectionView.
- Inserts the items in the collection view – but does so on the main queue, because all UIKit operations must be done on the main queue.
Build and run your app and scroll slowly through the photos; you’ll see that new photos are continually loaded as you page through the app:
Scroll through the photos a bit faster now…a little faster again…notice any issues? Yep, the scrolling is pretty choppy. That’s not the kind of experience you want to offer your users — but fortunately you’ll fix this issue in the next section.
Creating Custom Response Serializers
You’ve already seen how simple it is to use the provided JSON, string, and property list serializers in Alamofire. But sometimes you’ll want to create your own custom response serialization. For example, instead of receiving an NSData
that you then have to convert to a UIImage
, you can write a custom response serializer to convert it directly to a UIImage
for you.
In this section, you’ll learn how to do exactly that.
Open Five100px.swift and add the following code near the top of the file, just below the import Alamofire
statment:
extension Alamofire.Request { class func imageResponseSerializer() -> Serializer { return { request, response, data in if data == nil { return (nil, nil) } let image = UIImage(data: data!, scale: UIScreen.mainScreen().scale) return (image, nil) } } func responseImage(completionHandler: (NSURLRequest, NSHTTPURLResponse?, UIImage?, NSError?) -> Void) -> Self { return response(serializer: Request.imageResponseSerializer(), completionHandler: { (request, response, image, error) in completionHandler(request, response, image as? UIImage, error) }) } } |
In order to make a new response serializer, the first thing you need is a class function that returns a Serializer
closure (imageResponseSerializer()
in the example above). This closure is typealiased in Alamofire, accepts three parameters and returns two as shown in the snippet below:
public typealias Serializer = (NSURLRequest, NSHTTPURLResponse?, NSData?) -> (AnyObject?, NSError?) |
Your class function (i.e. imageResponseSerializer()
) accepts as arguments the underlying NSURLSession
request and response objects along with a raw NSData
representation of the data received from the server. The function then uses these objects to serialize the input into a meaningful data structure and return it from the function, along with any error that occurs during this process. In your case, you use UIImage
to translate the data into an image object.
Usually when you create a response serializer, you’ll also want to create a new response handler to go with it and make it easy to use. You do this here with .responseImage()
, which has a fairly simple job: it takes a completionHandler
, a block of code in form of a closure that will execute once you’ve serialized the data from the server. All you need to do in your response handlers is call Alamofire’s own general purpose .response()
response handler.
Let’s put this to use. Open PhotoBrowserCollectionViewController.swift and add the following property to PhotoBrowserCollectionViewCell
underneath the imageView
property:
var request: Alamofire.Request? |
This will store the Alamofire request to load the image for this cell.
Now replace the contents of collectionView(_: cellForItemAtIndexPath:)
with the following:
let cell = collectionView.dequeueReusableCellWithReuseIdentifier(PhotoBrowserCellIdentifier, forIndexPath: indexPath) as PhotoBrowserCollectionViewCell let imageURL = (photos.objectAtIndex(indexPath.row) as PhotoInfo).url cell.imageView.image = nil cell.request = Alamofire.request(.GET, imageURL).responseImage() { (request, _, image, error) in if error == nil && image != nil { if request.URLString == cell.request?.request.URLString { cell.imageView.image = image } } } return cell |
Build and run your app; scroll through the images again and the experience should be much more smooth:
Why Is It Faster?
But what exactly did you change to make scrolling so much faster? The critical code is in collectionView(_: cellForItemAtIndexPath:)
. But before you can understand how the fix works, you’ll need to understand the asynchronous nature of network calls.
Network requests — and by extension, Alamofire requests — are asynchronous in nature. That means making a network request shouldn’t block the execution of the rest of your code. Network requests can potentially take a long time to return — and you don’t your entire UI to freeze while waiting for pictures to download!
That being said, asynchronous requests pose a challenge. What if the UI changed between the time the request was made and the time you received a response back from the server?
For example, UICollectionView
has an internal mechanism that dequeues cells. Creating new cells is expensive, so rather than constantly creating new cells, collection views reuse existing cells that are no longer on the screen.
This means that the same cell objects, with the same memory addresses, will be used over and over again. Therefore it’s possible that between the time you made your Alamofire request and the time you received the image response, the user scrolled the cell off the screen and that image is no longer required. Maybe the cell was dequeued and is supposed to display a completely different image!
You did two things in the above code to handle this situation. First, when you dequeue a cell you invalidate the image by setting it to nil
; this ensures you’re not displaying the previous image. Second, your request completion handler checks to make sure the cell’s URL is the same as the request’s URL. If they aren’t, clearly the cell has moved on to a different image, and your completion handler won’t waste cycles setting the wrong image in the cell.
Where to Go From Here?
You can download the finished project from this Part 1 of the tutorial here..
Note: If you are discarding your own work and using the finished project above, don’t forget to replace your consumer key as appropriate in Five100px.swift as instructed earlier in the tutorial.
You covered a lot in this tutorial — this is a great time to take a little break! At this point your app has basic photo browsing functionality thanks to Alamofire.
In the process, you’ve learned how to make a GET request with Alamofire, send parameters, create a request router, and even create your own response serializer.
In the second part of this tutorial, you’ll add the following functionality:
- A photo viewer
- The ability to view comments and other details
- An option to download photos with a sleek inline progress bar
- And yes, Virginia, there is a pull-to-refresh clause! :)
I hope you enjoyed this part of the tutorial and that you’ll join us for Part 2. If you had any questions or comments on Alamofire, come join us in the discussion below!
Beginning Alamofire Tutorial is a post from: Ray Wenderlich
The post Beginning Alamofire Tutorial appeared first on Ray Wenderlich.