Alamofire: Elegant Networking in Swift

Build Status CocoaPods Compatible Carthage Compatible Platform Twitter Gitter

Alamofire is an HTTP networking library written in Swift.

Features

  • Chainable Request / Response Methods
  • URL / JSON / plist Parameter Encoding
  • Upload File / Data / Stream / MultipartFormData
  • Download File using Request or Resume Data
  • Authentication with URLCredential
  • HTTP Response Validation
  • Upload and Download Progress Closures with Progress
  • cURL Command Output
  • Dynamically Adapt and Retry Requests
  • TLS Certificate and Public Key Pinning
  • Network Reachability
  • Comprehensive Unit and Integration Test Coverage
  • Complete Documentation

Component Libraries

In order to keep Alamofire focused specifically on core networking implementations, additional component libraries have been created by the Alamofire Software Foundation to bring additional functionality to the Alamofire ecosystem.

  • AlamofireImage - An image library including image response serializers, UIImage and UIImageView extensions, custom image filters, an auto-purging in-memory cache and a priority-based image downloading system.
  • AlamofireNetworkActivityIndicator - Controls the visibility of the network activity indicator on iOS using Alamofire. It contains configurable delay timers to help mitigate flicker and can support URLSession instances not managed by Alamofire.

Requirements

  • iOS 8.0+ / macOS 10.10+ / tvOS 9.0+ / watchOS 2.0+
  • Xcode 8.1+
  • Swift 3.0+

Migration Guides

Communication

  • If you need help, use Stack Overflow. (Tag 'alamofire')
  • If you'd like to ask a general question, use Stack Overflow.
  • If you found a bug, open an issue.
  • If you have a feature request, open an issue.
  • If you want to contribute, submit a pull request.

Installation

CocoaPods

CocoaPods is a dependency manager for Cocoa projects. You can install it with the following command:

$ gem install cocoapods

CocoaPods 1.1.0+ is required to build Alamofire 4.0.0+.

To integrate Alamofire into your Xcode project using CocoaPods, specify it in your Podfile:

source 'https://github.com/CocoaPods/Specs.git'
platform :ios, '10.0'
use_frameworks!

target '<Your Target Name>' do
    pod 'Alamofire', '~> 4.4'
end

Then, run the following command:

$ pod install

Carthage

Carthage is a decentralized dependency manager that builds your dependencies and provides you with binary frameworks.

You can install Carthage with Homebrew using the following command:

$ brew update
$ brew install carthage

To integrate Alamofire into your Xcode project using Carthage, specify it in your Cartfile:

github "Alamofire/Alamofire" ~> 4.4

Run carthage update to build the framework and drag the built Alamofire.framework into your Xcode project.

Swift Package Manager

The Swift Package Manager is a tool for automating the distribution of Swift code and is integrated into the swift compiler. It is in early development, but Alamofire does support its use on supported platforms.

Once you have your Swift package set up, adding Alamofire as a dependency is as easy as adding it to the dependencies value of your Package.swift.

dependencies: [
    .Package(url: "https://github.com/Alamofire/Alamofire.git", majorVersion: 4)
]

Manually

If you prefer not to use either of the aforementioned dependency managers, you can integrate Alamofire into your project manually.

Embedded Framework

  • Open up Terminal, cd into your top-level project directory, and run the following command "if" your project is not initialized as a git repository:

$ git init


