Alamofire 入门指南,基于Swift的Http网络工具包

金鱼饭饭 8年前

来自: http://www.raywenderlich.com/121540/alamofire-tutorial-getting-started

Alamofire是一个基于Swift的HTTP网络工具库,适用于iOS和Mac OS X。它在苹果的基础网络协议栈之上提供了一个优雅的接口 简化了一些常见的网络任务。

Alamofire provides chainable response/request methods, JSON parameter and response serialization, authentication, and many other features. In this Alamofire tutorial, you’ll use Alamofire to perform basic networking tasks like uploading files and requesting data from a third-party RESTful API.

Alamofire’s elegance comes from the fact it was written from the ground up in Swift and does not inherit anything from its Objective-C counterpart, AFNetworking.

You should have a conceptual understanding of HTTP networking and some exposure to Apple’s networking classes such as NSURLSession and NSURLConnection .

While Alamofire does obscure some implementation details, it’s good to have some background knowledge if you ever need to troubleshoot your network requests. You’ll also need CocoaPods installed to pull Alamofire into the tutorial project.

Getting Started

Download thestarter project here.

The app for this Alamofire tutorial is named PhotoTagger ; when completed, it will let you select an image from your library (or camera if you’re running on an actual device) and upload the image to a third-party service which will perform some image recognition tasks to come up with a list of tags and primary colors for the image:

Build and run the project in Xcode and you’ll see the following:

Click Select Photo and choose a photo. The background image will be replaced with the image you chose.

Open Main.storyboard and you’ll see the additional screens for displaying tags and colors have been added for you. All that remains is to upload the image and fetch the tags and colors.

The Imagga API

Imagga is an image recognition Platform-as-a-Service that provides image tagging APIs for developers and businesses to build scalable, image-intensive cloud apps. You can play around with a demo of their auto-tagging service here .

You’ll need to create a free developer account with Imagga for this Alamofire tutorial. Imagga requires an authorization header in each HTTP request so only people with an account can use their services. Go to https://imagga.com/auth/signup/hacker and fill out the form. After you create your account, check out the dashboard:

Listed down in the Authorization section is a secret token that you’ll use later. This information needs to be included with every HTTP request as a header.

Note: Make sure you copy the whole secret token, be sure to scroll over to the right and verify you copied everything.

You’ll be using Imagga’s content endpoint to upload the photos, tagging endpoint for the image recognition and colors endpoint for color identification. You can read all about the Imagga API at http://docs.imagga.com .

Installing Dependencies

Create a file named Podfile in the main directory of the project with the following contents:

platform :ios, '9.0'     inhibit_all_warnings!  use_frameworks!     target 'PhotoTagger' do    pod 'Alamofire', '~> 3.1.2'end

Next, install the CocoaPods dependencies with pod install . If you don’t have CocoaPods installed on your machine, check out the How to Use CocoaPods with Swift tutorial for more information.

Close the project and open the newly created PhotoTagger.xcworkspace . Build and run your project – you shouldn’t notice any visual changes in the running app. That’s good – your next task is to add some HTTP calls to a RESTful service to retrieve some JSON.

REST, HTTP, JSON — What’s that?

If you’re coming to this Alamofire tutorial with very little experience in using third-party services over the Internet, you might be wondering what all those acronyms mean! :]

HTTP is the application protocol, or set of rules, that web sites use to transfer data from the web server to your screen. You’ve seen HTTP (or HTTPS) listed in the front of every URL you type into a web browser. You might have heard of other application protocols, such as FTP, Telnet, and SSH. HTTP defines several request methods, or verbs, that the client (your web browser or app) use to indicate the desired action:

  • GET : Used to retrieve data, such as a web page, but doesn’t alter any data on the server.
  • HEAD : Identical to GET but only sends back the headers and none of the actual data.
  • POST : Used to send data to the server, commonly used when filling a form and clicking submit.
  • PUT : Used to send data to the specific location provided.
  • DELETE : Deletes data from the specific location provided.

REST , or REpresentational State Transfer, is a set of rules for designing consistent, easy-to-use and maintainable web APIs. REST has several architecture rules that enforce things such as not persisting states across requests, making requests cacheable, and providing uniform interfaces. This makes it easy for app developers like you to integrate the API into your app — without needing to track the state of data across requests.

