Quantcast
Channel: Kodeco | High quality programming tutorials: iOS, Android, Swift, Kotlin, Unity, and more
Viewing all articles
Browse latest Browse all 4400

Intermediate Alamofire Tutorial

$
0
0
Learn how to easily make network requests in Swift with Alamofire!

Learn how to easily make network requests in Swift with Alamofire!

Welcome back to the second and final part of our two-part series on Alamofire!

In the first part of the series, you covered some basic use cases of Alamofire such as making GET requests, sending parameters, creating a request router, and even creating a custom response serializer.

In the process, you built a cool a photo gallery app named Photomania.

In this second and final part of the series, you’ll add the following bits of functionality to the app:

  • A photo viewer
  • The ability to view comments and other details
  • An option to download photos with a sleek inline progress bar
  • Optimized network calls and image caching
  • And yes, Virginia, there is a pull-to-refresh clause! :)

Getting Started

You can keep working with your own project from last time, but if you want to start with a clean starter project (or if you bypassed the first section of this tutorial altogether!), you can download the finished project from Part 1 here.

Note: If you didn’t work through Part 1 of this tutorial, don’t forget that you’ll first need to obtain a consumer key from 500px.com and replace it as necessary in Five100px.swift. Instructions on how to get this key — and where to put it — are provided in Part 1: Beginning Alamofire Tutorial.

Build and run the starter project to refresh yourself on how everything works; the photo preview browser is functional, but tap on a photo and the photo doesn’t open up fullscreen as you’d expect. Looks like that’s your first problem to solve! :]

Creating the Photo Viewer

Let’s be honest — generics are arguably one of the most powerful features of any modern language, including Swift. It just wouldn’t be right if you didn’t use any in this project.

Open Five100px.swift and add the following code near the top of the file, just below import Alamofire:

@objc public protocol ResponseObjectSerializable {
  init(response: NSHTTPURLResponse, representation: AnyObject)
}
 
extension Alamofire.Request {
  public func responseObject<T: ResponseObjectSerializable>(completionHandler: (NSURLRequest, NSHTTPURLResponse?, T?, NSError?) -> Void) -> Self {
    let serializer: Serializer = { (request, response, data) in
      let JSONSerializer = Request.JSONResponseSerializer(options: .AllowFragments)
      let (JSON: AnyObject?, serializationError) = JSONSerializer(request, response, data)
      if response != nil && JSON != nil {
        return (T(response: response!, representation: JSON!), nil)
      } else {
        return (nil, serializationError)
      }
    }
 
    return response(serializer: serializer, completionHandler: { (request, response, object, error) in
      completionHandler(request, response, object as? T, error)
    })
  }
}

In the code above you’re extending Alamofire once again by adding a new response serializer. This time, you’ve added a .responseObject() function; as a generic function, it can serialize any data object that conforms to the ResponseObjectSerializable you defined above.

That means if you define a new class that has an initializer of the form init(response:representation:), Alamofire can automatically return objects of that type from the server. You’ve encapsulated the serialization logic right inside the custom class itself. Ahh — the beautiful elegance of object-oriented design!

The photo viewer uses the PhotoInfo class, which already conforms to ResponseObjectSerializable (it implements the required method). But you need to make this official by marking the class as conforming to ResponseObjectSerializable.

Open Five100px.swift and modify the class declaration of PhotoInfo to explicitly conform to ResponseObjectSerializable as follows:

class PhotoInfo: NSObject, ResponseObjectSerializable {
Note: Although not required reading, readers who are interested in learning how the representation parameter is serialized into a PhotoInfo object can browse through required init(response:representation:) to see how it’s done.

Open PhotoViewerViewController.swift — NOT PhotoBrowserCollectionViewController.swift! — and add the requisite import statement to the top of the file:

import Alamofire

Next, add the following code to the end of viewDidLoad():

loadPhoto()

You’ll get an error that loadPhoto() is missing, but don’t worry – you’ll write this method next!

Still working in the same file, add the following code just before setupView():

func loadPhoto() {
  Alamofire.request(Five100px.Router.PhotoInfo(self.photoID, .Large)).validate().responseObject() {
    (_, _, photoInfo: PhotoInfo?, error) in
 
    if error == nil {
      self.photoInfo = photoInfo
 
      dispatch_async(dispatch_get_main_queue()) {
        self.addButtomBar()
        self.title = photoInfo!.name
      }
 
      Alamofire.request(.GET, photoInfo!.url).validate().responseImage() {
        (_, _, image, error) in
 
        if error == nil && image != nil {
          self.imageView.image = image
          self.imageView.frame = self.centerFrameFromImage(image)
 
          self.spinner.stopAnimating()
 
          self.centerScrollViewContents()
        }
      }
    }
  }
}

This time you’re making an Alamofire request inside another Alamofire request’s completion handler. The first request receives a JSON response and uses your new generic response serializer to create an instance of PhotoInfo out of that response.

(_, _, photoInfo: PhotoInfo?, error) in indicates the completion handler parameters: the first two underscores (“_” characters) mean the first two parameters are throwaways and there’s no need to explicitly name them request and response.

The third parameter is explicitly declared as an instance of PhotoInfo, so the generic serializer automatically initializes and returns an object of this type, which contains the URL of the photo. The second Alamofire request uses the image serializer you created earlier to convert the NSData to a UIImage that you then display in an image view.

Note: You’re not using the router here because you already have the absolute URL of the image; you aren’t constructing the URL yourself.

The .validate() function call before requesting a response object is another easy-to-use Alamofire feature. Chaining it between your request and response validates that the response has a status code in the default acceptable range of 200 to 299. If validation fails, the response handler will have an associated error that you can deal with in your completion handler.

Even if there’s an error, your completion handler will still be called. The fourth parameter error is an instance of NSError, which contains a value that lets you respond to errors in your own custom fashion.

Build and run your project; tap on one of the photos and you should see it fill the screen, like so:

Huzzah! Your photo viewer is working; double-tap the image to zoom then scroll.

When your type-safe, generic response serializer initializes a PhotoInfo you don’t set just the id and url properties; there are a few other properties you haven’t seen yet.

Tap the Menu button in the lower left corner of the app and you’ll see some extended details for the photo:

7

Tap anywhere on the screen to dismiss the photo details.

If you’re familiar with 500px.com, you know that users tend to leave lots of comments on the best and brightest photos on the site. Now that you have the photo viewer working, you can move on to the comments viewer of your app.

Creating a Collection Serializer for Comments

For photos that have comments, the photo viewer will display a button indicating the number of comments. Tapping on the comments button will open a popover listing the comments.

Open Five100px.swift and add the following code below the import Alamofire statement:

@objc public protocol ResponseCollectionSerializable {
  class func collection(#response: NSHTTPURLResponse, representation: AnyObject) -> [Self]
}
 
extension Alamofire.Request {
  public func responseCollection<T: ResponseCollectionSerializable>(completionHandler: (NSURLRequest, NSHTTPURLResponse?, [T]?, NSError?) -> Void) -> Self {
    let serializer: Serializer = { (request, response, data) in
      let JSONSerializer = Request.JSONResponseSerializer(options: .AllowFragments)
      let (JSON: AnyObject?, serializationError) = JSONSerializer(request, response, data)
      if response != nil && JSON != nil {
        return (T.collection(response: response!, representation: JSON!), nil)
      } else {
        return (nil, serializationError)
      }
    }
 
    return response(serializer: serializer, completionHandler: { (request, response, object, error) in
      completionHandler(request, response, object as? [T], error)
    })
  }
}

This should look familiar; it is very similar to the generic response serializer you created earlier.

The only difference is that this protocol defines a class function that returns a collection (rather than a single element) — in this case, [Self]. The completion handler has a collection as its third parameter — [T] — and calls collection on the type instead of an initializer.

Still working in the same file, replace the entire Comment class with the following:

final class Comment: ResponseCollectionSerializable {
  class func collection(#response: NSHTTPURLResponse, representation: AnyObject) -> [Comment] {
    var comments = [Comment]()
 
    for comment in representation.valueForKeyPath("comments") as [NSDictionary] {
      comments.append(Comment(JSON: comment))
    }
 
    return comments
  }
 
  let userFullname: String
  let userPictureURL: String
  let commentBody: String
 