- Add Alamofire as a git [submodule](http://git-scm.com/docs/git-submodule) by running the following command:

  ```bash
$ git submodule add https://github.com/Alamofire/Alamofire.git
  • Open the new Alamofire folder, and drag the Alamofire.xcodeproj into the Project Navigator of your application's Xcode project.

    It should appear nested underneath your application's blue project icon. Whether it is above or below all the other Xcode groups does not matter.

  • Select the Alamofire.xcodeproj in the Project Navigator and verify the deployment target matches that of your application target.

  • Next, select your application project in the Project Navigator (blue project icon) to navigate to the target configuration window and select the application target under the "Targets" heading in the sidebar.

  • In the tab bar at the top of that window, open the "General" panel.

  • Click on the + button under the "Embedded Binaries" section.

  • You will see two different Alamofire.xcodeproj folders each with two different versions of the Alamofire.framework nested inside a Products folder.

    It does not matter which Products folder you choose from, but it does matter whether you choose the top or bottom Alamofire.framework.

  • Select the top Alamofire.framework for iOS and the bottom one for OS X.

    You can verify which one you selected by inspecting the build log for your project. The build target for Alamofire will be listed as either Alamofire iOS, Alamofire macOS, Alamofire tvOS or Alamofire watchOS.

  • And that's it!

    The Alamofire.framework is automagically added as a target dependency, linked framework and embedded framework in a copy files build phase which is all you need to build on the simulator and a device.


Usage

Making a Request

import Alamofire

Alamofire.request("https://httpbin.org/get")

Response Handling

Handling the Response of a Request made in Alamofire involves chaining a response handler onto the Request.

Alamofire.request("https://httpbin.org/get").responseJSON { response in
    print(response.request)  // original URL request
    print(response.response) // HTTP URL response
    print(response.data)     // server data
    print(response.result)   // result of response serialization

    if let JSON = response.result.value {
        print("JSON: \(JSON)")
    }
}

In the above example, the responseJSON handler is appended to the Request to be executed once the Request is complete. Rather than blocking execution to wait for a response from the server, a callback in the form of a closure is specified to handle the response once it's received. The result of a request is only available inside the scope of a response closure. Any execution contingent on the response or data received from the server must be done within a response closure.

Networking in Alamofire is done asynchronously. Asynchronous programming may be a source of frustration to programmers unfamiliar with the concept, but there are very good reasons for doing it this way.

Alamofire contains five different response handlers by default including:

// Response Handler - Unserialized Response
func response(
    queue: DispatchQueue?,
    completionHandler: @escaping (DefaultDataResponse) -> Void)
    -> Self

// Response Data Handler - Serialized into Data
func responseData(
    queue: DispatchQueue?,
    completionHandler: @escaping (DataResponse<Data>) -> Void)
    -> Self

// Response String Handler - Serialized into String
func responseString(
    queue: DispatchQueue?,
    encoding: String.Encoding?,
    completionHandler: @escaping (DataResponse<String>) -> Void)
    -> Self

// Response JSON Handler - Serialized into Any
func responseJSON(
    queue: DispatchQueue?,
    completionHandler: @escaping (DataResponse<Any>) -> Void)
    -> Self

// Response PropertyList (plist) Handler - Serialized into Any
func responsePropertyList(
    queue: DispatchQueue?,
    completionHandler: @escaping (DataResponse<Any>) -> Void))
    -> Self

None of the response handlers perform any validation of the HTTPURLResponse it gets back from the server.

For example, response status codes in the 400..<499 and 500..<599 ranges do NOT automatically trigger an Error. Alamofire uses Response Validation method chaining to achieve this.

Response Handler

The response handler does NOT evaluate any of the response data. It merely forwards on all information directly from the URL session delegate. It is the Alamofire equivalent of using cURL to execute a Request.

Alamofire.request("https://httpbin.org/get").response { response in
    print("Request: \(response.request)")
    print("Response: \(response.response)")
    print("Error: \(response.error)")

    if let data = response.data, let utf8Text = String(data: data, encoding: .utf8) {
        print("Data: \(utf8Text)")
    }
}

We strongly encourage you to leverage the other response serializers taking advantage of Response and Result types.

Response Data Handler

The responseData handler uses the responseDataSerializer (the object that serializes the server data into some other type) to extract the Data returned by the server. If no errors occur and Data is returned, the response Result will be a .success and the value will be of type Data.

Alamofire.request("https://httpbin.org/get").responseData { response in
    debugPrint("All Response Info: \(response)")

    if let data = response.result.value, let utf8Text = String(data: data, encoding: .utf8) {
        print("Data: \(utf8Text)")
    }
}

Response String Handler

The responseString handler uses the responseStringSerializer to convert the Data returned by the server into a String with the specified encoding. If no errors occur and the server data is successfully serialized into a String, the response Result will be a .success and the value will be of type String.

Alamofire.request("https://httpbin.org/get").responseString { response in
    print("Success: \(response.result.isSuccess)")
    print("Response String: \(response.result.value)")
}

If no encoding is specified, Alamofire will use the text encoding specified in the HTTPURLResponse from the server. If the text encoding cannot be determined by the server response, it defaults to .isoLatin1.

Response JSON Handler

The responseJSON handler uses the responseJSONSerializer to convert the Data returned by the server into an Any type using the specified JSONSerialization.ReadingOptions. If no errors occur and the server data is successfully serialized into a JSON object, the response Result will be a .success and the value will be of type Any.

Alamofire.request("https://httpbin.org/get").responseJSON { response in
    debugPrint(response)

    if let json = response.result.value {
        print("JSON: \(json)")
    }
}

All JSON serialization is handled by the JSONSerialization API in the Foundation framework.

Chained Response Handlers

Response handlers can even be chained:

Alamofire.request("https://httpbin.org/get")
    .responseString { response in
        print("Response String: \(response.result.value)")
    }
    .responseJSON { response in
        print("Response JSON: \(response.result.value)")
    }

It is important to note that using multiple response handlers on the same Request requires the server data to be serialized multiple times. Once for each response handler.

Response Handler Queue

Response handlers by default are executed on the main dispatch queue. However, a custom dispatch queue can be provided instead.

let utilityQueue = DispatchQueue.global(qos: .utility)

Alamofire.request("https://httpbin.org/get").responseJSON(queue: utilityQueue) { response in
    print("Executing response handler on utility queue")
}

Response Validation

By default, Alamofire treats any completed request to be successful, regardless of the content of the response. Calling validate before a response handler causes an error to be generated if the response had an unacceptable status code or MIME type.

Manual Validation

Alamofire.request("https://httpbin.org/get")
    .validate(statusCode: 200..<300)
    .validate(contentType: ["application/json"])
    .responseData { response in
        switch response.result {
        case .success:
            print("Validation Successful")
        case .failure(let error):
            print(error)
        }
    }

Automatic Validation

Automatically validates status code within 200...299 range, and that the Content-Type header of the response matches the Accept header of the request, if one is provided.

Alamofire.request("https://httpbin.org/get").validate().responseJSON { response in
    switch response.result {
    case .success:
        print("Validation Successful")
    case .failure(let error):
        print(error)
    }
}

Response Caching

Response Caching is handled on the system framework level by URLCache. It provides a composite in-memory and on-disk cache and lets you manipulate the sizes of both the in-memory and on-disk portions.

By default, Alamofire leverages the shared URLCache. In order to customize it, see the Session Manager Configurations section.

HTTP Methods

The HTTPMethod enumeration lists the HTTP methods defined in RFC 7231 §4.3:

public enum HTTPMethod: String {
    case options = "OPTIONS"
    case get     = "GET"
    case head    = "HEAD"
    case post    = "POST"
    case put     = "PUT"
    case patch   = "PATCH"
    case delete  = "DELETE"
    case trace   = "TRACE"
    case connect = "CONNECT"
}

These values can be passed as the method argument to the Alamofire.request API:

Alamofire.request("https://httpbin.org/get") // method defaults to `.get`

Alamofire.request("https://httpbin.org/post", method: .post)
Alamofire.request("https://httpbin.org/put", method: .put)
Alamofire.request("https://httpbin.org/delete", method: .delete)

The Alamofire.request method parameter defaults to .get.

Parameter Encoding

Alamofire supports three types of parameter encoding including: URL, JSON and PropertyList. It can also support any custom encoding that conforms to the ParameterEncoding protocol.

URL Encoding

The URLEncoding type creates a url-encoded query string to be set as or appended to any existing URL query string or set as the HTTP body of the URL request. Whether the query string is set or appended to any existing URL query string or set as the HTTP body depends on the Destination of the encoding. The Destination enumeration has three cases:

  • .methodDependent - Applies encoded query string result to existing query string for GET, HEAD and DELETE requests and sets as the HTTP body for requests with any other HTTP method.
  • .queryString - Sets or appends encoded query string result to existing query string.
  • .httpBody - Sets encoded query string result as the HTTP body of the URL request.

The Content-Type HTTP header field of an encoded request with HTTP body is set to application/x-www-form-urlencoded; charset=utf-8. Since there is no published specification for how to encode collection types, the convention of appending [] to the key for array values (foo[]=1&foo[]=2), and appending the key surrounded by square brackets for nested dictionary values (foo[bar]=baz).

GET Request With URL-Encoded Parameters
let parameters: Parameters = ["foo": "bar"]

// All three of these calls are equivalent
Alamofire.request("https://httpbin.org/get", parameters: parameters) // encoding defaults to `URLEncoding.default`
Alamofire.request("https://httpbin.org/get", parameters: parameters, encoding: URLEncoding.default)
Alamofire.request("https://httpbin.org/get", parameters: parameters, encoding: URLEncoding(destination: .methodDependent))

// https://httpbin.org/get?foo=bar
POST Request With URL-Encoded Parameters
let parameters: Parameters = [
    "foo": "bar",
    "baz": ["a", 1],
    "qux": [
        "x": 1,
        "y": 2,
        "z": 3
    ]
]

// All three of these calls are equivalent
Alamofire.request("https://httpbin.org/post", method: .post, parameters: parameters)
Alamofire.request("https://httpbin.org/post", method: .post, parameters: parameters, encoding: URLEncoding.default)
Alamofire.request("https://httpbin.org/post", method: .post, parameters: parameters, encoding: URLEncoding.httpBody)

// HTTP body: foo=bar&baz[]=a&baz[]=1&qux[x]=1&qux[y]=2&qux[z]=3

JSON Encoding

The JSONEncoding type creates a JSON representation of the parameters object, which is set as the HTTP body of the request. The Content-Type HTTP header field of an encoded request is set to application/json.

POST Request with JSON-Encoded Parameters
let parameters: Parameters = [
    "foo": [1,2,3],
    "bar": [
        "baz": "qux"
    ]
]

// Both calls are equivalent
Alamofire.request("https://httpbin.org/post", method: .post, parameters: parameters, encoding: JSONEncoding.default)
Alamofire.request("https://httpbin.org/post", method: .post, parameters: parameters, encoding: JSONEncoding(options: []))

// HTTP body: {"foo": [1, 2, 3], "bar": {"baz": "qux"}}

Property List Encoding

The PropertyListEncoding uses PropertyListSerialization to create a plist representation of the parameters object, according to the associated format and write options values, which is set as the body of the request. The Content-Type HTTP header field of an encoded request is set to application/x-plist.

Custom Encoding

In the event that the provided ParameterEncoding types do not meet your needs, you can create your own custom encoding. Here's a quick example of how you could build a custom JSONStringArrayEncoding type to encode a JSON string array onto a Request.

struct JSONStringArrayEncoding: ParameterEncoding {
    private let array: [String]

    init(array: [String]) {
        self.array = array
    }

    func encode(_ urlRequest: URLRequestConvertible, with parameters: Parameters?) throws -> URLRequest {
        var urlRequest = urlRequest.urlRequest

        let data = try JSONSerialization.data(withJSONObject: array, options: [])

        if urlRequest.value(forHTTPHeaderField: "Content-Type") == nil {
            urlRequest.setValue("application/json", forHTTPHeaderField: "Content-Type")
        }

        urlRequest.httpBody = data

        return urlRequest
    }
}

Manual Parameter Encoding of a URLRequest

The ParameterEncoding APIs can be used outside of making network requests.

let url = URL(string: "https://httpbin.org/get")!
var urlRequest = URLRequest(url: url)

let parameters: Parameters = ["foo": "bar"]
let encodedURLRequest = try URLEncoding.queryString.encode(urlRequest, with: parameters)

HTTP Headers

Adding a custom HTTP header to a Request is supported directly in the global request method. This makes it easy to attach HTTP headers to a Request that can be constantly changing.

let headers: HTTPHeaders = [
    "Authorization": "Basic QWxhZGRpbjpvcGVuIHNlc2FtZQ==",
    "Accept": "application/json"
]

Alamofire.request("https://httpbin.org/headers", headers: headers).responseJSON { response in
    debugPrint(response)
}

For HTTP headers that do not change, it is recommended to set them on the URLSessionConfiguration so they are automatically applied to any URLSessionTask created by the underlying URLSession. For more information, see the Session Manager Configurations section.

The default Alamofire SessionManager provides a default set of headers for every Request. These include:

  • Accept-Encoding, which defaults to gzip;q=1.0, compress;q=0.5, per RFC 7230 §4.2.3.
  • Accept-Language, which defaults to up to the top 6 preferred languages on the system, formatted like en;q=1.0, per RFC 7231 §5.3.5.
  • User-Agent, which contains versioning information about the current app. For example: iOS Example/1.0 (com.alamofire.iOS-Example; build:1; iOS 10.0.0) Alamofire/4.0.0, per RFC 7231 §5.5.3.

If you need to customize these headers, a custom URLSessionConfiguration should be created, the defaultHTTPHeaders property updated and the configuration applied to a new SessionManager instance.

Authentication

Authentication is handled on the system framework level by URLCredential and URLAuthenticationChallenge.

Supported Authentication Schemes

HTTP Basic Authentication

The authenticate method on a Request will automatically provide a URLCredential to a URLAuthenticationChallenge when appropriate:

let user = "user"
let password = "password"

Alamofire.request("https://httpbin.org/basic-auth/\(user)/\(password)")
    .authenticate(user: user, password: password)
    .responseJSON { response in
        debugPrint(response)
    }

Depending upon your server implementation, an Authorization header may also be appropriate:

let user = "user"
let password = "password"

var headers: HTTPHeaders = [:]

if let authorizationHeader = Request.authorizationHeader(user: user, password: password) {
    headers[authorizationHeader.key] = authorizationHeader.value
}

Alamofire.request("https://httpbin.org/basic-auth/user/password", headers: headers)
    .responseJSON { response in
        debugPrint(response)
    }

Authentication with URLCredential

let user = "user"
let password = "password"

let credential = URLCredential(user: user, password: password, persistence: .forSession)

Alamofire.request("https://httpbin.org/basic-auth/\(user)/\(password)")
    .authenticate(usingCredential: credential)
    .responseJSON { response in
        debugPrint(response)
    }

It is important to note that when using a URLCredential for authentication, the underlying URLSession will actually end up making two requests if a challenge is issued by the server. The first request will not include the credential which "may" trigger a challenge from the server. The challenge is then received by Alamofire, the credential is appended and the request is retried by the underlying URLSession.

Downloading Data to a File

Requests made in Alamofire that fetch data from a server can download the data in-memory or on-disk. The Alamofire.request APIs used in all the examples so far always downloads the server data in-memory. This is great for smaller payloads because it's more efficient, but really bad for larger payloads because the download could run your entire application out-of-memory. Because of this, you can also use the Alamofire.download APIs to download the server data to a temporary file on-disk.

This will only work on macOS as is. Other platforms don't allow access to the filesystem outside of your app's sandbox. To download files on other platforms, see the Download File Destination section.

Alamofire.download("https://httpbin.org/image/png").responseData { response in
    if let data = response.result.value {
        let image = UIImage(data: data)
    }
}

The Alamofire.download APIs should also be used if you need to download data while your app is in the background. For more information, please see the Session Manager Configurations section.

Download File Destination

You can also provide a DownloadFileDestination closure to move the file from the temporary directory to a final destination. Before the temporary file is actually moved to the destinationURL, the DownloadOptions specified in the closure will be executed. The two currently supported DownloadOptions are:

  • .createIntermediateDirectories - Creates intermediate directories for the destination URL if specified.
  • .removePreviousFile - Removes a previous file from the destination URL if specified.
let destination: DownloadRequest.DownloadFileDestination = { _, _ in
    let documentsURL = FileManager.default.urls(for: .documentDirectory, in: .userDomainMask)[0]
    let fileURL = documentsURL.appendPathComponent("pig.png")

    return (fileURL, [.removePreviousFile, .createIntermediateDirectories])
}

Alamofire.download(urlString, to: destination).response { response in
    print(response)

    if response.error == nil, let imagePath = response.destinationURL?.path {
        let image = UIImage(contentsOfFile: imagePath)
    }
}

You can also use the suggested download destination API.

let destination = DownloadRequest.suggestedDownloadDestination(directory: .documentDirectory)
Alamofire.download("https://httpbin.org/image/png", to: destination)

Download Progress

Many times it can be helpful to report download progress to the user. Any DownloadRequest can report download progress using the downloadProgress API.

Alamofire.download("https://httpbin.org/image/png")
    .downloadProgress { progress in
        print("Download Progress: \(progress.fractionCompleted)")
    }
    .responseData { response in
        if let data = response.result.value {
            let image = UIImage(data: data)
        }
    }

The downloadProgress API also takes a queue parameter which defines which DispatchQueue the download progress closure should be called on.

let utilityQueue = DispatchQueue.global(qos: .utility)

Alamofire.download("https://httpbin.org/image/png")
    .downloadProgress(queue: utilityQueue) { progress in
        print("Download Progress: \(progress.fractionCompleted)")
    }
    .responseData { response in
        if let data = response.result.value {
            let image = UIImage(data: data)
        }
    }

Resuming a Download

If a DownloadRequest is cancelled or interrupted, the underlying URL session may generate resume data for the active DownloadRequest. If this happens, the resume data can be re-used to restart the DownloadRequest where it left off. The resume data can be accessed through the download response, then reused when trying to restart the request.

IMPORTANT: On the latest release of all the Apple platforms (iOS 10, macOS 10.12, tvOS 10, watchOS 3), resumeData is broken on background URL session configurations. There's an underlying bug in the resumeData generation logic where the data is written incorrectly and will always fail to resume the download. For more information about the bug and possible workarounds, please see this Stack Overflow post.

class ImageRequestor {
    private var resumeData: Data?
    private var image: UIImage?

    func fetchImage(completion: (UIImage?) -> Void) {
        guard image == nil else { completion(image) ; return }

        let destination: DownloadRequest.DownloadFileDestination = { _, _ in
            let documentsURL = FileManager.default.urls(for: .documentDirectory, in: .userDomainMask)[0]
            let fileURL = documentsURL.appendPathComponent("pig.png")

            return (fileURL, [.removePreviousFile, .createIntermediateDirectories])
        }

        let request: DownloadRequest

        if let resumeData = resumeData {
            request = Alamofire.download(resumingWith: resumeData)
        } else {
            request = Alamofire.download("https://httpbin.org/image/png")
        }

        request.responseData { response in
            switch response.result {
            case .success(let data):
                self.image = UIImage(data: data)
            case .failure:
                self.resumeData = response.resumeData
            }
        }
    }
}

Uploading Data to a Server

When sending relatively small amounts of data to a server using JSON or URL encoded parameters, the Alamofire.request APIs are usually sufficient. If you need to send much larger amounts of data from a file URL or an InputStream, then the Alamofire.upload APIs are what you want to use.

The Alamofire.upload APIs should also be used if you need to upload data while your app is in the background. For more information, please see the Session Manager Configurations section.

Uploading Data

let imageData = UIPNGRepresentation(image)!

Alamofire.upload(imageData, to: "https://httpbin.org/post").responseJSON { response in
    debugPrint(response)
}

Uploading a File

let fileURL = Bundle.main.url(forResource: "video", withExtension: "mov")

Alamofire.upload(fileURL, to: "https://httpbin.org/post").responseJSON { response in
    debugPrint(response)
}

Uploading Multipart Form Data

Alamofire.upload(
    multipartFormData: { multipartFormData in
        multipartFormData.append(unicornImageURL, withName: "unicorn")
        multipartFormData.append(rainbowImageURL, withName: "rainbow")
    },
    to: "https://httpbin.org/post",
    encodingCompletion: { encodingResult in
        switch encodingResult {
        case .success(let upload, _, _):
            upload.responseJSON { response in
                debugPrint(response)
            }
        case .failure(let encodingError):
            print(encodingError)
        }
    }
)

Upload Progress

While your user is waiting for their upload to complete, sometimes it can be handy to show the progress of the upload to the user. Any UploadRequest can report both upload progress and download progress of the response data using the uploadProgress and downloadProgress APIs.

let fileURL = Bundle.main.url(forResource: "video", withExtension: "mov")

Alamofire.upload(fileURL, to: "https://httpbin.org/post")
    .uploadProgress { progress in // main queue by default
        print("Upload Progress: \(progress.fractionCompleted)")
    }
    .downloadProgress { progress in // main queue by default
        print("Download Progress: \(progress.fractionCompleted)")
    }
    .responseJSON { response in
        debugPrint(response)
    }

Statistical Metrics

Timeline

Alamofire collects timings throughout the lifecycle of a Request and creates a Timeline object exposed as a property on all response types.

Alamofire.request("https://httpbin.org/get").responseJSON { response in
    print(response.timeline)
}

The above reports the following Timeline info:

  • Latency: 0.428 seconds
  • Request Duration: 0.428 seconds
  • Serialization Duration: 0.001 seconds
  • Total Duration: 0.429 seconds

URL Session Task Metrics

In iOS and tvOS 10 and macOS 10.12, Apple introduced the new URLSessionTaskMetrics APIs. The task metrics encapsulate some fantastic statistical information about the request and response execution. The API is very similar to the Timeline, but provides many more statistics that Alamofire doesn't have access to compute. The metrics can be accessed through any response type.

Alamofire.request("https://httpbin.org/get").responseJSON { response in
    print(response.metrics)
}

It's important to note that these APIs are only available on iOS and tvOS 10 and macOS 10.12. Therefore, depending on your deployment target, you may need to use these inside availability checks:

Alamofire.request("https://httpbin.org/get").responseJSON { response in
    if #available(iOS 10.0. *) {
        print(response.metrics)
    }
}

cURL Command Output

Debugging platform issues can be frustrating. Thankfully, Alamofire Request objects conform to both the CustomStringConvertible and CustomDebugStringConvertible protocols to provide some VERY helpful debugging tools.

CustomStringConvertible

let request = Alamofire.request("https://httpbin.org/ip")

print(request)
// GET https://httpbin.org/ip (200)

CustomDebugStringConvertible

let request = Alamofire.request("https://httpbin.org/get", parameters: ["foo": "bar"])
debugPrint(request)

Outputs:

$ curl -i \
    -H "User-Agent: Alamofire/4.0.0" \
    -H "Accept-Encoding: gzip;q=1.0, compress;q=0.5" \
    -H "Accept-Language: en;q=1.0,fr;q=0.9,de;q=0.8,zh-Hans;q=0.7,zh-Hant;q=0.6,ja;q=0.5" \
    "https://httpbin.org/get?foo=bar"

Advanced Usage

Alamofire is built on URLSession and the Foundation URL Loading System. To make the most of this framework, it is recommended that you be familiar with the concepts and capabilities of the underlying networking stack.

Recommended Reading

Session Manager

Top-level convenience methods like Alamofire.request use a default instance of Alamofire.SessionManager, which is configured with the default URLSessionConfiguration.

As such, the following two statements are equivalent:

Alamofire.request("https://httpbin.org/get")
let sessionManager = Alamofire.SessionManager.default
sessionManager.request("https://httpbin.org/get")

Applications can create session managers for background and ephemeral sessions, as well as new managers that customize the default session configuration, such as for default headers (httpAdditionalHeaders) or timeout interval (timeoutIntervalForRequest).

Creating a Session Manager with Default Configuration

let configuration = URLSessionConfiguration.default
let sessionManager = Alamofire.SessionManager(configuration: configuration)

Creating a Session Manager with Background Configuration

let configuration = URLSessionConfiguration.background(withIdentifier: "com.example.app.background")
let sessionManager = Alamofire.SessionManager(configuration: configuration)

Creating a Session Manager with Ephemeral Configuration

let configuration = URLSessionConfiguration.ephemeral
let sessionManager = Alamofire.SessionManager(configuration: configuration)

Modifying the Session Configuration

var defaultHeaders = Alamofire.SessionManager.defaultHTTPHeaders
defaultHeaders["DNT"] = "1 (Do Not Track Enabled)"

let configuration = URLSessionConfiguration.default
configuration.httpAdditionalHeaders = defaultHeaders

let sessionManager = Alamofire.SessionManager(configuration: configuration)

This is not recommended for Authorization or Content-Type headers. Instead, use the headers parameter in the top-level Alamofire.request APIs, URLRequestConvertible and ParameterEncoding, respectively.

Session Delegate

By default, an Alamofire SessionManager instance creates a SessionDelegate object to handle all the various types of delegate callbacks that are generated by the underlying URLSession. The implementations of each delegate method handle the most common use cases for these types of calls abstracting the complexity away from the top-level APIs. However, advanced users may find the need to override the default functionality for various reasons.

Override Closures

The first way to customize the SessionDelegate behavior is through the use of the override closures. Each closure gives you the ability to override the implementation of the matching SessionDelegate API, yet still use the default implementation for all other APIs. This makes it easy to customize subsets of the delegate functionality. Here are a few examples of some of the override closures available:

/// Overrides default behavior for URLSessionDelegate method `urlSession(_:didReceive:completionHandler:)`.
open var sessionDidReceiveChallenge: ((URLSession, URLAuthenticationChallenge) -> (URLSession.AuthChallengeDisposition, URLCredential?))?

/// Overrides default behavior for URLSessionDelegate method `urlSessionDidFinishEvents(forBackgroundURLSession:)`.
open var sessionDidFinishEventsForBackgroundURLSession: ((URLSession) -> Void)?

/// Overrides default behavior for URLSessionTaskDelegate method `urlSession(_:task:willPerformHTTPRedirection:newRequest:completionHandler:)`.
open var taskWillPerformHTTPRedirection: ((URLSession, URLSessionTask, HTTPURLResponse, URLRequest) -> URLRequest?)?

/// Overrides default behavior for URLSessionDataDelegate method `urlSession(_:dataTask:willCacheResponse:completionHandler:)`.
open var dataTaskWillCacheResponse: ((URLSession, URLSessionDataTask, CachedURLResponse) -> CachedURLResponse?)?

The following is a short example of how to use the taskWillPerformHTTPRedirection to avoid following redirects to any apple.com domains.

let sessionManager = Alamofire.SessionManager(configuration: URLSessionConfiguration.default)
let delegate: Alamofire.SessionDelegate = sessionManager.delegate

delegate.taskWillPerformHTTPRedirection = { session, task, response, request in
    var finalRequest = request

    if
        let originalRequest = task.originalRequest,
        let urlString = originalRequest.url?.urlString,
        urlString.contains("apple.com")
    {
        finalRequest = originalRequest
    }

    return finalRequest
}

Subclassing

Another way to override the default implementation of the SessionDelegate is to subclass it. Subclassing allows you completely customize the behavior of the API or to create a proxy for the API and still use the default implementation. Creating a proxy allows you to log events, emit notifications, provide pre and post hook implementations, etc. Here's a quick example of subclassing the SessionDelegate and logging a message when a redirect occurs.

class LoggingSessionDelegate: SessionDelegate {
    override func urlSession(
        _ session: URLSession,
        task: URLSessionTask,
        willPerformHTTPRedirection response: HTTPURLResponse,
        newRequest request: URLRequest,
        completionHandler: @escaping (URLRequest?) -> Void)
    {
        print("URLSession will perform HTTP redirection to request: \(request)")

        super.urlSession(
            session,
            task: task,
            willPerformHTTPRedirection: response,
            newRequest: request,
            completionHandler: completionHandler
        )
    }
}

Generally speaking, either the default implementation or the override closures should provide the necessary functionality required. Subclassing should only be used as a last resort.

It is important to keep in mind that the subdelegates are initialized and destroyed in the default implementation. Be careful when subclassing to not introduce memory leaks.

Request

The result of a request, download, upload or stream methods are a DataRequest, DownloadRequest, UploadRequest and StreamRequest which all inherit from Request. All Request instances are always created by an owning session manager, and never initialized directly.

Each subclass has specialized methods such as authenticate, validate, responseJSON and uploadProgress that each return the caller instance in order to facilitate method chaining.

Requests can be suspended, resumed and cancelled:

  • suspend(): Suspends the underlying task and dispatch queue.
  • resume(): Resumes the underlying task and dispatch queue. If the owning manager does not have startRequestsImmediately set to true, the request must call resume() in order to start.
  • cancel(): Cancels the underlying task, producing an error that is passed to any registered response handlers.

Routing Requests

As apps grow in size, it's important to adopt common patterns as you build out your network stack. An important part of that design is how to route your requests. The Alamofire URLConvertible and URLRequestConvertible protocols along with the Router design pattern are here to help.

URLConvertible

Types adopting the URLConvertible protocol can be used to construct URLs, which are then used to construct URL requests internally. String, URL, and URLComponents conform to URLConvertible by default, allowing any of them to be passed as url parameters to the request, upload, and download methods:

let urlString = "https://httpbin.org/post"
Alamofire.request(urlString, method: .post)

let url = URL(string: urlString)!
Alamofire.request(url, method: .post)

let urlComponents = URLComponents(url: url, resolvingAgainstBaseURL: true)!
Alamofire.request(urlComponents, method: .post)

Applications interacting with web applications in a significant manner are encouraged to have custom types conform to URLConvertible as a convenient way to map domain-specific models to server resources.

Type-Safe Routing
extension User: URLConvertible {
    static let baseURLString = "https://example.com"

    func asURL() throws -> URL {
        let urlString = User.baseURLString + "/users/\(username)/"
        return try urlString.asURL()
    }
}
let user = User(username: "mattt")
Alamofire.request(user) // https://example.com/users/mattt

URLRequestConvertible

Types adopting the URLRequestConvertible protocol can be used to construct URL requests. URLRequest conforms to URLRequestConvertible by default, allowing it to be passed into request, upload, and download methods directly (this is the recommended way to specify custom HTTP body for individual requests):

let url = URL(string: "https://httpbin.org/post")!
var urlRequest = URLRequest(url: url)
urlRequest.httpMethod = "POST"

let parameters = ["foo": "bar"]

do {
    urlRequest.httpBody = try JSONSerialization.data(withJSONObject: parameters, options: [])
} catch {
    // No-op
}

urlRequest.setValue("application/json", forHTTPHeaderField: "Content-Type")

Alamofire.request(urlRequest)

Applications interacting with web applications in a significant manner are encouraged to have custom types conform to URLRequestConvertible as a way to ensure consistency of requested endpoints. Such an approach can be used to abstract away server-side inconsistencies and provide type-safe routing, as well as manage authentication credentials and other state.

API Parameter Abstraction
enum Router: URLRequestConvertible {
    case search(query: String, page: Int)

    static let baseURLString = "https://example.com"
    static let perPage = 50

    // MARK: URLRequestConvertible

    func asURLRequest() throws -> URLRequest {
        let result: (path: String, parameters: Parameters) = {
            switch self {
            case let .search(query, page) where page > 0:
                return ("/search", ["q": query, "offset": Router.perPage * page])
            case let .search(query, _):
                return ("/search", ["q": query])
            }
        }()

        let url = try Router.baseURLString.asURL()
        let urlRequest = URLRequest(url: url.appendingPathComponent(result.path))

        return try URLEncoding.default.encode(urlRequest, with: result.parameters)
    }
}
Alamofire.request(Router.search(query: "foo bar", page: 1)) // https://example.com/search?q=foo%20bar&offset=50
CRUD & Authorization
import Alamofire

enum Router: URLRequestConvertible {
    case createUser(parameters: Parameters)
    case readUser(username: String)
    case updateUser(username: String, parameters: Parameters)
    case destroyUser(username: String)

    static let baseURLString = "https://example.com"

    var method: HTTPMethod {
        switch self {
        case .createUser:
            return .post
        case .readUser:
            return .get
        case .updateUser:
            return .put
        case .destroyUser:
            return .delete
        }
    }

    var path: String {
        switch self {
        case .createUser:
            return "/users"
        case .readUser(let username):
            return "/users/\(username)"
        case .updateUser(let username, _):
            return "/users/\(username)"
        case .destroyUser(let username):
            return "/users/\(username)"
        }
    }

    // MARK: URLRequestConvertible

    func asURLRequest() throws -> URLRequest {
        let url = try Router.baseURLString.asURL()

        var urlRequest = URLRequest(url: url.appendingPathComponent(path))
        urlRequest.httpMethod = method.rawValue

        switch self {
        case .createUser(let parameters):
            urlRequest = try URLEncoding.default.encode(urlRequest, with: parameters)
        case .updateUser(_, let parameters):
            urlRequest = try URLEncoding.default.encode(urlRequest, with: parameters)
        default:
            break
        }

        return urlRequest
    }
}
Alamofire.request(Router.readUser("mattt")) // GET https://example.com/users/mattt

Adapting and Retrying Requests

Most web services these days are behind some sort of authentication system. One of the more common ones today is OAuth. This generally involves generating an access token authorizing your application or user to call the various supported web services. While creating these initial access tokens can be laborsome, it can be even more complicated when your access token expires and you need to fetch a new one. There are many thread-safety issues that need to be considered.

The RequestAdapter and RequestRetrier protocols were created to make it much easier to create a thread-safe authentication system for a specific set of web services.

RequestAdapter

The RequestAdapter protocol allows each Request made on a SessionManager to be inspected and adapted before being created. One very specific way to use an adapter is to append an Authorization header to requests behind a certain type of authentication.

class AccessTokenAdapter: RequestAdapter {
    private let accessToken: String

    init(accessToken: String) {
        self.accessToken = accessToken
    }

    func adapt(_ urlRequest: URLRequest) throws -> URLRequest {
        var urlRequest = urlRequest

        if let urlString = urlRequest.url?.absoluteString, urlString.hasPrefix("https://httpbin.org") {
            urlRequest.setValue("Bearer " + accessToken, forHTTPHeaderField: "Authorization")
        }

        return urlRequest
    }
}
let sessionManager = SessionManager()
sessionManager.adapter = AccessTokenAdapter(accessToken: "1234")

sessionManager.request("https://httpbin.org/get")

RequestRetrier

The RequestRetrier protocol allows a Request that encountered an Error while being executed to be retried. When using both the RequestAdapter and RequestRetrier protocols together, you can create credential refresh systems for OAuth1, OAuth2, Basic Auth and even exponential backoff retry policies. The possibilities are endless. Here's an example of how you could implement a refresh flow for OAuth2 access tokens.

DISCLAIMER: This is NOT a global OAuth2 solution. It is merely an example demonstrating how one could use the RequestAdapter in conjunction with the RequestRetrier to create a thread-safe refresh system.

To reiterate, do NOT copy this sample code and drop it into a production application. This is merely an example. Each authentication system must be tailored to a particular platform and authentication type.

class OAuth2Handler: RequestAdapter, RequestRetrier {
    private typealias RefreshCompletion = (_ succeeded: Bool, _ accessToken: String?, _ refreshToken: String?) -> Void

    private let sessionManager: SessionManager = {
        let configuration = URLSessionConfiguration.default
        configuration.httpAdditionalHeaders = SessionManager.defaultHTTPHeaders

        return SessionManager(configuration: configuration)
    }()

    private let lock = NSLock()

    private var clientID: String
    private var baseURLString: String
    private var accessToken: String
    private var refreshToken: String

    private var isRefreshing = false
    private var requestsToRetry: [RequestRetryCompletion] = []

    // MARK: - Initialization

    public init(clientID: String, baseURLString: String, accessToken: String, refreshToken: String) {
        self.clientID = clientID
        self.baseURLString = baseURLString
        self.accessToken = accessToken
        self.refreshToken = refreshToken
    }

    // MARK: - RequestAdapter

    func adapt(_ urlRequest: URLRequest) throws -> URLRequest {
        if let urlString = urlRequest.url?.absoluteString, urlString.hasPrefix(baseURLString) {
            var urlRequest = urlRequest
            urlRequest.setValue("Bearer " + accessToken, forHTTPHeaderField: "Authorization")
            return urlRequest
        }

        return urlRequest
    }

    // MARK: - RequestRetrier

    func should(_ manager: SessionManager, retry request: Request, with error: Error, completion: @escaping RequestRetryCompletion) {
        lock.lock() ; defer { lock.unlock() }

        if let response = request.task?.response as? HTTPURLResponse, response.statusCode == 401 {
            requestsToRetry.append(completion)

            if !isRefreshing {
                refreshTokens { [weak self] succeeded, accessToken, refreshToken in
                    guard let strongSelf = self else { return }

                    strongSelf.lock.lock() ; defer { strongSelf.lock.unlock() }

                    if let accessToken = accessToken, let refreshToken = refreshToken {
                        strongSelf.accessToken = accessToken
                        strongSelf.refreshToken = refreshToken
                    }

                    strongSelf.requestsToRetry.forEach { $0(succeeded, 0.0) }
                    strongSelf.requestsToRetry.removeAll()
                }
            }
        } else {
            completion(false, 0.0)
        }
    }

    // MARK: - Private - Refresh Tokens

    private func refreshTokens(completion: @escaping RefreshCompletion) {
        guard !isRefreshing else { return }

        isRefreshing = true

        let urlString = "\(baseURLString)/oauth2/token"

        let parameters: [String: Any] = [
            "access_token": accessToken,
            "refresh_token": refreshToken,
            "client_id": clientID,
            "grant_type": "refresh_token"
        ]

        sessionManager.request(urlString, method: .post, parameters: parameters, encoding: JSONEncoding.default)
            .responseJSON { [weak self] response in
                guard let strongSelf = self else { return }

                if 
                    let json = response.result.value as? [String: Any], 
                    let accessToken = json["access_token"] as? String, 
                    let refreshToken = json["refresh_token"] as? String 
                {
                    completion(true, accessToken, refreshToken)
                } else {
                    completion(false, nil, nil)
                }

                strongSelf.isRefreshing = false
            }
    }
}
let baseURLString = "https://some.domain-behind-oauth2.com"

let oauthHandler = OAuth2Handler(
    clientID: "12345678",
    baseURLString: baseURLString,
    accessToken: "abcd1234",
    refreshToken: "ef56789a"
)

let sessionManager = SessionManager()
sessionManager.adapter = oauthHandler
sessionManager.retrier = oauthHandler

let urlString = "\(baseURLString)/some/endpoint"

sessionManager.request(urlString).validate().responseJSON { response in
    debugPrint(response)
}

Once the OAuth2Handler is applied as both the adapter and retrier for the SessionManager, it will handle an invalid access token error by automatically refreshing the access token and retrying all failed requests in the same order they failed.

If you needed them to execute in the same order they were created, you could sort them by their task identifiers.

The example above only checks for a 401 response code which is not nearly robust enough, but does demonstrate how one could check for an invalid access token error. In a production application, one would want to check the realm and most likely the www-authenticate header response although it depends on the OAuth2 implementation.

Another important note is that this authentication system could be shared between multiple session managers. For example, you may need to use both a default and ephemeral session configuration for the same set of web services. The example above allows the same oauthHandler instance to be shared across multiple session managers to manage the single refresh flow.

Custom Response Serialization

Alamofire provides built-in response serialization for data, strings, JSON, and property lists:

Alamofire.request(...).responseData { (resp: DataResponse<Data>) in ... }
Alamofire.request(...).responseString { (resp: DataResponse<String>) in ... }
Alamofire.request(...).responseJSON { (resp: DataResponse<Any>) in ... }
Alamofire.request(...).responsePropertyList { resp: DataResponse<Any>) in ... }

Those responses wrap deserialized values (Data, String, Any) or errors (network, validation errors), as well as meta-data (URL request, HTTP headers, status code, metrics, ...).

You have several ways to customize all of those response elements:

Response Mapping

Response mapping is the simplest way to produce customized responses. It transforms the value of a response, while preserving eventual errors and meta-data. For example, you can turn a json response DataResponse<Any> into a response that holds an application model, such as DataResponse<User>. You perform response mapping with the DataResponse.map method:

Alamofire.request("https://example.com/users/mattt").responseJSON { (response: DataResponse<Any>) in
    let userResponse = response.map { json in
        // We assume an existing User(json: Any) initializer
        return User(json: json)
    }

    // Process userResponse, of type DataResponse<User>:
    if let user = userResponse.value {
        print("User: { username: \(user.username), name: \(user.name) }")
    }
}

When the transformation may throw an error, use flatMap instead:

Alamofire.request("https://example.com/users/mattt").responseJSON { response in
    let userResponse = response.flatMap { json in
        try User(json: json)
    }
}

Response mapping is a good fit for your custom completion handlers:

@discardableResult
func loadUser(completionHandler: @escaping (DataResponse<User>) -> Void) -> Alamofire.DataRequest {
    return Alamofire.request("https://example.com/users/mattt").responseJSON { response in
        let userResponse = response.flatMap { json in
            try User(json: json)
        }

        completionHandler(userResponse)
    }
}

loadUser { response in
    if let user = userResponse.value {
        print("User: { username: \(user.username), name: \(user.name) }")
    }
}

When the map/flatMap closure may process a big amount of data, make sure you execute it outside of the main thread:

@discardableResult
func loadUser(completionHandler: @escaping (DataResponse<User>) -> Void) -> Alamofire.DataRequest {
    let utilityQueue = DispatchQueue.global(qos: .utility)

    return Alamofire.request("https://example.com/users/mattt").responseJSON(queue: utilityQueue) { response in
        let userResponse = response.flatMap { json in
            try User(json: json)
        }

        DispatchQueue.main.async {
            completionHandler(userResponse)
        }
    }
}

map and flatMap are also available for download responses.

Handling Errors

Before implementing custom response serializers or object serialization methods, it's important to consider how to handle any errors that may occur. There are two basic options: passing existing errors along unmodified, to be dealt with at response time; or, wrapping all errors in an Error type specific to your app.

For example, here's a simple BackendError enum which will be used in later examples:

enum BackendError: Error {
    case network(error: Error) // Capture any underlying Error from the URLSession API
    case dataSerialization(error: Error)
    case jsonSerialization(error: Error)
    case xmlSerialization(error: Error)
    case objectSerialization(reason: String)
}

Creating a Custom Response Serializer

Alamofire provides built-in response serialization for strings, JSON, and property lists, but others can be added in extensions on Alamofire.DataRequest and / or Alamofire.DownloadRequest.

For example, here's how a response handler using Ono might be implemented:

extension DataRequest {
    static func xmlResponseSerializer() -> DataResponseSerializer<ONOXMLDocument> {
        return DataResponseSerializer { request, response, data, error in
            // Pass through any underlying URLSession error to the .network case.
            guard error == nil else { return .failure(BackendError.network(error: error!)) }

            // Use Alamofire's existing data serializer to extract the data, passing the error as nil, as it has
            // already been handled.
            let result = Request.serializeResponseData(response: response, data: data, error: nil)

            guard case let .success(validData) = result else {
                return .failure(BackendError.dataSerialization(error: result.error! as! AFError))
            }

            do {
                let xml = try ONOXMLDocument(data: validData)
                return .success(xml)
            } catch {
                return .failure(BackendError.xmlSerialization(error: error))
            }
        }
    }

    @discardableResult
    func responseXMLDocument(
        queue: DispatchQueue? = nil,
        completionHandler: @escaping (DataResponse<ONOXMLDocument>) -> Void)
        -> Self
    {
        return response(
            queue: queue,
            responseSerializer: DataRequest.xmlResponseSerializer(),
            completionHandler: completionHandler
        )
    }
}

Generic Response Object Serialization

Generics can be used to provide automatic, type-safe response object serialization.

protocol ResponseObjectSerializable {
    init?(response: HTTPURLResponse, representation: Any)
}

extension DataRequest {
    func responseObject<T: ResponseObjectSerializable>(
        queue: DispatchQueue? = nil,
        completionHandler: @escaping (DataResponse<T>) -> Void)
        -> Self
    {
        let responseSerializer = DataResponseSerializer<T> { request, response, data, error in
            guard error == nil else { return .failure(BackendError.network(error: error!)) }

            let jsonResponseSerializer = DataRequest.jsonResponseSerializer(options: .allowFragments)
            let result = jsonResponseSerializer.serializeResponse(request, response, data, nil)

            guard case let .success(jsonObject) = result else {
                return .failure(BackendError.jsonSerialization(error: result.error!))
            }

            guard let response = response, let responseObject = T(response: response, representation: jsonObject) else {
                return .failure(BackendError.objectSerialization(reason: "JSON could not be serialized: \(jsonObject)"))
            }

            return .success(responseObject)
        }

        return response(queue: queue, responseSerializer: responseSerializer, completionHandler: completionHandler)
    }
}
struct User: ResponseObjectSerializable, CustomStringConvertible {
    let username: String
    let name: String

    var description: String {
        return "User: { username: \(username), name: \(name) }"
    }

    init?(response: HTTPURLResponse, representation: Any) {
        guard
            let username = response.url?.lastPathComponent,
            let representation = representation as? [String: Any],
            let name = representation["name"] as? String
        else { return nil }

        self.username = username
        self.name = name
    }
}
Alamofire.request("https://example.com/users/mattt").responseObject { (response: DataResponse<User>) in
    debugPrint(response)

    if let user = response.result.value {
        print("User: { username: \(user.username), name: \(user.name) }")
    }
}

The same approach can also be used to handle endpoints that return a representation of a collection of objects:

protocol ResponseCollectionSerializable {
    static func collection(from response: HTTPURLResponse, withRepresentation representation: Any) -> [Self]
}

extension ResponseCollectionSerializable where Self: ResponseObjectSerializable {
    static func collection(from response: HTTPURLResponse, withRepresentation representation: Any) -> [Self] {
        var collection: [Self] = []

        if let representation = representation as? [[String: Any]] {
            for itemRepresentation in representation {
                if let item = Self(response: response, representation: itemRepresentation) {
                    collection.append(item)
                }
            }
        }

        return collection
    }
}
extension DataRequest {
    @discardableResult
    func responseCollection<T: ResponseCollectionSerializable>(
        queue: DispatchQueue? = nil,
        completionHandler: @escaping (DataResponse<[T]>) -> Void) -> Self
    {
        let responseSerializer = DataResponseSerializer<[T]> { request, response, data, error in
            guard error == nil else { return .failure(BackendError.network(error: error!)) }

            let jsonSerializer = DataRequest.jsonResponseSerializer(options: .allowFragments)
            let result = jsonSerializer.serializeResponse(request, response, data, nil)

            guard case let .success(jsonObject) = result else {
                return .failure(BackendError.jsonSerialization(error: result.error!))
            }

            guard let response = response else {
                let reason = "Response collection could not be serialized due to nil response."
                return .failure(BackendError.objectSerialization(reason: reason))
            }

            return .success(T.collection(from: response, withRepresentation: jsonObject))
        }

        return response(responseSerializer: responseSerializer, completionHandler: completionHandler)
    }
}
struct User: ResponseObjectSerializable, ResponseCollectionSerializable, CustomStringConvertible {
    let username: String
    let name: String

    var description: String {
        return "User: { username: \(username), name: \(name) }"
    }

    init?(response: HTTPURLResponse, representation: Any) {
        guard
            let username = response.url?.lastPathComponent,
            let representation = representation as? [String: Any],
            let name = representation["name"] as? String
        else { return nil }

        self.username = username
        self.name = name
    }
}
Alamofire.request("https://example.com/users").responseCollection { (response: DataResponse<[User]>) in
    debugPrint(response)

    if let users = response.result.value {
        users.forEach { print("- \($0)") }
    }
}

Security

Using a secure HTTPS connection when communicating with servers and web services is an important step in securing sensitive data. By default, Alamofire will evaluate the certificate chain provided by the server using Apple's built in validation provided by the Security framework. While this guarantees the certificate chain is valid, it does not prevent man-in-the-middle (MITM) attacks or other potential vulnerabilities. In order to mitigate MITM attacks, applications dealing with sensitive customer data or financial information should use certificate or public key pinning provided by the ServerTrustPolicy.

ServerTrustPolicy

The ServerTrustPolicy enumeration evaluates the server trust generally provided by an URLAuthenticationChallenge when connecting to a server over a secure HTTPS connection.

let serverTrustPolicy = ServerTrustPolicy.pinCertificates(
    certificates: ServerTrustPolicy.certificates(),
    validateCertificateChain: true,
    validateHost: true
)

There are many different cases of server trust evaluation giving you complete control over the validation process:

  • performDefaultEvaluation: Uses the default server trust evaluation while allowing you to control whether to validate the host provided by the challenge.
  • pinCertificates: Uses the pinned certificates to validate the server trust. The server trust is considered valid if one of the pinned certificates match one of the server certificates.
  • pinPublicKeys: Uses the pinned public keys to validate the server trust. The server trust is considered valid if one of the pinned public keys match one of the server certificate public keys.
  • disableEvaluation: Disables all evaluation which in turn will always consider any server trust as valid.
  • customEvaluation: Uses the associated closure to evaluate the validity of the server trust thus giving you complete control over the validation process. Use with caution.

Server Trust Policy Manager

The ServerTrustPolicyManager is responsible for storing an internal mapping of server trust policies to a particular host. This allows Alamofire to evaluate each host against a different server trust policy.

let serverTrustPolicies: [String: ServerTrustPolicy] = [
    "test.example.com": .pinCertificates(
        certificates: ServerTrustPolicy.certificates(),
        validateCertificateChain: true,
        validateHost: true
    ),
    "insecure.expired-apis.com": .disableEvaluation
]

let sessionManager = SessionManager(
    serverTrustPolicyManager: ServerTrustPolicyManager(policies: serverTrustPolicies)
)

Make sure to keep a reference to the new SessionManager instance, otherwise your requests will all get cancelled when your sessionManager is deallocated.

These server trust policies will result in the following behavior:

  • test.example.com will always use certificate pinning with certificate chain and host validation enabled thus requiring the following criteria to be met to allow the TLS handshake to succeed:
    • Certificate chain MUST be valid.
    • Certificate chain MUST include one of the pinned certificates.
    • Challenge host MUST match the host in the certificate chain's leaf certificate.
  • insecure.expired-apis.com will never evaluate the certificate chain and will always allow the TLS handshake to succeed.
  • All other hosts will use the default evaluation provided by Apple.
Subclassing Server Trust Policy Manager

If you find yourself needing more flexible server trust policy matching behavior (i.e. wildcarded domains), then subclass the ServerTrustPolicyManager and override the serverTrustPolicyForHost method with your own custom implementation.

class CustomServerTrustPolicyManager: ServerTrustPolicyManager {
    override func serverTrustPolicy(forHost host: String) -> ServerTrustPolicy? {
        var policy: ServerTrustPolicy?

        // Implement your custom domain matching behavior...

        return policy
    }
}

Validating the Host

The .performDefaultEvaluation, .pinCertificates and .pinPublicKeys server trust policies all take a validateHost parameter. Setting the value to true will cause the server trust evaluation to verify that hostname in the certificate matches the hostname of the challenge. If they do not match, evaluation will fail. A validateHost value of false will still evaluate the full certificate chain, but will not validate the hostname of the leaf certificate.

It is recommended that validateHost always be set to true in production environments.

Validating the Certificate Chain

Pinning certificates and public keys both have the option of validating the certificate chain using the validateCertificateChain parameter. By setting this value to true, the full certificate chain will be evaluated in addition to performing a byte equality check against the pinned certificates or public keys. A value of false will skip the certificate chain validation, but will still perform the byte equality check.

There are several cases where it may make sense to disable certificate chain validation. The most common use cases for disabling validation are self-signed and expired certificates. The evaluation would always fail in both of these cases, but the byte equality check will still ensure you are receiving the certificate you expect from the server.

It is recommended that validateCertificateChain always be set to true in production environments.

App Transport Security

With the addition of App Transport Security (ATS) in iOS 9, it is possible that using a custom ServerTrustPolicyManager with several ServerTrustPolicy objects will have no effect. If you continuously see CFNetwork SSLHandshake failed (-9806) errors, you have probably run into this problem. Apple's ATS system overrides the entire challenge system unless you configure the ATS settings in your app's plist to disable enough of it to allow your app to evaluate the server trust.

If you run into this problem (high probability with self-signed certificates), you can work around this issue by adding the following to your Info.plist.

<dict>
    <key>NSAppTransportSecurity</key>
    <dict>
        <key>NSExceptionDomains</key>
        <dict>
            <key>example.com</key>
            <dict>
                <key>NSExceptionAllowsInsecureHTTPLoads</key>
                <true/>
                <key>NSExceptionRequiresForwardSecrecy</key>
                <false/>
                <key>NSIncludesSubdomains</key>
                <true/>
                <!-- Optional: Specify minimum TLS version -->
                <key>NSTemporaryExceptionMinimumTLSVersion</key>
                <string>TLSv1.2</string>
            </dict>
        </dict>
    </dict>
</dict>

Whether you need to set the NSExceptionRequiresForwardSecrecy to NO depends on whether your TLS connection is using an allowed cipher suite. In certain cases, it will need to be set to NO. The NSExceptionAllowsInsecureHTTPLoads MUST be set to YES in order to allow the SessionDelegate to receive challenge callbacks. Once the challenge callbacks are being called, the ServerTrustPolicyManager will take over the server trust evaluation. You may also need to specify the NSTemporaryExceptionMinimumTLSVersion if you're trying to connect to a host that only supports TLS versions less than 1.2.

It is recommended to always use valid certificates in production environments.

Network Reachability

The NetworkReachabilityManager listens for reachability changes of hosts and addresses for both WWAN and WiFi network interfaces.

let manager = NetworkReachabilityManager(host: "www.apple.com")

manager?.listener = { status in
    print("Network Status Changed: \(status)")
}

manager?.startListening()

Make sure to remember to retain the manager in the above example, or no status changes will be reported. Also, do not include the scheme in the host string or reachability won't function correctly.

There are some important things to remember when using network reachability to determine what to do next.

  • Do NOT use Reachability to determine if a network request should be sent.
    • You should ALWAYS send it.
  • When Reachability is restored, use the event to retry failed network requests.
    • Even though the network requests may still fail, this is a good moment to retry them.
  • The network reachability status can be useful for determining why a network request may have failed.
    • If a network request fails, it is more useful to tell the user that the network request failed due to being offline rather than a more technical error, such as "request timed out."

It is recommended to check out WWDC 2012 Session 706, "Networking Best Practices" for more info.


Open Radars

The following radars have some effect on the current implementation of Alamofire.

  • rdar://21349340 - Compiler throwing warning due to toll-free bridging issue in test case
  • rdar://26761490 - Swift string interpolation causing memory leak with common usage
  • rdar://26870455 - Background URL Session Configurations do not work in the simulator
  • rdar://26849668 - Some URLProtocol APIs do not properly handle URLRequest

FAQ

What's the origin of the name Alamofire?

Alamofire is named after the Alamo Fire flower, a hybrid variant of the Bluebonnet, the official state flower of Texas.

What logic belongs in a Router vs. a Request Adapter?

Simple, static data such as paths, parameters and common headers belong in the Router. Dynamic data such as an Authorization header whose value can changed based on an authentication system belongs in a RequestAdapter.

The reason the dynamic data MUST be placed into the RequestAdapter is to support retry operations. When a Request is retried, the original request is not rebuilt meaning the Router will not be called again. The RequestAdapter is called again allowing the dynamic data to be updated on the original request before retrying the Request.


Credits

Alamofire is owned and maintained by the Alamofire Software Foundation. You can follow them on Twitter at @AlamofireSF for project updates and releases.

Security Disclosure

If you believe you have identified a security vulnerability with Alamofire, you should report it as soon as possible via email to security@alamofire.org. Please do not post it to a public issue tracker.

Donations

The ASF is looking to raise money to officially register as a federal non-profit organization. Registering will allow us members to gain some legal protections and also allow us to put donations to use, tax free. Donating to the ASF will enable us to:

  • Pay our legal fees to register as a federal non-profit organization
  • Pay our yearly legal fees to keep the non-profit in good status
  • Pay for our mail servers to help us stay on top of all questions and security issues
  • Potentially fund test servers to make it easier for us to test the edge cases
  • Potentially fund developers to work on one of our projects full-time

The community adoption of the ASF libraries has been amazing. We are greatly humbled by your enthusiasm around the projects, and want to continue to do everything we can to move the needle forward. With your continued support, the ASF will be able to improve its reach and also provide better legal safety for the core members. If you use any of our libraries for work, see if your employers would be interested in donating. Our initial goal is to raise $1000 to get all our legal ducks in a row and kickstart this campaign. Any amount you can donate today to help us reach our goal would be greatly appreciated.

Click here to lend your support to: Alamofire Software Foundation and make a donation at pledgie.com !

License

Alamofire is released under the MIT license. See LICENSE for details.



建立状态 的CocoaPods兼容 迦太基兼容 平台 推 Gitterdata-canonical-src

Alamofire是一个用Swift编写的HTTP网络库。

功能

  • 可链接请求/响应方法
  • URL / JSON / plist参数编码
  • 上传File / Data / Stream / MultipartFormData < li>
  • 使用请求或恢复数据下载文件
  • 使用URLCredential验证
  • HTTP响应验证
  • 上传并下载进度关闭与进度
  • cURL命令输出
  • 动态修改和重试请求
  • TLS证书和公钥固定
  • 网络可达性
  • 综合单元和集成测试覆盖
  • 完整文档

组件库

为了保持Alamofire专注于核心网络实施, Alamofire软件基金会创建了另外的组件库来带来对Alamofire生态系统的附加功能。

  • AlamofireImage - 包含图像响应序列化程序, UIImage UIImageView 的图像库,代码>扩展,自定义图像过滤器,自动清除内存中缓存和基于优先级的图像下载系统。
  • AlamofireNetworkActivityIndi​​cator - 使用Alamofire控制iOS上的网络活动指标的可见性。它包含可配置的延迟定时器,以帮助缓解闪烁,并且可以支持不由Alamofire管理的 URLSession 实例。

要求

  • iOS 8.0+ / macOS 10.10+ / tvOS 9.0+ / watchOS 2.0 +
  • Xcode 8.1 +
  • Swift 3.0 +

迁移指南

通讯

  • 如果您需要需要帮助,请使用堆栈溢出。 (Tag'alamofire')
  • 如果您想提出一般问题,请使用堆栈溢出。< / li>
  • 如果您发现错误,请打开问题。
  • 如果您拥有功能请求,请打开问题。
  • 如果您想要贡献,请提交拉取请求。

安装

CocoaPods

CocoaPods 是Cocoa项目的依赖管理员。您可以使用以下命令安装它:

$ gem install cocoapods

CocoaPods 1.1.0+ is required to build Alamofire 4.0.0+.

使用CocoaPods将Alamofire集成到您的Xcode项目中,请在您的 Podfile 中指定:

source 'https://github.com/CocoaPods/Specs.git'
platform :ios, '10.0'
use_frameworks!

target '<Your Target Name>' do
    pod 'Alamofire', '~> 4.4'
end

然后,运行以下命令:

$ pod install

Carthage

Carthage 是一个分散的依赖管理器,用于构建您的依赖关系并为您提供二进制框架。

您可以使用以下命令,使用 Homebrew 安装Carthage:

$ brew update
$ brew install carthage

要使用Carthage将Alamofire集成到Xcode项目中,请在您的 Cartfile 中指定:

github "Alamofire/Alamofire" ~> 4.4

运行 carthage update 构建框架,并将构建的 Alamofire.framework 拖放到Xcode项目中。

Swift包管理器

Swift软件包管理器是一种用于自动分发Swift代码的工具,并集成到 swift < / code>编译器。它正处于早期发展阶段,但Alamofire确实支持其在支持的平台上的使用。

一旦你安装了Swift软件包,添加Alamofire作为一个依赖关系就像将它添加到你的 Package.swift dependencies 值一样简单。

dependencies: [
    .Package(url: "https://github.com/Alamofire/Alamofire.git", majorVersion: 4)
]

手动

如果您不想使用上述依赖管理器之一,可以手动将Alamofire集成到项目中。

Embedded Framework

  • 打开终端,将 cd 放入您的顶级项目目录,并运行以下命令if您的项目未初始化为git仓库:

     

$ git init


- Add Alamofire as a git [submodule](http://git-scm.com/docs/git-submodule) by running the following command:

  ```bash
$ git submodule add https://github.com/Alamofire/Alamofire.git
  • 打开新的 Alamofire 文件夹,并将 Alamofire.xcodeproj 拖到应用程序的Xcode项目的Project Navigator中。

    应该出现嵌套在应用程序的蓝色项目图标下面。无论是高于还是低于所有其他Xcode组都不重要。

  • 在Project Navigator中选择 Alamofire.xcodeproj ,并验证部署目标与应用程序目标的匹配。

  • 接下来,在Project Navigator(蓝色项目图标)中选择您的应用程序项目以导航到目标配置窗口,并在侧栏中的Targets标题下选择应用程序目标。

  • 在该窗口顶部的标签栏中,打开常规面板。

  • 单击嵌入式二进制文件部分下的 + 按钮。

  • 您将看到两个不同的 Alamofire.xcodeproj 文件夹,每个文件夹都有两个不同版本的 Alamofire.framework 嵌套在 Products 文件夹中。 / p>

    您选择哪个 Products 文件夹无关紧要,但无论选择顶部还是底部的 Alamofire.framework

  • 为iOS选择顶部的 Alamofire.framework ,而对于OS X,请选择底部。

    您可以通过检查项目的构建日志来验证您选择了哪一个。 Alamofire 的构建目标将列为 Alamofire iOS Alamofire macOS Alamofire tvOS Alamofire watchOS

  • 就是这样!

    将代码Alamofire.framework 作为目标依赖关系,链接框架和嵌入式框架自动添加到复制文件构建阶段,这是您需要在模拟器和设备上构建的。 >


用法

发出请求

import Alamofire

Alamofire.request("https://httpbin.org/get")

响应处理

处理Alamofire制作的请求响应涉及将响应处理程序链接到请求上。

Alamofire.request("https://httpbin.org/get").responseJSON { response in
    print(response.request)  // original URL request
    print(response.response) // HTTP URL response
    print(response.data)     // server data
    print(response.result)   // result of response serialization

    if let JSON = response.result.value {
        print("JSON: \(JSON)")
    }
}

在上面的示例中, responseJSON 处理程序在 Request 附加到 Request 完成后执行。而不是阻止执行来等待来自服务器的响应,而关闭形式的回调是被指定为处理响应一旦收到。请求的结果仅在响应关闭的范围内可用。任何依赖于从服务器接收的响应或数据执行的执行必须在响应闭包中完成。

Networking in Alamofire is done asynchronously. Asynchronous programming may be a source of frustration to programmers unfamiliar with the concept, but there are very good reasons for doing it this way.

Alamofire默认包含五个不同的响应处理程序,包括:

// Response Handler - Unserialized Response
func response(
    queue: DispatchQueue?,
    completionHandler: @escaping (DefaultDataResponse) -> Void)
    -> Self

// Response Data Handler - Serialized into Data
func responseData(
    queue: DispatchQueue?,
    completionHandler: @escaping (DataResponse<Data>) -> Void)
    -> Self

// Response String Handler - Serialized into String
func responseString(
    queue: DispatchQueue?,
    encoding: String.Encoding?,
    completionHandler: @escaping (DataResponse<String>) -> Void)
    -> Self

// Response JSON Handler - Serialized into Any
func responseJSON(
    queue: DispatchQueue?,
    completionHandler: @escaping (DataResponse<Any>) -> Void)
    -> Self

// Response PropertyList (plist) Handler - Serialized into Any
func responsePropertyList(
    queue: DispatchQueue?,
    completionHandler: @escaping (DataResponse<Any>) -> Void))
    -> Self