JSON stands for JavaScript Object Notation; it provides a straightforward, human-readable and portable mechanism for transporting data between two systems. JSON has a limited number of data types: string, boolean, array, object/dictionary, null and number; there’s no distinction between integers and decimals. Apple supplies the NSJSONSerialization class to help convert your objects in memory to JSON and vice-versa.

The combination of HTTP, REST and JSON make up a good portion of the web services available to you as a developer. Trying to understand how every little piece works can be overwhelming. Libraries like Alamofire can help reduce the complexity of working with these services — and get you up and running faster than you could without its help.

What is Alamofire Good For?

Why do you need Alamofire at all? Apple already provides NSURLSession and other 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 which 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.

There are several major functions available with Alamofire:

  • .upload : Upload files with multipart, stream, file or data methods.
  • .download : Download files or resume a download that was already in progress.
  • .request : Every other HTTP request not associated with file transfers.

These Alamofire functions are scoped to the module, not to a class or struct. There are underlying pieces to Alamofire that are classes and structs, like Manager , Request , and Response ; however, you don’t need to fully understand the entire structure of Alamofire to start using it.

Here’s an example of the same networking operation with both Apple’s NSURLSession and Alamofire’s request function:

// With NSURLSession  public func fetchAllRooms(completion: ([RemoteRoom]?) -> Void) {    let url = NSURL(string: "http://localhost:5984/rooms/_all_docs?include_docs=true")!       let urlRequest = NSMutableURLRequest(      URL: url,      cachePolicy: .ReloadIgnoringLocalAndRemoteCacheData,      timeoutInterval: 10.0 * 1000)    urlRequest.HTTPMethod = "GET"    urlRequest.addValue("application/json", forHTTPHeaderField: "Accept")       let task = urlSession.dataTaskWithRequest(urlRequest)      { (data, response, error) -> Void in      guard error == nil else {        print("Error while fetching remote rooms: \(error)")        completion(nil)        return      }         guard let json = try? NSJSONSerialization.JSONObjectWithData(data!,        options: []) as? [String: AnyObject] else {          print("Nil data received from fetchAllRooms service")          completion(nil)          return      }         guard let rows = json["rows"] as? [[String: AnyObject]] {        print("Malformed data received from fetchAllRooms service")        completion(nil)        return      }         var rooms = [RemoteRoom]()      for roomDict in rows {        rooms.append(RemoteRoom(jsonData: roomDict))      }         completion(rooms)    }       task.resume()  }

Versus:

// With Alamofire  func fetchAllRooms(completion: ([RemoteRoom]?) -> Void) {    Alamofire.request(      .GET,      "http://localhost:5984/rooms/_all_docs",      parameters: ["include_docs": "true"],      encoding: .URL)      .validate()      .responseJSON { (response) -> Void in        guard response.result.isSuccess else {          print("Error while fetching remote rooms: \(response.result.error)")          completion(nil)          return        }           guard let value = response.result.value as? [String: AnyObject],          rows = value["rows"] as? [[String: AnyObject]] else {            print("Malformed data received from fetchAllRooms service")             completion(nil)             return        }           var rooms = [RemoteRoom]()        for roomDict in rows {          rooms.append(RemoteRoom(jsonData: roomDict))        }           completion(rooms)    }  }

You can see that the required setup for Alamofire is shorter and is much more clear as to what the function does. You deserialize the response with responseJSON(options:completionHandler:) and calling validate() on the Response object simplifies error condition handling.

Uploading Files

Open ViewController.swift and add the following class extension to the end of the file:

// Networking calls  extension ViewController {    func uploadImage(image: UIImage, progress: (percent: Float) -> Void,      completion: (tags: [String], colors: [PhotoColor]) -> Void) {      guard let imageData = UIImageJPEGRepresentation(image, 0.5) else {        print("Could not get JPEG representation of UIImage")        return      }    }  }

The first step in uploading an image to Imagga is to get the correct format for the API. Above, the Image Picker API returns a UIImage instance that you convert into a JPEG NSData instance.

Next, go to imagePickerController(_:didFinishPickingMediaWithInfo:) and add the following right after the point where you set the image on imageView :

// 1  takePictureButton.hidden = true  progressView.progress = 0.0  progressView.hidden = false  activityIndicatorView.startAnimating()     uploadImage(    image,    progress: { [unowned self] percent in      // 2      self.progressView.setProgress(percent, animated: true)    },    completion: { [unowned self] tags, colors in      // 3      self.takePictureButton.hidden = false      self.progressView.hidden = true      self.activityIndicatorView.stopAnimating()         self.tags = tags      self.colors = colors         // 4      self.performSegueWithIdentifier("ShowResults", sender: self)  })