  init(JSON: AnyObject) {
    userFullname = JSON.valueForKeyPath("user.fullname") as String
    userPictureURL = JSON.valueForKeyPath("user.userpic_url") as String
    commentBody = JSON.valueForKeyPath("body") as String
  }
}

This makes Comment conform to ResponseCollectionSerializable so it works with your above response serializer.

Now all you need to do is use it. Open PhotoCommentsViewController.swift, and add the requisite import statement at the top of the file:

import Alamofire

Now add the following code to the end of viewDidLoad():

Alamofire.request(Five100px.Router.Comments(photoID, 1)).validate().responseCollection() {
  (_, _, comments: [Comment]?, error) in
 
  if error == nil {
    self.comments = comments
 
    self.tableView.reloadData()
  }
}

This uses your new response serializer to deserialize the NSData response into a collection of Comments, saves them in a property, and reloads the table view.

Next, add the following code to tableView(_:cellForRowAtIndexPath), just above the return cell statement:

cell.userFullnameLabel.text = comments![indexPath.row].userFullname
cell.commentLabel.text = comments![indexPath.row].commentBody
 
cell.userImageView.image = nil
 
let imageURL = comments![indexPath.row].userPictureURL
 
Alamofire.request(.GET, imageURL).validate().responseImage() {
  (request, _, image, error) in
 
  if error == nil {
    if request.URLString.isEqual(imageURL) {
      cell.userImageView.image = image
    }
  }
}

This displays the information from the comment in the table view cell, and also fires off a secondary Alamofire request to load the image (this is similar to what you did in part 1 of the series).

Build and run your app; browse through the photos until you find one with comments; you’ll know the photo has comments when a number displays beside the comments icon. Tap the Comments button and you’ll see the comments on this photo appear like so:

By now you’ve probably found a photo or two (or a hundred! :]) that you’d like to download to your device. The next section shows you how to do just that!

Displaying Download Progress

Your photo viewer has an action button in the middle of the bottom bar. It shows a UIActionSheet that should let you download a photo — but right now it does nothing. Up until now, you’ve only loaded photos from 500px.com into memory. How do you download and save a file to your device?

Open PhotoViewerViewController.swift and replace the empty downloadPhoto() with the following:

func downloadPhoto() {
  // 1
  Alamofire.request(Five100px.Router.PhotoInfo(photoInfo!.id, .XLarge)).validate().responseJSON() {
    (_, _, JSON, error) in
 
    if error == nil {
      let jsonDictionary = (JSON as NSDictionary)
      let imageURL = jsonDictionary.valueForKeyPath("photo.image_url") as String
 
      // 2
      let destination = Alamofire.Request.suggestedDownloadDestination(directory: .DocumentDirectory, domain: .UserDomainMask)
 
      // 3
      Alamofire.download(.GET, imageURL, destination)
 
    }
  }
}

Head through the code comment by comment and you’ll find the following:

  1. You first request a new PhotoInfo, only this time asking for an XLarge size image.
  2. Get the default location on disk to which to save your files — this will be a subdirectory in the Documents directory of your app. The name of the file on disk will be the same as the name that the server suggests. destination is a closure in disguise — more on that in just a moment.
  3. Alamofire.download(_:_:_) is a bit different than Alamofire.request(_:_) in that it doesn’t need a response handler or a serializer in order to perform an operation on the data, as it already knows what to do with it — save it to disk! The destination closure returns the location of the saved image.

Build and run your app; find your favorite photo, tap the action button and tap Save. You won’t see any visual feedback just yet, but go back to the tab view and tap the Downloads tab where you’ll see the photo you just saved.

You may ask, “Why not simply pass a constant location for the file?” That’s because you might not know the name of the file before you download it. In the case of 500px.com, the server will always suggest 1.jpg, 2.jpg, 3.jpg, 4.jpg or 5.jpg based on the size of the file, and you can’t save files with the same filename overtop of each other.

Instead of passing a constant location as a string, you pass a closure as the third parameter of Alamofire.download. Alamofire then calls this closure at an appropriate time, passing in temporaryURL and NSHTTPURLResponse as arguments and expecting an instance of NSURL that points to a location on disk where you have write access.

Save a few more photos and return to the Downloads tab where you’ll see — only one file? Hey, what’s going on?

It turns out the filename isn’t unique, so you’ll need to implement your own naming logic. Replace the line just below the //2 comment in downloadPhoto() with the following code:

let destination: (NSURL, NSHTTPURLResponse) -> (NSURL) = {
  (temporaryURL, response) in
 
  if let directoryURL = NSFileManager.defaultManager().URLsForDirectory(.DocumentDirectory, inDomains: .UserDomainMask)[0] as? NSURL {
    return directoryURL.URLByAppendingPathComponent("\(self.photoInfo!.id).\(response.suggestedFilename)")
  }
 
  return temporaryURL
}

Once again, let destination is a closure. But this time you implemented your own naming logic: you use the id of the photo captured from outside the closure and concatenate it with the file name suggested by the server, adding a “.” in between for separation.

Build and run, and now you can save multiple photos:

The file save functionality is now working well, but it would be nice to display some progress to the user as the file is downloading. Alamofire makes it easy to implement indicators showing the progress of the download.

Replace the line below // 3 in downloadPhoto() with the following:

// 4
let progressIndicatorView = UIProgressView(frame: CGRect(x: 0.0, y: 80.0, width: self.view.bounds.width, height: 10.0))
progressIndicatorView.tintColor = UIColor.blueColor()
self.view.addSubview(progressIndicatorView)
 
// 5
Alamofire.download(.GET, imageURL, destination).progress {
  (_, totalBytesRead, totalBytesExpectedToRead) in
 
  dispatch_async(dispatch_get_main_queue()) {
    // 6
    progressIndicatorView.setProgress(Float(totalBytesRead) / Float(totalBytesExpectedToRead), animated: true)
 
    // 7
    if totalBytesRead == totalBytesExpectedToRead {
      progressIndicatorView.removeFromSuperview()
    }
  }
}

Looking at each numbered comment in detail:

  1. You use a standard UIProgressView to show the progress of downloading a photo. Set it up and add it to the view hierarchy.
  2. With Alamofire you can chain .progress(), which takes a closure called periodically with three parameters: bytesRead, totalBytesRead, totalBytesExpectedToRead.
  3. Simply divide totalBytesRead by totalBytesExpectedToRead and you’ll get a number between 0 and 1 that represents the progress of the download task. This closure may execute multiple times if the if the download time isn’t near-instantaneous; each execution gives you a chance to update a progress bar on the screen.
  4. Once the download is finished, simply remove the progress bar from the view hierarchy.

Build and run your app; find another fantastic photo and save it to see the progress bar in action:

9

The progress bar disappears when the download completes, so you may not see it on particularly fast network connections.

Note that downloadPhoto is still using .resposneJSON() in section #1. Here’s a challenge to make sure you understand how response serializers work. Update the code above to use your generic response serializer . responseObject() instead. If you want to check your solution, you can see how we did it below.

Solution Inside: Solution SelectShow>

Optimizing and Refreshing

Okay, it’s time to implement the pull-to-refresh feature. (Have you been doing this instinctively in the app? You’re not alone! :])

Open PhotoBrowserCollectionViewController.swift, and replace func handleRefresh() with the following:

func handleRefresh() {
  refreshControl.beginRefreshing()
 
  self.photos.removeAllObjects()
  self.currentPage = 1
 
  self.collectionView.reloadData()
 
  refreshControl.endRefreshing()
 
  populatePhotos()
}

The code above simply empties your current All it does is empty your model (self.photos), reset the currentPage, and refresh the UI.

Build and run your app; go to the photo browser and pull to refresh; you should see the newest pictures from 500px.com show up in your app:

PullToRefresh

When you scroll quickly through the photo browser, you’ll notice that you can send cells off the screen whose image requests are still active. In fact, the image request still runs to completion, but the downloaded photo and associated data is just discarded.

Additionally, when you return to earlier cells you have to make a network request again for the photo — even though you just downloaded it a moment ago. You can definitely improve on this bandwidth-wasting design!

You’ll do this by caching retrieved images so they don’t have to be retrieved numerous times; as well, you’ll cancel any in-progress network requests if the associated cell is dequeued before the request completes.

Open PhotoBrowserCollectionViewController.swift and add the following code just above let refreshControl:

let imageCache = NSCache()

This creates an NSCache object that you will use to cache your images.

Next, replace collectionView(_:cellForItemAtIndexPath:) with the following:

override func collectionView(collectionView: UICollectionView, cellForItemAtIndexPath indexPath: NSIndexPath) -> UICollectionViewCell {
  let cell = collectionView.dequeueReusableCellWithReuseIdentifier(PhotoBrowserCellIdentifier, forIndexPath: indexPath) as PhotoBrowserCollectionViewCell
 
  let imageURL = (photos.objectAtIndex(indexPath.row) as PhotoInfo).url
 
  // 1
  if cell.request?.request.URLString != imageURL {
    cell.request?.cancel()
  }
 
  // 2
  if let image = self.imageCache.objectForKey(imageURL) as? UIImage {
    cell.imageView.image = image
  } else {
    // 3
    cell.imageView.image = nil
 
    // 4
    cell.request = Alamofire.request(.GET, imageURL).validate(contentType: ["image/*"]).responseImage() {
      (request, _, image, error) in
      if error == nil && image != nil {
        // 5
        self.imageCache.setObject(image!, forKey: request.URLString)
 
        // 6
        if request.URLString == cell.request?.request.URLString {
          cell.imageView.image = image
        }
      } else {
        /*
        If the cell went off-screen before the image was downloaded, we cancel it and
        an NSURLErrorDomain (-999: cancelled) is returned. This is a normal behavior.
        */
      }
    }
  }
 
  return cell
}

Here’s a comment-by-comment view of what’s happening in the above code:

  1. The dequeued cell may already have an Alamofire request attached to it. Check if this request is relevant — that is, if the request’s URL matches the image URL the cell is supposed to show. If not, then cancel the request.
  2. Use optional binding to check if you have a cached version of this photo. If so, use the cached version instead of downloading it again.
  3. If you don’t have a cached version of the photo, download it. However, the the dequeued cell may be already showing another image; in this case, set it to nil so that the cell is blank while the requested photo is downloaded.
  4. Download the image from the server, but this time validate the content-type of the returned response. If it’s not an image, error will contain a value and therefore you won’t do anything with the potentially invalid image response. The key here is that you you store the Alamofire request object in the cell, for use when your asynchronous network call returns.
  5. If you did not receive an error and you downloaded a proper photo, cache it for later.
  6. Check that the cell hasn’t been dequeued to show a new photo; if not, the set the cell’s image accordingly.

Build and run your app; you’ll note that as you scroll back and forth in the photo browser that your images load a lot faster; you’ve optimized your network requests by pruning unnecessary requests and caching downloaded photos for re-use. Smart use of bandwidth and a snappy user interface make your users very happy, indeed! :]

Note: Some of the cells may appear empty but show up in the full screen view when you tap on them. This isn’t your fault; 500px.com doesn’t have thumbnail versions of some of the images.

Where to Go From Here?

Here’s the completed project from this tutorial series. You’ll see that it has more comments on the UI elements that weren’t covered in the tutorial.

If you followed along with both parts of the tutorial, you now have a good understanding of the most common use cases of networking with Alamofire. You learned about chainable request/response methods, built your own response serializers, created a router and encoded URLs with URL parameter encoding, downloaded a file to disk, and used a progress closure and validated responses. That’s quite a list of achievements! :]

Alamofire can also authenticate with servers using different schemes; as well, it can upload files and streams which weren’t covered in this tutorial. But with your new-found knowledge of Alamofire, learning about these features should be an easy task.

Alamofire is currently not as fully-featured as AFNetworking, but if you’re starting a new project in Swift, using Alamofire is much more enjoyable and covers most common networking use cases. UIKit extensions — one of the most popular features of AFNetworking — are noticeably absent from Alamofire but the two libraries can peacefully co-exist in the same project.

If you’ve used AFNetworking before and can’t live without the setImageWithURL set of category methods on UIKit, you may need to keep using AFNetworking in your projects. For example, you can use Alamofire for your server API calls and then use AFNetworking to display images asynchronously. AFNetworking has a shared cache, so you won’t need to manually cache or cancel your requests.

You can take your tutorial project to the next level in a couple of different ways; you could do away with the consumer key by authenticating with the server using a username and password so you can vote on your favorite photos on 500px.com, or you could add some pagination to the Comments controller to make it a little more friendly. I’d love to see what you come up with!

I hope you enjoyed this tutorial series; if you have any comments or questions about Alamofire or the project in this tutorial, please join the discussion below!

Intermediate Alamofire Tutorial is a post from: Ray Wenderlich

The post Intermediate Alamofire Tutorial appeared first on Ray Wenderlich.


Viewing all articles
Browse latest Browse all 4400

Trending Articles



<script src="https://jsc.adskeeper.com/r/s/rssing.com.1596347.js" async> </script>