没有一个响应处理程序对从服务器返回的 HTTPURLResponse 进行任何验证。

For example, response status codes in the 400..<499 and 500..<599 ranges do NOT automatically trigger an Error. Alamofire uses Response Validation method chaining to achieve this.

Response Handler

响应处理程序不评估任何响应数据。它只是直接从URL会话委托转发所有信息。它是使用 cURL 来执行请求的Alamofire等价物。

Alamofire.request("https://httpbin.org/get").response { response in
    print("Request: \(response.request)")
    print("Response: \(response.response)")
    print("Error: \(response.error)")

    if let data = response.data, let utf8Text = String(data: data, encoding: .utf8) {
        print("Data: \(utf8Text)")
    }
}

We strongly encourage you to leverage the other response serializers taking advantage of Response and Result types.

Response Data Handler

responseData 处理程序使用 responseDataSerializer (将服务器数据序列化为某种其他类型的对象)来提取由...返回的 Data 服务器。如果没有发生错误,并返回 Data ,则响应 Result 将是 .success 将为类型为 Data

Alamofire.request("https://httpbin.org/get").responseData { response in
    debugPrint("All Response Info: \(response)")

    if let data = response.result.value, let utf8Text = String(data: data, encoding: .utf8) {
        print("Data: \(utf8Text)")
    }
}