Everything with Alamofire is asynchronous , which means the you’ll update UI in an asynchronous manner:

  1. Hide the upload button, and show the progress view and activity view.
  2. While the file uploads, you call the progress handler with an updated percent. This changes the amount of progress bar showing.
  3. The completion handler executes when the upload finishes. This sets the state on the controls back to their original state.
  4. Finally the Storyboard advances to the results screen after a successful (or unsuccessful) upload. The user interface doesn’t change based on the error condition.

Next add the following to the top of ViewController.swift :

import Alamofire

This lets you use the functionality provided by the Alamofire module in your code.

Next, go back to uploadImage(_:progress:completion:) and add the following after the point where you convert the UIImage instance:

Alamofire.upload(    .POST,    "http://api.imagga.com/v1/content",    headers: ["Authorization" : "Basic xxx"],    multipartFormData: { multipartFormData in      multipartFormData.appendBodyPart(data: imageData, name: "imagefile",        fileName: "image.jpg", mimeType: "image/jpeg")    },    encodingCompletion: { encodingResult in    }  )

Make sure to replace Basic xxx with the actual authorization header taken from the Imagga dashboard. Here you convert the JPEG data blob ( imageData ) into a MIME multipart request to the Imagga content endpoint.

Next, add the following to the encodingCompletion closure:

switch encodingResult {  case .Success(let upload, _, _):    upload.progress { bytesWritten, totalBytesWritten, totalBytesExpectedToWrite in      dispatch_async(dispatch_get_main_queue()) {        let percent = (Float(totalBytesWritten) / Float(totalBytesExpectedToWrite))        progress(percent: percent)      }    }    upload.validate()    upload.responseJSON { response in    }  case .Failure(let encodingError):    print(encodingError)  }

This chunk of code calls the Alamofire upload function and passes in a small calculation to update the progress bar as the file uploads. Notice the progress callback is made in a dispatch to the main queue since you’re updating the UI.

Note: Alamofire is not guaranteed to call the progress callback on the main queue; therefore you must guard against updating the UI by dispatching to the main queue. Some of Alamofire’s callbacks, such as responseJSON , are called on the main queue by default. For example:

dispatch_async(queue ?? dispatch_get_main_queue()) {    let response = ...    completionHandler(response)  }

To change this default behavior you would provide a dispatch_queue_t to Alamofire.

</div>

Next, add the following code to upload.responseJSON :

// 1.  guard response.result.isSuccess else {    print("Error while uploading file: \(response.result.error)")    completion(tags: [String](), colors: [PhotoColor]())    return  }// 2.  guard let responseJSON = response.result.value as? [String: AnyObject],    uploadedFiles = responseJSON["uploaded"] as? [AnyObject],    firstFile = uploadedFiles.first as? [String: AnyObject],    firstFileID = firstFile["id"] as? String else {      print("Invalid information received from service")      completion(tags: [String](), colors: [PhotoColor]())      return  }     print("Content uploaded with ID: \(firstFileID)")// 3.  completion(tags: [String](), colors: [PhotoColor]())

Here’s a step-by-step explanation of the above code:

  1. Check if the response was successful; if not, print the error and call the completion handler.
  2. Check each portion of the response, verifying the expected type is the actual type received. Retrieve the firstFileID from the response. If firstFileID cannot be resolved, print out an error message and call the completion handler.
  3. Call the completion handler to update the UI. At this point, you don’t have any downloaded tags or colors, so simply call this with empty data.

Note: Every response has a Result enum with a value and type. Using automatic validation, the result is considered a success when it returns a valid HTTP Code between 200 and 299 and the Content Type is of a valid type specified in the Accept HTTP header field.

You can perform manual validation by adding .validate options as shown below:

Alamofire.request(.GET, "https://httpbin.org/get", parameters: ["foo": "bar"])    .validate(statusCode: 200..<300)    .validate(contentType: ["application/json"])    .response { response in      // response handling code  }
</div>

The UI won’t show an error if you hit an error during the upload; it merely returns no tags or colors to the user. This isn’t the best user experience, but it’s fine for this Alamofire tutorial.

Build and run your project; select an image and watch the progress bar change as the file uploads. You should see a note like the following in your console when the upload completes:

You’ve successfully uploaded a file over the Interwebs!

Retrieving Data

The next step after uploading the image to Imagga is to fetch the tags Imagga produces after it analyzes the photo.

Add the following method to the ViewController extension below uploadImage(_:progress:completion:) :

func downloadTags(contentID: String, completion: ([String]) -> Void) {    Alamofire.request(      .GET,      "http://api.imagga.com/v1/tagging",      parameters: ["content": contentID],      headers: ["Authorization" : "Basic xxx"]      )      .responseJSON { response in        guard response.result.isSuccess else {          print("Error while fetching tags: \(response.result.error)")          completion([String]())          return        }           guard let responseJSON = response.result.value as? [String: AnyObject] else {           print("Invalid tag information received from service")           completion([String]())           return        }        print(responseJSON)        completion([String]())    }  }

Again, be sure to replace Basic xxx with your actual authorization header. Here you perform an HTTP GET request against the tagging endpoint, sending the URL parameter content with the ID you received after the upload.

Next, go back to uploadImage(_:progress:completion:) and replace the call to the completion handler in the success condition with the following:

self.downloadTags(firstFileID) { tags in    completion(tags: tags, colors: [PhotoColor]())  }

This simply sends along the tags to the completion handler.

Build and run your project; upload a file and you’ll see a dump of the data in the console:

You don’t care about the confidence score for this Alamofire tutorial, only the array of tag names.

Next, go back to downloadTags(_:completion:) and replace the code inside .responseJSON with the following:

// 1.  guard response.result.isSuccess else {    print("Error while fetching tags: \(response.result.error)")    completion([String]())    return  }   // 2.  guard let responseJSON = response.result.value as? [String: AnyObject],    results = responseJSON["results"] as? [AnyObject],    firstResult = results.first,    tagsAndConfidences = firstResult["tags"] as? [[String: AnyObject]] else {      print("Invalid tag information received from the service")      completion([String]())      return  }   // 3.  let tags = tagsAndConfidences.flatMap({ dict in    return dict["tag"] as? String  })   // 4.  completion(tags)

Here’s a step-by-step explanation of the above code:

  1. Check if the response was successful; if not, print the error and call the completion handler.
  2. Check each portion of the response, verifying the expected type is the actual type received. Retrieve the tagsAndConfidences information from the response. If tagsAndConfidences cannot be resolved, print out an error message and call the completion handler.
  3. Iterate over each dictionary object in the tagsAndConfidences array, retrieving the value associated with the tag key.
  4. Call the completion handler passing in the tags received from the service.

Note: You’re using the Swift flatMap method to iterate over each dictionary in the tagsAndConfidences array; this method handles nil values without crashing and will remove those values from the returned result. This lets you use conditional unwrapping ( as? ) to verify whether the dictionary value can be converted to a String .

Build and run your project again; select a photo and you should see the following appear:

Pretty slick! That Immaga is one smart API. :] Next, you’ll fetch the colors of the image.

Add the following method to the ViewController extension below downloadTags(_:completion:) :