Response String Handler

responseString 处理程序使用 responseStringSerializer 将服务器返回的 Data 转换为 String 指定编码。如果没有发生错误,服务器数据成功序列化为 String ,响应 Result 将是 .success 将是 String
Alamofire.request("https://httpbin.org/get").responseString { response in
    print("Success: \(response.result.isSuccess)")
    print("Response String: \(response.result.value)")
}

If no encoding is specified, Alamofire will use the text encoding specified in the HTTPURLResponse from the server. If the text encoding cannot be determined by the server response, it defaults to .isoLatin1.

Response JSON Handler

responseJSON 处理程序使用 responseJSONSerializer 将服务器返回的 Data 转换为 Any 类型,使用指定的 JSONSerialization.ReadingOptions 。如果没有发生错误并且服务器数据成功地序列化成JSON对象,则响应 Result 将是 .success 将为的类型为 Any

Alamofire.request("https://httpbin.org/get").responseJSON { response in
    debugPrint(response)

    if let json = response.result.value {
        print("JSON: \(json)")
    }
}

All JSON serialization is handled by the JSONSerialization API in the Foundation framework.

Chained Response Handlers

响应处理程序甚至可以链接:

Alamofire.request("https://httpbin.org/get")
    .responseString { response in
        print("Response String: \(response.result.value)")
    }
    .responseJSON { response in
        print("Response JSON: \(response.result.value)")
    }

It is important to note that using multiple response handlers on the same Request requires the server data to be serialized multiple times. Once for each response handler.

Response Handler Queue

默认情况下,响应处理程序在主调度队列上执行。但是,可以提供自定义分派队列。

let utilityQueue = DispatchQueue.global(qos: .utility)

Alamofire.request("https://httpbin.org/get").responseJSON(queue: utilityQueue) { response in
    print("Executing response handler on utility queue")
}

响应验证

默认情况下,Alamofire会对任何已完成的请求进行处理,无论响应的内容如何。如果响应具有不可接受的状态代码或MIME类型,则在响应处理程序导致生成错误之前调用 validate

Manual Validation

Alamofire.request("https://httpbin.org/get")
    .validate(statusCode: 200..<300)
    .validate(contentType: ["application/json"])
    .responseData { response in
        switch response.result {
        case .success:
            print("Validation Successful")
        case .failure(let error):
            print(error)
        }
    }

Automatic Validation

自动验证 200 ... 299 范围内的状态代码,并且响应的 Content-Type 标头符合 Accept 标题的请求,如果提供。

Alamofire.request("https://httpbin.org/get").validate().responseJSON { response in
    switch response.result {
    case .success:
        print("Validation Successful")
    case .failure(let error):
        print(error)
    }
}

回应缓存

响应缓存通过 URLCache 在系统框架级别处理。它提供了一个复合的内存和磁盘缓存,并允许您操纵内存和磁盘部分的大小。

By default, Alamofire leverages the shared URLCache. In order to customize it, see the Session Manager Configurations section.

HTTP方法

HTTPMethod 枚举列出了 RFC 7231§4.3中定义的HTTP方法

public enum HTTPMethod: String {
    case options = "OPTIONS"
    case get     = "GET"
    case head    = "HEAD"
    case post    = "POST"
    case put     = "PUT"
    case patch   = "PATCH"
    case delete  = "DELETE"
    case trace   = "TRACE"
    case connect = "CONNECT"
}

这些值可以作为方法参数传递给 Alamofire.request API:

Alamofire.request("https://httpbin.org/get") // method defaults to `.get`

Alamofire.request("https://httpbin.org/post", method: .post)
Alamofire.request("https://httpbin.org/put", method: .put)
Alamofire.request("https://httpbin.org/delete", method: .delete)

The Alamofire.request method parameter defaults to .get.

参数编码

Alamofire支持三种类型的参数编码,包括: URL JSON PropertyList 。它还可以支持符合 ParameterEncoding 协议的任何自定义编码。

URL Encoding

URLEncoding 类型创建一个url编码的查询字符串,设置为或附加到任何现有的URL查询字符串或设置为URL请求的HTTP正文。查询字符串是否设置或附加到任何现有的URL查询字符串或设置为HTTP主体取决于编码的 Destination Destination 枚举有三种情况:

    .methodDependent - 将编码的查询字符串结果应用于 GET HEAD DELETE 请求的现有查询字符串并设置为具有任何其他HTTP方法的请求的HTTP主体。
  • .queryString - 将编码的查询字符串结果设置或附加到现有查询字符串。
  • .httpBody - 将编码的查询字符串结果设置为URL请求的HTTP正文。

具有HTTP主体的编码请求的 Content-Type HTTP头字段设置为 application / x-www-form-urlencoded; charset = utf-8 。由于没有关于如何对集合类型进行编码的已发布规范,因此将 [] 附加到数组值的键( foo [] = 1&amp; foo [] = 2 ),并附加用于嵌套字典值( foo [bar] = baz )的方括号包围的键。

GET Request With URL-Encoded Parameters
let parameters: Parameters = ["foo": "bar"]

// All three of these calls are equivalent
Alamofire.request("https://httpbin.org/get", parameters: parameters) // encoding defaults to `URLEncoding.default`
Alamofire.request("https://httpbin.org/get", parameters: parameters, encoding: URLEncoding.default)
Alamofire.request("https://httpbin.org/get", parameters: parameters, encoding: URLEncoding(destination: .methodDependent))

// https://httpbin.org/get?foo=bar
POST Request With URL-Encoded Parameters
let parameters: Parameters = [
    "foo": "bar",
    "baz": ["a", 1],
    "qux": [
        "x": 1,
        "y": 2,
        "z": 3
    ]
]

// All three of these calls are equivalent
Alamofire.request("https://httpbin.org/post", method: .post, parameters: parameters)
Alamofire.request("https://httpbin.org/post", method: .post, parameters: parameters, encoding: URLEncoding.default)
Alamofire.request("https://httpbin.org/post", method: .post, parameters: parameters, encoding: URLEncoding.httpBody)

// HTTP body: foo=bar&baz[]=a&baz[]=1&qux[x]=1&qux[y]=2&qux[z]=3

JSON Encoding

JSONEncoding 类型创建参数对象的JSON表示形式,它被设置为请求的HTTP主体。编码请求的 Content-Type HTTP头字段设置为 application / json

POST Request with JSON-Encoded Parameters
let parameters: Parameters = [
    "foo": [1,2,3],
    "bar": [
        "baz": "qux"
    ]
]

// Both calls are equivalent
Alamofire.request("https://httpbin.org/post", method: .post, parameters: parameters, encoding: JSONEncoding.default)
Alamofire.request("https://httpbin.org/post", method: .post, parameters: parameters, encoding: JSONEncoding(options: []))

// HTTP body: {"foo": [1, 2, 3], "bar": {"baz": "qux"}}

Property List Encoding

PropertyListEncoding 使用 PropertyListSerialization 根据相关格式和写入选项值创建参数对象的plist表示,该值被设置为请求的正文。编码请求的 Content-Type HTTP头字段设置为 application / x-plist

Custom Encoding

如果提供的 ParameterEncoding 类型不符合您的需要,您可以创建自己的自定义编码。以下是一个简单的例子,说明如何构建一个自定义的 JSONStringArrayEncoding 类型,将JSON字符串数组编码到请求

struct JSONStringArrayEncoding: ParameterEncoding {
    private let array: [String]

    init(array: [String]) {
        self.array = array
    }

    func encode(_ urlRequest: URLRequestConvertible, with parameters: Parameters?) throws -> URLRequest {
        var urlRequest = urlRequest.urlRequest

        let data = try JSONSerialization.data(withJSONObject: array, options: [])

        if urlRequest.value(forHTTPHeaderField: "Content-Type") == nil {
            urlRequest.setValue("application/json", forHTTPHeaderField: "Content-Type")
        }

        urlRequest.httpBody = data

        return urlRequest
    }
}

Manual Parameter Encoding of a URLRequest

可以在进行网络请求之外使用 ParameterEncoding API。

let url = URL(string: "https://httpbin.org/get")!
var urlRequest = URLRequest(url: url)