func downloadColors(contentID: String, completion: ([PhotoColor]) -> Void) {    Alamofire.request(      .GET,      "http://api.imagga.com/v1/colors",      parameters: ["content": contentID, "extract_object_colors": NSNumber(int: 0)],      // 1.      headers: ["Authorization" : "Basic xxx"]      )      .responseJSON { response in        // 2.        guard response.result.isSuccess else {          print("Error while fetching colors: \(response.result.error)")          completion([PhotoColor]())          return        }           // 3.        guard let responseJSON = response.result.value as? [String: AnyObject],          results = responseJSON["results"] as? [AnyObject],          firstResult = results.first as? [String: AnyObject],          info = firstResult["info"] as? [String: AnyObject],          imageColors = info["image_colors"] as? [[String: AnyObject]] else {            print("Invalid color information received from service")            completion([PhotoColor]())            return        }           // 4.        let photoColors = imageColors.flatMap({ (dict) -> PhotoColor? in          guard let r = dict["r"] as? String,            g = dict["g"] as? String,            b = dict["b"] as? String,            closestPaletteColor = dict["closest_palette_color"] as? String else {              return nil          }          return PhotoColor(red: Int(r),            green: Int(g),            blue: Int(b),            colorName: closestPaletteColor)        })           // 5.        completion(photoColors)    }  }

Taking each numbered comment in turn:

  1. Be sure to replace Basic xxx with your actual authorization header.
  2. Check that the response was successful; if not, print the error and call the completion handler.
  3. Check each portion of the response, verifying the expected type is the actual type received. Retrieve the imageColors information from the response. If imageColors cannot be resolved, print out an error message and call the completion handler.
  4. Using flatMap again, you iterate over the returned imageColors , transforming the data into PhotoColor objects which pairs colors in the RGB format with the color name as a string. Note the provided closure allows returning nil values since flatMap will simply ignore them.
  5. Call the completion handler, passing in the photoColors from the service.

Finally, go back to uploadImage(_:progress:completion:) and replace the call to the completion handler in the success condition with the following:

self.downloadTags(firstFileID) { tags in    self.downloadColors(firstFileID) { colors in      completion(tags: tags, colors: colors)    }  }

This nests the operations of uploading the image, downloading tags and downloading colors.

Build and run your project again; this time, you should see the returned color tags:

You use the RGB colors you mapped to PhotoColor structs to change the background color of the view. You’ve now successfully uploaded an image to Imagga and fetched data from two different endpoints. You’ve come a long way — but there’s some room for improvement in how you’re using Alamofire in PhotoTagger.

Improving PhotoTagger

You probably noticed some repeated code in PhotoTagger. If Imagga released v2 of their API and deprecated v1, PhotoTagger would no longer function and you’d have to update the URL in each of the three methods. Similarly, if your authorization token changed you’d be updating it all over the place.

Alamofire provides a simple method to eliminate this code duplication and provide centralized configuration. The technique involves creating a struct conforming to the URLRequestConvertible protocol and updating your upload and request calls.

Create a new Swift file by clicking File\New\File… and selecting Swift file under iOS . Click Next , name the file ImaggaRouter.swift , select the Group PhotoTagger with the yellow folder icon and click Create .

Replace the contents of your new file with the following:

import Foundation  import Alamofire     public enum ImaggaRouter: URLRequestConvertible {    static let baseURLPath = "http://api.imagga.com/v1"    static let authenticationToken = "Basic xxx"       case Content    case Tags(String)    case Colors(String)       public var URLRequest: NSMutableURLRequest {      let result: (path: String, method: Alamofire.Method, parameters: [String: AnyObject]) = {        switch self {        case .Content:          return ("/content", .POST, [String: AnyObject]())        case .Tags(let contentID):          let params = [ "content" : contentID ]          return ("/tagging", .GET, params)        case .Colors(let contentID):          let params = [ "content" : contentID, "extract_object_colors" : NSNumber(int: 0) ]          return ("/colors", .GET, params)        }      }()         let URL = NSURL(string: ImaggaRouter.baseURLPath)!      let URLRequest = NSMutableURLRequest(URL: URL.URLByAppendingPathComponent(result.path))      URLRequest.HTTPMethod = result.method.rawValue      URLRequest.setValue(ImaggaRouter.authenticationToken, forHTTPHeaderField: "Authorization")      URLRequest.timeoutInterval = NSTimeInterval(10 * 1000)         let encoding = Alamofire.ParameterEncoding.URL         return encoding.encode(URLRequest, parameters: result.parameters).0    }  }

Replace Basic xxx with your actual authorization header. This router helps create instances of NSMutableURLRequest by providing it one of the three cases: .Content , .Tags(String) , or .Colors(String) . Now all of your boilerplate code is in single place, should you ever need to update it.

Go back to uploadImage(_:progress:completion:) and replace the beginning of the call to Alamofire.upload with the following:

Alamofire.upload(    ImaggaRouter.Content,    multipartFormData: { multipartFormData in      multipartFormData.appendBodyPart(data: imageData, name: "imagefile",        fileName: "image.jpg", mimeType: "image/jpeg")  },/// original code continues...

Next replace the call for Alamofire.request in downloadTags(_:completion:) with:

Alamofire.request(ImaggaRouter.Tags(contentID))

Finally, update the call to Alamofire.request in downloadColors(_:completion:) with:

Alamofire.request(ImaggaRouter.Colors(contentID))

Build and run for the final time; everything should function just as before, which means you’ve refactored everything without breaking your app. Awesome job!

Where To Go From Here?

You can download the completed version of this Alamofire tutorial’s project here . Don’t forget to replace your authorization token as appropriate!

This Alamofire tutorial covered the very basics. You can take a deeper dive by looking at the documentation on the Alamofire site at https://github.com/Alamofire/Alamofire .

Also, you can take some time to learn more about Apple’s NSURLSession which Alamofire uses under the hood:

Please share any comments or questions about this Alamofire tutorial in the forum discussion below!

</div>