let parameters: Parameters = ["foo": "bar"]
let encodedURLRequest = try URLEncoding.queryString.encode(urlRequest, with: parameters)

HTTP头

请求中添加一个自定义HTTP头,直接支持在全局 request 方法中。这样可以轻松地将HTTP头连接到可以不断变化的请求

let headers: HTTPHeaders = [
    "Authorization": "Basic QWxhZGRpbjpvcGVuIHNlc2FtZQ==",
    "Accept": "application/json"
]

Alamofire.request("https://httpbin.org/headers", headers: headers).responseJSON { response in
    debugPrint(response)
}

For HTTP headers that do not change, it is recommended to set them on the URLSessionConfiguration so they are automatically applied to any URLSessionTask created by the underlying URLSession. For more information, see the Session Manager Configurations section.

默认的Alamofire SessionManager 为每个 Request 提供一组默认的标头。这些包括:

  • 接受编码,默认为 gzip; q = 1.0,compress; q = 0.5 ,根据 RFC 7230§4.2.3
  • Accept-Language ,默认最多为系统上最佳的6种首选语言,格式如 en; q = 1.0 ,根据 RFC 7231§5.3.5
  • User-Agent ,其中包含有关当前应用程序的版本信息。例如: iOS示例/ 1.0(com.alamofire.iOS-example; build:1; iOS 10.0.0)Alamofire / 4.0.0 ,根据RFC 7231§5.5.3

如果您需要自定义这些头文件,应该创建一个自定义的 URLSessionConfiguration ,更新了 defaultHTTPHeaders 属性,并将配置应用于新的 SessionManager 实例。

认证

通过 URLCredential URLAuthenticationChallenge

支持的身份验证方案

HTTP Basic Authentication

Request 中的验证方法将在适当时自动向 URLAuthenticationChallenge 提供 URLCredential : p>

let user = "user"
let password = "password"

Alamofire.request("https://httpbin.org/basic-auth/\(user)/\(password)")
    .authenticate(user: user, password: password)
    .responseJSON { response in
        debugPrint(response)
    }

根据您的服务器实现,授权标题也可能适合:

let user = "user"
let password = "password"

var headers: HTTPHeaders = [:]

if let authorizationHeader = Request.authorizationHeader(user: user, password: password) {
    headers[authorizationHeader.key] = authorizationHeader.value
}

Alamofire.request("https://httpbin.org/basic-auth/user/password", headers: headers)
    .responseJSON { response in
        debugPrint(response)
    }

Authentication with URLCredential

let user = "user"
let password = "password"

let credential = URLCredential(user: user, password: password, persistence: .forSession)

Alamofire.request("https://httpbin.org/basic-auth/\(user)/\(password)")
    .authenticate(usingCredential: credential)
    .responseJSON { response in
        debugPrint(response)
    }

It is important to note that when using a URLCredential for authentication, the underlying URLSession will actually end up making two requests if a challenge is issued by the server. The first request will not include the credential which "may" trigger a challenge from the server. The challenge is then received by Alamofire, the credential is appended and the request is retried by the underlying URLSession.

将数据下载到文件

在Alamofire中从服务器获取数据的请求可以下载内存或磁盘上的数据。迄今为止,所有示例中使用的 Alamofire.request API总是将内存中的服务器数据下载。这对于较小的有效载荷是非常好的,因为它更有效率,但对于较大的有效载荷来说真的很糟糕,因为下载可能会运行您的整个应用程序的内存不足。因此,您还可以使用 Alamofire.download API将服务器数据下载到磁盘上的临时文件。

This will only work on macOS as is. Other platforms don't allow access to the filesystem outside of your app's sandbox. To download files on other platforms, see the Download File Destination section.

Alamofire.download("https://httpbin.org/image/png").responseData { response in
    if let data = response.result.value {
        let image = UIImage(data: data)
    }
}

The Alamofire.download APIs should also be used if you need to download data while your app is in the background. For more information, please see the Session Manager Configurations section.

Download File Destination

您还可以提供一个 DownloadFileDestination 闭包将文件从临时目录移动到最终目的地。在临时文件实际移动到 destinationURL 之前,将会执行闭包中指定的 DownloadOptions 。目前支持的两个 DownloadOptions 是:

  • .createIntermediateDirectories - 如果指定,创建目标网址的中间目录。
  • .removePreviousFile - 如果指定,从目标网址中删除以前的文件。
let destination: DownloadRequest.DownloadFileDestination = { _, _ in
    let documentsURL = FileManager.default.urls(for: .documentDirectory, in: .userDomainMask)[0]
    let fileURL = documentsURL.appendPathComponent("pig.png")

    return (fileURL, [.removePreviousFile, .createIntermediateDirectories])
}

Alamofire.download(urlString, to: destination).response { response in
    print(response)

    if response.error == nil, let imagePath = response.destinationURL?.path {
        let image = UIImage(contentsOfFile: imagePath)
    }
}

您也可以使用建议的下载目的地API。

let destination = DownloadRequest.suggestedDownloadDestination(directory: .documentDirectory)
Alamofire.download("https://httpbin.org/image/png", to: destination)

Download Progress

很多时候向用户报告下载进度是有帮助的。任何 DownloadRequest 可以使用 downloadProgress API来报告下载进度。

Alamofire.download("https://httpbin.org/image/png")
    .downloadProgress { progress in
        print("Download Progress: \(progress.fractionCompleted)")
    }
    .responseData { response in
        if let data = response.result.value {
            let image = UIImage(data: data)
        }
    }

downloadProgress API还采用 queue 参数,该参数定义应该调用下载进度关闭的 DispatchQueue

let utilityQueue = DispatchQueue.global(qos: .utility)

Alamofire.download("https://httpbin.org/image/png")
    .downloadProgress(queue: utilityQueue) { progress in
        print("Download Progress: \(progress.fractionCompleted)")
    }
    .responseData { response in
        if let data = response.result.value {
            let image = UIImage(data: data)
        }
    }

Resuming a Download

如果 DownloadRequest 被取消或中断,底层的URL会话可能会为活动的 DownloadRequest 生成恢复数据。如果发生这种情况,可以重新使用恢复数据重新启动它所在的 DownloadRequest 。可以通过下载响应访问简历数据,然后在尝试重新启动请求时重新使用。

IMPORTANT: On the latest release of all the Apple platforms (iOS 10, macOS 10.12, tvOS 10, watchOS 3), resumeData is broken on background URL session configurations. There's an underlying bug in the resumeData generation logic where the data is written incorrectly and will always fail to resume the download. For more information about the bug and possible workarounds, please see this Stack Overflow post.

class ImageRequestor {
    private var resumeData: Data?
    private var image: UIImage?

    func fetchImage(completion: (UIImage?) -> Void) {
        guard image == nil else { completion(image) ; return }

        let destination: DownloadRequest.DownloadFileDestination = { _, _ in
            let documentsURL = FileManager.default.urls(for: .documentDirectory, in: .userDomainMask)[0]
            let fileURL = documentsURL.appendPathComponent("pig.png")

            return (fileURL, [.removePreviousFile, .createIntermediateDirectories])
        }

        let request: DownloadRequest

        if let resumeData = resumeData {
            request = Alamofire.download(resumingWith: resumeData)
        } else {
            request = Alamofire.download("https://httpbin.org/image/png")
        }

        request.responseData { response in
            switch response.result {
            case .success(let data):
                self.image = UIImage(data: data)
            case .failure:
                self.resumeData = response.resumeData
            }
        }
    }
}

将数据上传到服务器

当使用JSON或URL编码参数向服务器发送相对较少量的数据时,通常情况下, Alamofire.request API就足够了。如果您需要从文件URL或 InputStream 发送大量数据,那么您需要使用 Alamofire.upload API。

The Alamofire.upload APIs should also be used if you need to upload data while your app is in the background. For more information, please see the Session Manager Configurations section.

Uploading Data

let imageData = UIPNGRepresentation(image)!

Alamofire.upload(imageData, to: "https://httpbin.org/post").responseJSON { response in
    debugPrint(response)
}

Uploading a File

let fileURL = Bundle.main.url(forResource: "video", withExtension: "mov")

Alamofire.upload(fileURL, to: "https://httpbin.org/post").responseJSON { response in
    debugPrint(response)
}

Uploading Multipart Form Data

Alamofire.upload(
    multipartFormData: { multipartFormData in
        multipartFormData.append(unicornImageURL, withName: "unicorn")
        multipartFormData.append(rainbowImageURL, withName: "rainbow")
    },
    to: "https://httpbin.org/post",
    encodingCompletion: { encodingResult in
        switch encodingResult {
        case .success(let upload, _, _):
            upload.responseJSON { response in
                debugPrint(response)
            }
        case .failure(let encodingError):
            print(encodingError)
        }
    }
)

Upload Progress

当您的用户正在等待上传完成时,有时可以方便地向用户显示上传的进度。任何 UploadRequest 可以使用 uploadProgress downloadProgress API报告上传进度和下载响应数据的进度。

let fileURL = Bundle.main.url(forResource: "video", withExtension: "mov")

Alamofire.upload(fileURL, to: "https://httpbin.org/post")
    .uploadProgress { progress in // main queue by default
        print("Upload Progress: \(progress.fractionCompleted)")
    }
    .downloadProgress { progress in // main queue by default
        print("Download Progress: \(progress.fractionCompleted)")
    }
    .responseJSON { response in
        debugPrint(response)
    }

统计指标

Timeline

Alamofire在 Request 的整个生命周期中收集时间,并创建一个作为所有响应类型的属性公开的 Timeline 对象。

Alamofire.request("https://httpbin.org/get").responseJSON { response in
    print(response.timeline)
}

以上报告了以下 Timeline info:

  • Latency :0.428秒
  • 请求持续时间:0.428秒
  • 序列化持续时间:0.001秒
  • 总持续时间:0.429秒

URL Session Task Metrics

在iOS和tvOS 10和macOS 10.12中,Apple推出了新的 URLSessionTaskMetrics API。任务指标封装了关于请求和响应执行的一些奇妙的统计信息。 API非常类似于时间线,但提供了更多的Alamofire无法访问的统计信息。可以通过任何响应类型访问度量。

Alamofire.request("https://httpbin.org/get").responseJSON { response in
    print(response.metrics)
}

请注意,这些API仅适用于iOS和tvOS 10以及macOS 10.12。因此,根据您的部署目标,您可能需要使用这些内部可用性检查:

Alamofire.request("https://httpbin.org/get").responseJSON { response in
    if #available(iOS 10.0. *) {
        print(response.metrics)
    }
}

cURL命令输出

调试平台问题可能令人沮丧。幸运的是,Alamofire Request 对象符合 CustomStringConvertible CustomDebugStringConvertible 协议,以提供一些非常有用的调试工具。

CustomStringConvertible

let request = Alamofire.request("https://httpbin.org/ip")

print(request)
// GET https://httpbin.org/ip (200)

CustomDebugStringConvertible

let request = Alamofire.request("https://httpbin.org/get", parameters: ["foo": "bar"])
debugPrint(request)

输出:

$ curl -i \
    -H "User-Agent: Alamofire/4.0.0" \
    -H "Accept-Encoding: gzip;q=1.0, compress;q=0.5" \
    -H "Accept-Language: en;q=1.0,fr;q=0.9,de;q=0.8,zh-Hans;q=0.7,zh-Hant;q=0.6,ja;q=0.5" \
    "https://httpbin.org/get?foo=bar"

高级用法

Alamofire建立在 URLSession 和基础URL加载系统之上。为了充分利用此框架,建议您熟悉底层网络堆栈的概念和功能。

推荐阅读

会话管理器

顶级方便方法,如 Alamofire.request 使用默认的 Alamofire.SessionManager 实例,它配置了默认的 URLSessionConfiguration

因此,以下两个语句是等价的:

Alamofire.request("https://httpbin.org/get")
let sessionManager = Alamofire.SessionManager.default
sessionManager.request("https://httpbin.org/get")

应用程序可以为后台和临时会话创建会话管理器,以及定制默认会话配置的新管理器,如默认头文件( httpAdditionalHeaders )或超时间隔( timeoutIntervalForRequest < / code>)。

Creating a Session Manager with Default Configuration

let configuration = URLSessionConfiguration.default
let sessionManager = Alamofire.SessionManager(configuration: configuration)

Creating a Session Manager with Background Configuration

let configuration = URLSessionConfiguration.background(withIdentifier: "com.example.app.background")
let sessionManager = Alamofire.SessionManager(configuration: configuration)

Creating a Session Manager with Ephemeral Configuration

let configuration = URLSessionConfiguration.ephemeral
let sessionManager = Alamofire.SessionManager(configuration: configuration)

Modifying the Session Configuration

var defaultHeaders = Alamofire.SessionManager.defaultHTTPHeaders
defaultHeaders["DNT"] = "1 (Do Not Track Enabled)"

let configuration = URLSessionConfiguration.default
configuration.httpAdditionalHeaders = defaultHeaders

let sessionManager = Alamofire.SessionManager(configuration: configuration)

This is not recommended for Authorization or Content-Type headers. Instead, use the headers parameter in the top-level Alamofire.request APIs, URLRequestConvertible and ParameterEncoding, respectively.

会话代表

默认情况下,Alamofire SessionManager 实例创建一个 SessionDelegate 对象来处理由底层的 URLSession 。每个代理方法的实现处理这些类型调用的最常见用例,从而将复杂性从顶级API中抽象出来。但是,由于各种原因,高级用户可能会发现需要覆盖默认功能。

Override Closures

定制 SessionDelegate 行为的第一种方法是使用override closure。每个闭包使您能够覆盖匹配的 SessionDelegate API的实现,但仍然使用所有其他API的默认实现。这样可以轻松地定制委托功能的子集。以下是一些可用覆盖闭包的示例:

/// Overrides default behavior for URLSessionDelegate method `urlSession(_:didReceive:completionHandler:)`.
open var sessionDidReceiveChallenge: ((URLSession, URLAuthenticationChallenge) -> (URLSession.AuthChallengeDisposition, URLCredential?))?

/// Overrides default behavior for URLSessionDelegate method `urlSessionDidFinishEvents(forBackgroundURLSession:)`.
open var sessionDidFinishEventsForBackgroundURLSession: ((URLSession) -> Void)?

/// Overrides default behavior for URLSessionTaskDelegate method `urlSession(_:task:willPerformHTTPRedirection:newRequest:completionHandler:)`.
open var taskWillPerformHTTPRedirection: ((URLSession, URLSessionTask, HTTPURLResponse, URLRequest) -> URLRequest?)?

/// Overrides default behavior for URLSessionDataDelegate method `urlSession(_:dataTask:willCacheResponse:completionHandler:)`.
open var dataTaskWillCacheResponse: ((URLSession, URLSessionDataTask, CachedURLResponse) -> CachedURLResponse?)?

以下是使用 taskWillPerformHTTPRedirection 的简短示例,以避免以下重定向到任何 apple.com 域。

let sessionManager = Alamofire.SessionManager(configuration: URLSessionConfiguration.default)
let delegate: Alamofire.SessionDelegate = sessionManager.delegate

delegate.taskWillPerformHTTPRedirection = { session, task, response, request in
    var finalRequest = request

    if
        let originalRequest = task.originalRequest,
        let urlString = originalRequest.url?.urlString,
        urlString.contains("apple.com")
    {
        finalRequest = originalRequest
    }

    return finalRequest
}

Subclassing

另一种覆盖 SessionDelegate 的默认实现的方法是将其子类化。子类可让您完全自定义API的行为,或为API创建代理,并仍然使用默认实现。创建一个代理可以让你记录事件,发出通知,提供前后挂钩的实现等等。下面是一个简单的例子,它将 SessionDelegate 进行子类化,并在发生重定向时记录消息。

class LoggingSessionDelegate: SessionDelegate {
    override func urlSession(
        _ session: URLSession,
        task: URLSessionTask,
        willPerformHTTPRedirection response: HTTPURLResponse,
        newRequest request: URLRequest,
        completionHandler: @escaping (URLRequest?) -> Void)
    {
        print("URLSession will perform HTTP redirection to request: \(request)")

        super.urlSession(
            session,
            task: task,
            willPerformHTTPRedirection: response,
            newRequest: request,
            completionHandler: completionHandler
        )
    }
}
一般来说,默认实现或覆盖关闭应提供所需的必要功能。子类化只能用作最后的手段。

It is important to keep in mind that the subdelegates are initialized and destroyed in the default implementation. Be careful when subclassing to not introduce memory leaks.

请求

请求 download upload stream 方法的结果是一个 DataRequest < / code>, DownloadRequest UploadRequest StreamRequest ,它们都继承自 Request 。所有请求实例始终由拥有的会话管理器创建,并且不会直接初始化。

每个子类都有专门的方法,例如: authenticate validate responseJSON uploadProgress 实例,以方便链接。

请求可以暂停,恢复和取消:

  • suspend():挂起底层任务和调度队列。
  • resume():恢复底层任务和调度队列。如果拥有的经理没有将 startRequestsImmediately 设置为 true ,则请求必须调用 resume()才能启动。
  • cancel():取消基础任务,产生传递给任何注册的响应处理程序的错误。

路由请求

随着应用程序的规模越来越大,建立网络堆栈时,采用通用模式很重要。该设计的重要部分是如何路由您的请求。 Alamofire URLConvertible URLRequestConvertible 协议以及路由器设计模式在这里有所帮助。

URLConvertible

采用 URLConvertible 协议的类型可以用于构造URL,然后用于内部构造URL请求。默认情况下, String URL URLComponents 符合 URLConvertible ,允许其中的任何一个作为参数请求 upload 下载方法:

let urlString = "https://httpbin.org/post"
Alamofire.request(urlString, method: .post)

let url = URL(string: urlString)!
Alamofire.request(url, method: .post)

let urlComponents = URLComponents(url: url, resolvingAgainstBaseURL: true)!
Alamofire.request(urlComponents, method: .post)
鼓励以重要的方式与Web应用程序进行交互的应用程序使定制类型符合 URLConvertible ,作为将特定于域的模型映射到服务器资源的方便方法。

Type-Safe Routing
extension User: URLConvertible {
    static let baseURLString = "https://example.com"

    func asURL() throws -> URL {
        let urlString = User.baseURLString + "/users/\(username)/"
        return try urlString.asURL()
    }
}
let user = User(username: "mattt")
Alamofire.request(user) // https://example.com/users/mattt

URLRequestConvertible

采用 URLRequestConvertible 协议的类型可以用于构造URL请求。默认情况下, URLRequest 符合 URLRequestConvertible ,允许将其传递到请求 upload 下载方法直接(这是为各个请求指定自定义HTTP主体的推荐方式):

let url = URL(string: "https://httpbin.org/post")!
var urlRequest = URLRequest(url: url)
urlRequest.httpMethod = "POST"

let parameters = ["foo": "bar"]

do {
    urlRequest.httpBody = try JSONSerialization.data(withJSONObject: parameters, options: [])
} catch {
    // No-op
}

urlRequest.setValue("application/json", forHTTPHeaderField: "Content-Type")

Alamofire.request(urlRequest)
鼓励以重要方式与Web应用程序交互的应用程序使定制类型符合 URLRequestConvertible ,以确保请求的端点的一致性。这种方法可以用于抽象出服务器端的不一致,并提供类型安全的路由,以及管理身份验证凭据和其他状态。

API Parameter Abstraction
enum Router: URLRequestConvertible {
    case search(query: String, page: Int)

    static let baseURLString = "https://example.com"
    static let perPage = 50

    // MARK: URLRequestConvertible

    func asURLRequest() throws -> URLRequest {
        let result: (path: String, parameters: Parameters) = {
            switch self {
            case let .search(query, page) where page > 0:
                return ("/search", ["q": query, "offset": Router.perPage * page])
            case let .search(query, _):
                return ("/search", ["q": query])
            }
        }()

        let url = try Router.baseURLString.asURL()
        let urlRequest = URLRequest(url: url.appendingPathComponent(result.path))

        return try URLEncoding.default.encode(urlRequest, with: result.parameters)
    }
}
Alamofire.request(Router.search(query: "foo bar", page: 1)) // https://example.com/search?q=foo%20bar&offset=50
CRUD & Authorization
import Alamofire

enum Router: URLRequestConvertible {
    case createUser(parameters: Parameters)
    case readUser(username: String)
    case updateUser(username: String, parameters: Parameters)
    case destroyUser(username: String)

    static let baseURLString = "https://example.com"

    var method: HTTPMethod {
        switch self {
        case .createUser:
            return .post
        case .readUser:
            return .get
        case .updateUser:
            return .put
        case .destroyUser:
            return .delete
        }
    }

    var path: String {
        switch self {
        case .createUser:
            return "/users"
        case .readUser(let username):
            return "/users/\(username)"
        case .updateUser(let username, _):
            return "/users/\(username)"
        case .destroyUser(let username):
            return "/users/\(username)"
        }
    }

    // MARK: URLRequestConvertible

    func asURLRequest() throws -> URLRequest {
        let url = try Router.baseURLString.asURL()

        var urlRequest = URLRequest(url: url.appendingPathComponent(path))
        urlRequest.httpMethod = method.rawValue

        switch self {
        case .createUser(let parameters):
            urlRequest = try URLEncoding.default.encode(urlRequest, with: parameters)
        case .updateUser(_, let parameters):
            urlRequest = try URLEncoding.default.encode(urlRequest, with: parameters)
        default:
            break
        }

        return urlRequest
    }
}
Alamofire.request(Router.readUser("mattt")) // GET https://example.com/users/mattt

适应和重试请求

大多数web服务现在都落后于某种认证系统。今天更常见的一个是OAuth。这通常涉及生成访问令牌,授权您的应用程序或用户调用各种支持的Web服务。在创建这些初始访问令牌时可能会变得非常糟糕,当您的访问令牌过期并且需要获取新访问令牌时,可能会更复杂。有许多线程安全问题需要考虑。

RequestAdapter RequestRetrier 协议被创建,以便为特定的一组Web服务创建一个线程安全的身份验证系统更容易。

RequestAdapter

RequestAdapter 协议允许在创建之前对 SessionManager 中的每个 Request 进行检查和修改。使用适配器的一个非常具体的方法是将授权头附加到某种类型的身份验证后面的请求。

class AccessTokenAdapter: RequestAdapter {
    private let accessToken: String

    init(accessToken: String) {
        self.accessToken = accessToken
    }

    func adapt(_ urlRequest: URLRequest) throws -> URLRequest {
        var urlRequest = urlRequest

        if let urlString = urlRequest.url?.absoluteString, urlString.hasPrefix("https://httpbin.org") {
            urlRequest.setValue("Bearer " + accessToken, forHTTPHeaderField: "Authorization")
        }

        return urlRequest
    }
}
let sessionManager = SessionManager()
sessionManager.adapter = AccessTokenAdapter(accessToken: "1234")

sessionManager.request("https://httpbin.org/get")

RequestRetrier

RequestRetrier 协议允许在执行时遇到 Error 请求。当同时使用 RequestAdapter RequestRetrier 协议时,您可以为OAuth1,OAuth2,Basic Auth甚至指数退避重试策略创建凭据刷新系统。可能性是无止境的。以下是您如何为OAuth2访问令牌实现刷新流程的示例。

DISCLAIMER: This is NOT a global OAuth2 solution. It is merely an example demonstrating how one could use the RequestAdapter in conjunction with the RequestRetrier to create a thread-safe refresh system.

To reiterate, do NOT copy this sample code and drop it into a production application. This is merely an example. Each authentication system must be tailored to a particular platform and authentication type.

class OAuth2Handler: RequestAdapter, RequestRetrier {
    private typealias RefreshCompletion = (_ succeeded: Bool, _ accessToken: String?, _ refreshToken: String?) -> Void

    private let sessionManager: SessionManager = {
        let configuration = URLSessionConfiguration.default
        configuration.httpAdditionalHeaders = SessionManager.defaultHTTPHeaders

        return SessionManager(configuration: configuration)
    }()

    private let lock = NSLock()

    private var clientID: String
    private var baseURLString: String
    private var accessToken: String
    private var refreshToken: String

    private var isRefreshing = false
    private var requestsToRetry: [RequestRetryCompletion] = []

    // MARK: - Initialization

    public init(clientID: String, baseURLString: String, accessToken: String, refreshToken: String) {
        self.clientID = clientID
        self.baseURLString = baseURLString
        self.accessToken = accessToken
        self.refreshToken = refreshToken
    }

    // MARK: - RequestAdapter

    func adapt(_ urlRequest: URLRequest) throws -> URLRequest {
        if let urlString = urlRequest.url?.absoluteString, urlString.hasPrefix(baseURLString) {
            var urlRequest = urlRequest
            urlRequest.setValue("Bearer " + accessToken, forHTTPHeaderField: "Authorization")
            return urlRequest
        }

        return urlRequest
    }

    // MARK: - RequestRetrier

    func should(_ manager: SessionManager, retry request: Request, with error: Error, completion: @escaping RequestRetryCompletion) {
        lock.lock() ; defer { lock.unlock() }

        if let response = request.task?.response as? HTTPURLResponse, response.statusCode == 401 {
            requestsToRetry.append(completion)

            if !isRefreshing {
                refreshTokens { [weak self] succeeded, accessToken, refreshToken in
                    guard let strongSelf = self else { return }

                    strongSelf.lock.lock() ; defer { strongSelf.lock.unlock() }

                    if let accessToken = accessToken, let refreshToken = refreshToken {
                        strongSelf.accessToken = accessToken
                        strongSelf.refreshToken = refreshToken
                    }

                    strongSelf.requestsToRetry.forEach { $0(succeeded, 0.0) }
                    strongSelf.requestsToRetry.removeAll()
                }
            }
        } else {
            completion(false, 0.0)
        }
    }

    // MARK: - Private - Refresh Tokens

    private func refreshTokens(completion: @escaping RefreshCompletion) {
        guard !isRefreshing else { return }

        isRefreshing = true

        let urlString = "\(baseURLString)/oauth2/token"

        let parameters: [String: Any] = [
            "access_token": accessToken,
            "refresh_token": refreshToken,
            "client_id": clientID,
            "grant_type": "refresh_token"
        ]

        sessionManager.request(urlString, method: .post, parameters: parameters, encoding: JSONEncoding.default)
            .responseJSON { [weak self] response in
                guard let strongSelf = self else { return }

                if 
                    let json = response.result.value as? [String: Any], 
                    let accessToken = json["access_token"] as? String, 
                    let refreshToken = json["refresh_token"] as? String 
                {
                    completion(true, accessToken, refreshToken)
                } else {
                    completion(false, nil, nil)
                }

                strongSelf.isRefreshing = false
            }
    }
}
let baseURLString = "https://some.domain-behind-oauth2.com"

let oauthHandler = OAuth2Handler(
    clientID: "12345678",
    baseURLString: baseURLString,
    accessToken: "abcd1234",
    refreshToken: "ef56789a"
)

let sessionManager = SessionManager()
sessionManager.adapter = oauthHandler
sessionManager.retrier = oauthHandler

let urlString = "\(baseURLString)/some/endpoint"

sessionManager.request(urlString).validate().responseJSON { response in
    debugPrint(response)
}

一旦将 OAuth2Handler 应用于 SessionManager 适配器 retrier ,则它将处理通过自动刷新访问令牌并以与失败相同的顺序重试所有失败的请求,无效访问令牌错误。

If you needed them to execute in the same order they were created, you could sort them by their task identifiers.

上面的例子只是检查一个不太健壮的 401 响应代码,但是它能说明如何检查一个无效的访问令牌错误。在生产应用程序中,我们希望检查 realm ,并且很可能是 www-authenticate 标头响应,尽管它取决于OAuth2实现。

另一个重要的注意事项是,该认证系统可以在多个会话管理器之间共享。例如,您可能需要为同一组Web服务使用默认短暂会话配置。上述示例允许在多个会话管理器之间共享相同的 oauthHandler 实例来管理单个刷新流。

自定义响应序列化

Alamofire为数据,字符串,JSON和属性列表提供内置响应序列化:

Alamofire.request(...).responseData { (resp: DataResponse<Data>) in ... }
Alamofire.request(...).responseString { (resp: DataResponse<String>) in ... }
Alamofire.request(...).responseJSON { (resp: DataResponse<Any>) in ... }
Alamofire.request(...).responsePropertyList { resp: DataResponse<Any>) in ... }
(数据,字符串,任何)或错误(网络,验证错误)以及元数据(URL请求,HTTP标头,状态代码,指标,...)。

您有几种方法可以自定义所有这些响应元素:

Response Mapping

响应映射是生成自定义响应的最简单的方法。它转换响应的值,同时保留最终的错误和元数据。例如,您可以将json响应 DataResponse&lt; Any&gt; 转换为保存应用程序模型的响应,例如 DataResponse&lt; User&gt; 。您可以使用 DataResponse.map 方法执行响应映射:

Alamofire.request("https://example.com/users/mattt").responseJSON { (response: DataResponse<Any>) in
    let userResponse = response.map { json in
        // We assume an existing User(json: Any) initializer
        return User(json: json)
    }

    // Process userResponse, of type DataResponse<User>:
    if let user = userResponse.value {
        print("User: { username: \(user.username), name: \(user.name) }")
    }
}

当转换可能会发生错误时,请改用 flatMap

Alamofire.request("https://example.com/users/mattt").responseJSON { response in
    let userResponse = response.flatMap { json in
        try User(json: json)
    }
}

响应映射非常适合您的自定义完成处理程序:

@discardableResult
func loadUser(completionHandler: @escaping (DataResponse<User>) -> Void) -> Alamofire.DataRequest {
    return Alamofire.request("https://example.com/users/mattt").responseJSON { response in
        let userResponse = response.flatMap { json in
            try User(json: json)
        }

        completionHandler(userResponse)
    }
}

loadUser { response in
    if let user = userResponse.value {
        print("User: { username: \(user.username), name: \(user.name) }")
    }
}

当map / flatMap关闭可能会处理大量数据时,请确保在主线程之外执行:

@discardableResult
func loadUser(completionHandler: @escaping (DataResponse<User>) -> Void) -> Alamofire.DataRequest {
    let utilityQueue = DispatchQueue.global(qos: .utility)

    return Alamofire.request("https://example.com/users/mattt").responseJSON(queue: utilityQueue) { response in
        let userResponse = response.flatMap { json in
            try User(json: json)
        }

        DispatchQueue.main.async {
            completionHandler(userResponse)
        }
    }
}

map flatMap 也可用于下载回复

Handling Errors

在实现自定义响应序列化程序或对象序列化方法之前,请考虑如何处理可能发生的任何错误。有两个基本选择:将未经修改的现有错误传递到响应时间处理;或者将您的应用程序特定的错误类型中的所有错误包装起来。

例如,这里是一个简单的 BackendError 枚举,将在稍后的例子中使用:

enum BackendError: Error {
    case network(error: Error) // Capture any underlying Error from the URLSession API
    case dataSerialization(error: Error)
    case jsonSerialization(error: Error)
    case xmlSerialization(error: Error)
    case objectSerialization(reason: String)
}

Creating a Custom Response Serializer

Alamofire为字符串,JSON和属性列表提供内置响应序列化,但其他可以在 Alamofire.DataRequest 和/或 Alamofire.DownloadRequest 中的扩展中添加。 。

例如,以下是使用 Ono 的响应处理程序如何实现:

extension DataRequest {
    static func xmlResponseSerializer() -> DataResponseSerializer<ONOXMLDocument> {
        return DataResponseSerializer { request, response, data, error in
            // Pass through any underlying URLSession error to the .network case.
            guard error == nil else { return .failure(BackendError.network(error: error!)) }

            // Use Alamofire's existing data serializer to extract the data, passing the error as nil, as it has
            // already been handled.
            let result = Request.serializeResponseData(response: response, data: data, error: nil)

            guard case let .success(validData) = result else {
                return .failure(BackendError.dataSerialization(error: result.error! as! AFError))
            }

            do {
                let xml = try ONOXMLDocument(data: validData)
                return .success(xml)
            } catch {
                return .failure(BackendError.xmlSerialization(error: error))
            }
        }
    }

    @discardableResult
    func responseXMLDocument(
        queue: DispatchQueue? = nil,
        completionHandler: @escaping (DataResponse<ONOXMLDocument>) -> Void)
        -> Self
    {
        return response(
            queue: queue,
            responseSerializer: DataRequest.xmlResponseSerializer(),
            completionHandler: completionHandler
        )
    }
}

Generic Response Object Serialization

泛型可用于提供自动,类型安全的响应对象序列化。

protocol ResponseObjectSerializable {
    init?(response: HTTPURLResponse, representation: Any)
}

extension DataRequest {
    func responseObject<T: ResponseObjectSerializable>(
        queue: DispatchQueue? = nil,
        completionHandler: @escaping (DataResponse<T>) -> Void)
        -> Self
    {
        let responseSerializer = DataResponseSerializer<T> { request, response, data, error in
            guard error == nil else { return .failure(BackendError.network(error: error!)) }

            let jsonResponseSerializer = DataRequest.jsonResponseSerializer(options: .allowFragments)
            let result = jsonResponseSerializer.serializeResponse(request, response, data, nil)

            guard case let .success(jsonObject) = result else {
                return .failure(BackendError.jsonSerialization(error: result.error!))
            }

            guard let response = response, let responseObject = T(response: response, representation: jsonObject) else {
                return .failure(BackendError.objectSerialization(reason: "JSON could not be serialized: \(jsonObject)"))
            }

            return .success(responseObject)
        }

        return response(queue: queue, responseSerializer: responseSerializer, completionHandler: completionHandler)
    }
}
struct User: ResponseObjectSerializable, CustomStringConvertible {
    let username: String
    let name: String

    var description: String {
        return "User: { username: \(username), name: \(name) }"
    }

    init?(response: HTTPURLResponse, representation: Any) {
        guard
            let username = response.url?.lastPathComponent,
            let representation = representation as? [String: Any],
            let name = representation["name"] as? String
        else { return nil }

        self.username = username
        self.name = name
    }
}
Alamofire.request("https://example.com/users/mattt").responseObject { (response: DataResponse<User>) in
    debugPrint(response)

    if let user = response.result.value {
        print("User: { username: \(user.username), name: \(user.name) }")
    }
}

也可以使用相同的方法来处理返回对象集合的表示的端点:

protocol ResponseCollectionSerializable {
    static func collection(from response: HTTPURLResponse, withRepresentation representation: Any) -> [Self]
}

extension ResponseCollectionSerializable where Self: ResponseObjectSerializable {
    static func collection(from response: HTTPURLResponse, withRepresentation representation: Any) -> [Self] {
        var collection: [Self] = []

        if let representation = representation as? [[String: Any]] {
            for itemRepresentation in representation {
                if let item = Self(response: response, representation: itemRepresentation) {
                    collection.append(item)
                }
            }
        }

        return collection
    }
}
extension DataRequest {
    @discardableResult
    func responseCollection<T: ResponseCollectionSerializable>(
        queue: DispatchQueue? = nil,
        completionHandler: @escaping (DataResponse<[T]>) -> Void) -> Self
    {
        let responseSerializer = DataResponseSerializer<[T]> { request, response, data, error in
            guard error == nil else { return .failure(BackendError.network(error: error!)) }

            let jsonSerializer = DataRequest.jsonResponseSerializer(options: .allowFragments)
            let result = jsonSerializer.serializeResponse(request, response, data, nil)

            guard case let .success(jsonObject) = result else {
                return .failure(BackendError.jsonSerialization(error: result.error!))
            }

            guard let response = response else {
                let reason = "Response collection could not be serialized due to nil response."
                return .failure(BackendError.objectSerialization(reason: reason))
            }

            return .success(T.collection(from: response, withRepresentation: jsonObject))
        }

        return response(responseSerializer: responseSerializer, completionHandler: completionHandler)
    }
}
struct User: ResponseObjectSerializable, ResponseCollectionSerializable, CustomStringConvertible {
    let username: String
    let name: String

    var description: String {
        return "User: { username: \(username), name: \(name) }"
    }

    init?(response: HTTPURLResponse, representation: Any) {
        guard
            let username = response.url?.lastPathComponent,
            let representation = representation as? [String: Any],
            let name = representation["name"] as? String
        else { return nil }

        self.username = username
        self.name = name
    }
}
Alamofire.request("https://example.com/users").responseCollection { (response: DataResponse<[User]>) in
    debugPrint(response)

    if let users = response.result.value {
        users.forEach { print("- \($0)") }
    }
}

安全性

在与服务器和Web服务通信时使用安全的HTTPS连接是保护敏感数据的重要步骤。默认情况下,Alamofire将使用安全框架提供的Apple内置验证来评估服务器提供的证书链。虽然这保证证书链是有效的,但它并不妨碍中间人(MITM)攻击或其他潜在的漏洞。为了减轻MITM攻击,处理敏感客户数据或财务信息的应用程序应使用由 ServerTrustPolicy 提供的证书或公钥锁定。

ServerTrustPolicy

ServerTrustPolicy 枚举通过安全的HTTPS连接连接到服务器时,评估通常由 URLAuthenticationChallenge 提供的服务器信任。

let serverTrustPolicy = ServerTrustPolicy.pinCertificates(
    certificates: ServerTrustPolicy.certificates(),
    validateCertificateChain: true,
    validateHost: true
)

服务器信任评估有许多不同的情况,使您可以完全控制验证过程:

  • performDefaultEvaluation :使用默认服务器信任评估,同时允许您控制是否验证由挑战提供的主机。
  • pinCertificates :使用固定证书验证服务器信任。如果其中一个固定证书与其中一个服务器证书匹配,服务器信任被认为是有效的。
  • pinPublicKeys :使用固定的公钥验证服务器信任。如果其中一个固定的公钥与其中一个服务器证书公钥匹配,服务器信任就被视为有效。
  • disableEvaluation :禁用所有评估,反过来总是会将任何服务器信任视为有效。
  • customEvaluation :使用关联的关闭来评估服务器信任的有效性,从而使您完全控制验证过程。谨慎使用。

Server Trust Policy Manager

ServerTrustPolicyManager 负责将服务器信任策略的内部映射存储到特定主机。这允许Alamofire根据不同的服务器信任策略评估每个主机。

let serverTrustPolicies: [String: ServerTrustPolicy] = [
    "test.example.com": .pinCertificates(
        certificates: ServerTrustPolicy.certificates(),
        validateCertificateChain: true,
        validateHost: true
    ),
    "insecure.expired-apis.com": .disableEvaluation
]

let sessionManager = SessionManager(
    serverTrustPolicyManager: ServerTrustPolicyManager(policies: serverTrustPolicies)
)

Make sure to keep a reference to the new SessionManager instance, otherwise your requests will all get cancelled when your sessionManager is deallocated.

这些服务器信任策略将导致以下行为:

  • test.example.com 将始终使用证书链接并启用主机验证,因此需要满足以下条件才能使TLS握手成功:
    • 证书链必须有效。
    • 证书链必须包括其中一个固定证书。
    • 挑战主机必须匹配证书链的叶子证书中的主机。
  • insecure.expired-apis.com 永远不会评估证书链,并始终允许TLS握手成功。
  • 所有其他主机将使用Apple提供的默认评估。
Subclassing Server Trust Policy Manager

如果您发现自己需要更灵活的服务器信任策略匹配行为(即通配符域),那么将 ServerTrustPolicyManager 子类化,并使用自己的自定义实现覆盖 serverTrustPolicyForHost 方法。

class CustomServerTrustPolicyManager: ServerTrustPolicyManager {
    override func serverTrustPolicy(forHost host: String) -> ServerTrustPolicy? {
        var policy: ServerTrustPolicy?

        // Implement your custom domain matching behavior...

        return policy
    }
}

Validating the Host

.performDefaultEvaluation .pinCertificates .pinPublicKeys 服务器信任策略都采用 validateHost 参数。将值设置为 true 将导致服务器信任评估来验证证书中的主机名是否与挑战的主机名匹配。如果不匹配,评估将失败。 false false 值仍将评估完整的证书链,但不会验证叶证书的主机名。

It is recommended that validateHost always be set to true in production environments.

Validating the Certificate Chain

固定证书和公钥都可以使用 validateCertificateChain 参数来验证证书链。通过将此值设置为 true ,除了针对固定证书或公钥执行字节相等检查之外,还将评估完整的证书链。 false 的值将跳过证书链验证,但仍将执行字节等式检查。

在几种情况下,禁用证书链验证可能是有意义的。禁用验证的最常见用例是自签名和过期的证书。在这两种情况下,评估总是失败,但是字节等式检查仍将确保您正在从服务器接收您期望的证书。

It is recommended that validateCertificateChain always be set to true in production environments.

App Transport Security

通过在iOS 9中添加应用传输安全(ATS),可能使用几个 ServerTrustPolicy 对象的自定义 ServerTrustPolicyManager 将不起作用。如果您持续看到 CFNetwork SSLHandshake失败(-9806)错误,可能遇到此问题。苹果的ATS系统将覆盖整个挑战系统,除非您在应用程序的plist中配置ATS设置,以禁用足够的ATS设置,以允许您的应用程序评估服务器信任。

如果遇到此问题(具有自签名证书的概率很高),可以通过将以下内容添加到 Info.plist 中来解决此问题。

<dict>
    <key>NSAppTransportSecurity</key>
    <dict>
        <key>NSExceptionDomains</key>
        <dict>
            <key>example.com</key>
            <dict>
                <key>NSExceptionAllowsInsecureHTTPLoads</key>
                <true/>
                <key>NSExceptionRequiresForwardSecrecy</key>
                <false/>
                <key>NSIncludesSubdomains</key>
                <true/>
                <!-- Optional: Specify minimum TLS version -->
                <key>NSTemporaryExceptionMinimumTLSVersion</key>
                <string>TLSv1.2</string>
            </dict>
        </dict>
    </dict>
</dict>

无论您需要将 NSExceptionRequiresForwardSecrecy 设置为取决于您的TLS连接是否使用允许的密码套件。在某些情况下,需要将其设置为 NO 。必须将 NSExceptionAllowsInsecureHTTPLoads 设置为 YES ,以便允许 SessionDelegate 接收挑战回调。一旦调用了挑战回调函数,代码ServerTrustPolicyManager 将接管服务器信任评估。您可能还需要指定 NSTemporaryExceptionMinimumTLSVersion ,如果您尝试连接到只支持小于 1.2 的TLS版本的主机。

It is recommended to always use valid certificates in production environments.

网络可达性

NetworkReachabilityManager 监听WWAN和WiFi网络接口的主机和地址的可达性更改。

let manager = NetworkReachabilityManager(host: "www.apple.com")

manager?.listener = { status in
    print("Network Status Changed: \(status)")
}

manager?.startListening()

Make sure to remember to retain the manager in the above example, or no status changes will be reported. Also, do not include the scheme in the host string or reachability won't function correctly.

使用网络可达性确定接下来要做什么时,有一些重要的事情要记住

  • 不要使用可达性来确定是否应发送网络请求。
    • 您应该始终发送。
  • 恢复可达性时,请使用该事件重试失败的网络请求。
    • 即使网络请求可能仍然失败,这是重试它们的好时机。
  • 网络可达性状态可用于确定网络请求可能失败的原因。
    • 如果网络请求失败,告诉用户网络请求由于脱机而失败,而不是更为技术性的错误(例如请求超时)更为有用。

It is recommended to check out WWDC 2012 Session 706, "Networking Best Practices" for more info.


打开雷达

以下雷达对Alamofire的当前实现有一些影响。

  • rdar:// 21349340 - 由于免费的编译器投掷警告测试案例中的桥接问题
  • rdar:// 26761490 - Swift字符串插入导致内存泄漏与常见用法
  • rdar:// 26870455 - 后台URL会话配置在模拟器中不起作用
  • rdar:// 26849668 - 一些URLProtocol API没有正确处理 URLRequest

常见问题

阿拉莫夫名字的起源是什么?

Alamofire以 Alamo Fire flower 命名,这是Bluebonnet的混合版本,官方状态得克萨斯花。

路由器中的逻辑与请求适配器有什么逻辑?

简单的静态数据(如路径,参数和公用头)属于路由器。动态数据,例如可以根据身份验证系统更改其值的 Authorization 标题属于 RequestAdapter

动态数据必须放在 RequestAdapter 中的原因是支持重试操作。当 Request 被重试时,原始请求不会被重建,这意味着不再重新调用路由器。再次调用 RequestAdapter ,允许在重新执行 Request 之前对原始请求更新动态数据。


信用

Alamofire由 Alamofire Software Foundation 拥有和维护。您可以通过 @AlamofireSF 在Twitter上关注项目更新和版本。

安全披露

如果您认为您已经发现了Alamofire的安全漏洞,那么您应该尽快通过电子邮件报告给 security@alamofire.org 。请不要发布到公共问题跟踪器。

捐款

ASF 正在筹集资金,正式注册为联邦非营利组织。登记将使我们的成员获得一些法律保护,也允许我们捐赠使用,免税。向ASF捐款将使我们能够:

  • 支付我们的法律费用,作为联邦非营利组织注册
  • 支付我们的年费法律费用,以保持非营利状况良好
  • 支付我们的邮件服务器,帮助我们保持所有问题和安全问题
  • 潜在的资金测试服务器,使我们更容易测试边缘案例
  • 潜在地资助开发人员全职工作我们的项目
社区采用ASF图书馆是惊人的。我们非常沮丧,因为你对项目的热情,并希望继续尽我们所能,把针推向前进。随着您的持续支持,ASF将能够提高其覆盖面,并为核心成员提供更好的法律安全。如果您使用我们的任何图书馆工作,看看您的雇主是否有兴趣捐款。我们的初步目标是筹集1000美元,让我们的所有合法鸭子连续起来,并启动这个运动。您今天可以捐赠的任何金额,以帮助我们实现我们的目标,将不胜感激。

点击此处以支持:Alamofire软件基金会,并在pledgie.com捐款!

许可证

Alamofire是根据MIT许可证发布的。有关详细信息,请参阅许可证。




相关问题推荐