Alamofire 使用指南

Alamofire 是一款 Swift 寫的 HTTP 網(wǎng)絡(luò)請(qǐng)求庫(kù)

前言

本篇內(nèi)容為 Alamofire 官方 Readme 文件的翻譯,如有翻譯不恰當(dāng)?shù)牡胤?,您也可以給我提交PR,該翻譯github地址。本篇包含如下內(nèi)容:

Alamofire 是 Swift 語(yǔ)言編寫的 HTTP 網(wǎng)絡(luò)庫(kù)。

特性

  • [x] 鏈?zhǔn)秸?qǐng)求 / 響應(yīng)方法調(diào)用
  • [x] URL / JSON / plist 請(qǐng)求參數(shù)編碼
  • [x] 上傳 File / Data / Stream / MultipartFormData
  • [x] 文件下載和斷點(diǎn)續(xù)傳
  • [x] URLCredential 認(rèn)證方式
  • [x] HTTP 響應(yīng)驗(yàn)證
  • [x] 上傳下載進(jìn)度
  • [x] cURL 命令輸出
  • [x] Dynamically Adapt and Retry Requests
  • [x] TLS Certificate and Public Key Pinning
  • [x] 網(wǎng)絡(luò)可用性
  • [x] 測(cè)試單元和集成測(cè)試

組件

為了讓 Alamofire 集中于核心網(wǎng)絡(luò)操作的實(shí)現(xiàn),Alamofire Software Foundation 采用組件的形式為 Alamofire 添加額外的功能。

  • AlamofireImage - 一個(gè)包含圖片響應(yīng)序列化, UIImageUIImageView 的擴(kuò)展,自定義圖片過(guò)濾器,自動(dòng)清理的內(nèi)存緩存,基于優(yōu)先級(jí)的圖片下載的圖片庫(kù)。
  • AlamofireNetworkActivityIndicator - 控制網(wǎng)絡(luò)活動(dòng)指示器的顯示。允許用戶配置延遲時(shí)間來(lái)延緩活動(dòng)指示器的顯示,同時(shí)也支持獨(dú)立創(chuàng)建的 URLSession 實(shí)例對(duì)象。

環(huán)境需求

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

移植指南

安裝

Cocoapods

CocoaPods 是一款 Cocoa 項(xiàng)目的依賴庫(kù)管理工具。使用下面命令安裝 cocoapods:

$ gem install cocoapods

編譯 Alamofire 4.0.0+ 需要 1.1.0 以上版本的 cocoapods

Podfile 中進(jìn)行聲明來(lái)集成 Alamofire:

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

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

然后運(yùn)行下面的命令進(jìn)行安裝:

$ pod install

Carthage

Carthage 是一款編譯管理依賴庫(kù)的工具,可提供編譯好的 frameworks。

通過(guò) Homebrew 使用一下命令安裝 Carthage:

$ brew update
$ brew install carthage

Cartfile 中進(jìn)行聲明來(lái)集成 Alamofire:

github "Alamofire/Alamofire" ~> 4.0

運(yùn)行 carthage update 命令進(jìn)行編譯,并把編譯好的 Alamofire.framework 拖拽到你的 Xcode 項(xiàng)目中。

用法

發(fā)起請(qǐng)求

import Alamofire

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

響應(yīng)回調(diào)

處理請(qǐng)求的響應(yīng)只需在 Request 后面加上處理響應(yīng)的回調(diào)。

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 回調(diào)拼接在 Request 后面,一旦網(wǎng)絡(luò)請(qǐng)求完成便會(huì)執(zhí)行該 responseJSON。這里沒(méi)有阻塞線程來(lái)等待響應(yīng),而是采用了閉包形式的回調(diào)來(lái)異步接受響應(yīng)。請(qǐng)求的結(jié)果只能在響應(yīng)的閉包中進(jìn)行處理。對(duì)響應(yīng)或從服務(wù)器接收到的數(shù)據(jù)只能在響應(yīng)閉包中處理。

Alamofire 中網(wǎng)絡(luò)請(qǐng)求是異步處理的。對(duì)異步編程相關(guān)的概念不太熟悉的話會(huì)讓人一頭霧水,但有太多的理由讓我們采用異步編程。

Alamofire 默認(rèn)包含五種響應(yīng)回調(diào):

// 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

這些回調(diào)都沒(méi)有對(duì) HTTPURLResponse 進(jìn)行驗(yàn)證。

比如,400..499500..599 之間的響應(yīng)狀態(tài)碼不會(huì)自動(dòng)觸發(fā) Error。Alamofire 采用 Response Validation 的鏈?zhǔn)椒椒▉?lái)進(jìn)行驗(yàn)證。

Response 回調(diào)

response 不對(duì)響應(yīng)數(shù)據(jù)進(jìn)行任何處理,直接把 URL session 代理中的數(shù)據(jù)交給后面的流程,與使用 cURL 執(zhí)行請(qǐng)求效果一樣。

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)")
    }
}

強(qiáng)烈建議使用其他響應(yīng)序列化器將數(shù)據(jù)變?yōu)楦资褂玫?ResponseResult 類型

Response Data 回調(diào)

responseData 回調(diào)使用 responseDataSerializer (該對(duì)象用于將從服務(wù)器接收的數(shù)據(jù)序列化為其他類型) 處理服務(wù)器端返回的 Data。如果沒(méi)有錯(cuò)誤就返回 Data,response 的 Result 會(huì)被設(shè)置為 .success value 會(huì)被設(shè)置為 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 回調(diào)

responseString 回調(diào)使用 responseStringSerializer 根據(jù)指定的編碼方式將從服務(wù)器接收到的 Data 轉(zhuǎn)換為 String 類型。如果沒(méi)有錯(cuò)誤并且數(shù)據(jù)被成功序列化為 String,則 response 的 Result 會(huì)被設(shè)置為 .success,value 會(huì)被設(shè)置為 String 類型。

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

如果未指定編碼方式,Alamofire 會(huì)根據(jù)從服務(wù)器端接收到的 HTTPURLResponse 中的編碼方式進(jìn)行編碼。如果響應(yīng)中也未指定編碼方式,默認(rèn)使用 .isoLatin1 編碼方式

Response JSON 回調(diào)

responseJSON 使用 responseJSONSerializer 根據(jù)指定的 JSONSerialization.ReadingOptions 將從服務(wù)器接收到的數(shù)據(jù) Data 轉(zhuǎn)換為 Any 類型。如果沒(méi)有錯(cuò)誤并且接收到的數(shù)據(jù)成功的序列化為 JSON 對(duì)象,則 response 的 Result 會(huì)被設(shè)置為 .success 并且 value 會(huì)被設(shè)置為 Any 類型。

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

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

JSON 的序列化由 Foundation 框架中的 JSONSerialization 接口完成。

鏈?zhǔn)巾憫?yīng)回調(diào)

響應(yīng)回調(diào)也可以鏈?zhǔn)秸{(diào)用:

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

注意:對(duì)同一個(gè)請(qǐng)求使用多個(gè)響應(yīng)回調(diào)需要服務(wù)器對(duì)數(shù)據(jù)進(jìn)行多次序列化,每次序列化針對(duì)一個(gè)響應(yīng)回調(diào)。

響應(yīng)回調(diào)的操作隊(duì)列

響應(yīng)回調(diào)默認(rèn)是在主派發(fā)隊(duì)列中執(zhí)行。然而可以為響應(yīng)回調(diào)指定自定義的操作隊(duì)列。

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

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

響應(yīng)驗(yàn)證

默認(rèn)情況下 Alamofire 會(huì)忽略響應(yīng)內(nèi)容是否正確,只要請(qǐng)求完成就標(biāo)志著成功。在響應(yīng)回調(diào)調(diào)用之前調(diào)用 validata 時(shí),若響應(yīng)中有錯(cuò)誤的網(wǎng)絡(luò)狀態(tài)碼或錯(cuò)誤的 MIME 格式的數(shù)據(jù)則會(huì)拋出錯(cuò)誤。

手動(dòng)驗(yàn)證

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)
      }
    }

自動(dòng)驗(yàn)證

自動(dòng)驗(yàn)證會(huì)驗(yàn)證 200...299 之間的狀態(tài)碼并驗(yàn)證響應(yīng)數(shù)據(jù)的 Content-Type 是否和請(qǐng)求頭的指定的 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)
    }
}

響應(yīng)緩存

響應(yīng)的緩存操作由系統(tǒng)級(jí)框架 URLCache 完成。其同時(shí)提供了內(nèi)存,硬盤兩種緩存方式并且用戶可以設(shè)置可緩存的大小。

Alamofire 默認(rèn)會(huì)使用共享的 URLCache。查看 Session Manager Configurations 進(jìn)行自定義。

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 接口的 method 參數(shù)設(shè)置這些值:

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)

Alamofire.request 的 method 參數(shù)默認(rèn)是 .get

請(qǐng)求參數(shù)編碼

Alamofire 默認(rèn)提供了三種參數(shù)編碼方式,包括 URL,JSON,PropertyList。同時(shí)也支持遵循了 ParameterEncoding 協(xié)議的編碼方式。

URL 編碼

URLEncoding 編碼方式創(chuàng)建了 url 編碼的查詢字符串并將其拼接到存在的請(qǐng)求字符串后或者設(shè)置為 URL 請(qǐng)求的 HTTP body。對(duì)于編碼后的查詢字符串,是直接使用,拼接還是設(shè)置為 HTTP body 取決于編碼的 DestinationDestination 包含三種方式:

  • .methodDependent - 若請(qǐng)求方式是 GET,HEAD,DELETE,則將編碼的查詢字符串與存在的查詢字符串進(jìn)行拼接,對(duì)于其他請(qǐng)求方式則設(shè)置為請(qǐng)求的 HTTP body。
  • .queryString - 將編碼的查詢字符串與存在的查詢字符串進(jìn)行拼接。
  • .httpBody - 設(shè)置為請(qǐng)求的 HTTP body。

請(qǐng)求頭中的 Content-Type 字段被設(shè)置為 application/x-www-form-urlencoded; charset=utf-8。URL 編碼中并沒(méi)有規(guī)定集合類型該如何進(jìn)行編碼。我們約定,對(duì)數(shù)組類型將[]拼接到 key 后面(foo[]=1&foo[]=2),對(duì)字典類型將中括號(hào)包圍的 key 拼接在請(qǐng)求的鍵后(foo[bar]=baz`)。

獲取使用 URL 編碼參數(shù)的請(qǐng)求
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
發(fā)起使用 URL 編碼參數(shù)的請(qǐng)求
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", parameters: parameters)
Alamofire.request("https://httpbin.org/post", parameters: parameters, encoding: URLEncoding.default)
Alamofire.request("https://httpbin.org/post", parameters: parameters, encoding: URLEncoding.httpBody)

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

JSON 編碼

JSONEncoding 的編碼方式創(chuàng)建了 JSON 格式的請(qǐng)求參數(shù),并設(shè)置為請(qǐng)求的 HTTP body。HTTP 請(qǐng)求頭的 Content-Type 字段設(shè)置為 applicatioin/json。

發(fā)起使用 JSON 編碼參數(shù)的請(qǐng)求
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"}}

自定義編碼

當(dāng) Alamofire 提供的參數(shù)編碼方式不能滿足需求時(shí),可以創(chuàng)建自定義的編碼方式。下面是一個(gè)自定義的 JSONStringEncoding 編碼方式的例子,該方式將 string 數(shù)組的 JSON 對(duì)象編碼到 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
    }
}

下載數(shù)據(jù)到文件

Alamofire 從服務(wù)器接收到的數(shù)據(jù)保存在緩存或硬盤上。到目前為止所有例子中使用 Alamofire.request 接口獲取的數(shù)據(jù)都保存在緩存中。對(duì)于小數(shù)據(jù)這是很高效的,但對(duì)于較大的數(shù)據(jù)量可能會(huì)耗盡緩存。因此需要使用 Alamofire.download 接口將數(shù)據(jù)保存在硬盤的臨時(shí)文件中。

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

當(dāng)需要在后臺(tái)下載數(shù)據(jù)時(shí)也應(yīng)該使用 Alamofire.download 接口。更多信息請(qǐng)查看 Session Manager Configurations 章節(jié)

下載路徑

你可以提供一個(gè) DownloadFileDestination 閉包用于把臨時(shí)文件移動(dòng)到指定的路徑下。在移動(dòng)臨時(shí)文件前會(huì)先執(zhí)行閉包中指定的 DownloadOptioins。當(dāng)前支持的兩種 DownloadOptions 分別是:

  • .createIntermediateDirectories - 為指定的路徑創(chuàng)建完整的路徑
  • .removePreviousFile - 移除目標(biāo)路徑下存在的文件
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)

下載進(jìn)度

在下載時(shí)能夠報(bào)告下載進(jìn)度是非常有用的。任何 DownloadRequest 請(qǐng)求可以通過(guò) downloadProgress 接口報(bào)告下載進(jìn)度。

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 接口指定下載進(jìn)度閉包執(zhí)行的派發(fā)隊(duì)列。

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)
      }
    }

喚醒下載

如果一個(gè) DownloadRequest 請(qǐng)求取消或中斷了,URL 會(huì)話可能會(huì)為該請(qǐng)求生成恢復(fù)數(shù)據(jù),該恢復(fù)數(shù)據(jù)可用于 DownloadRequest 請(qǐng)求從中斷的地方恢復(fù)下載?;謴?fù)數(shù)據(jù)可以從下載響應(yīng)中獲取,然后用于恢復(fù)下載。

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
          }
        }
    }
}

上傳數(shù)據(jù)到服務(wù)器

上傳少量的數(shù)據(jù)到服務(wù)器可以采用 JSON 或者 URL 編碼參數(shù)的方式進(jìn)行,這時(shí) Alamofire.request 接口通常很高效。當(dāng)需要上傳的數(shù)據(jù)較大,比如文件或者 InputStream,這時(shí)需要使用 Alamofire.upload 接口。

當(dāng)需要在后臺(tái)上傳數(shù)據(jù)時(shí)也應(yīng)該使用 Alamofire.upload,更多信息請(qǐng)查看 Session Manager Configurations 章節(jié)。

上傳數(shù)據(jù)

let imageData = UIPNGRepresentation(image)!

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

上傳文件

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

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

上傳多格式表單數(shù)據(jù)

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)
      }
    }
)

上傳進(jìn)度

當(dāng)用戶在上傳時(shí)能夠顯示上傳進(jìn)度是非常友好的。任何 UploadRequest 請(qǐng)求都能通過(guò) uploadProgressdoanloadProgress 接口報(bào)告上傳進(jìn)度和下載進(jìn)度。

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)
}

注意,這些接口僅在 iOS,tvOS 10 和 macOS 10.12 三個(gè)平臺(tái)上可用。因此,取決于您的部署環(huán)境,您需要做以下檢測(cè):

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

cURL 命令輸出

不好的調(diào)試平臺(tái)會(huì)讓工作變得很麻煩. 幸好, Alamofire Request 對(duì)象實(shí)現(xiàn)了 CustomStringConvertibleCustomDebugStringConvertible 協(xié)議,這為我們提供了很好的調(diào)試工具。

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"

高級(jí)用法

Alamofire 建立在 URLSession 和 URL 加載系統(tǒng)上。為了更好的使用該框架,強(qiáng)烈建議要非常熟悉底層網(wǎng)絡(luò)棧的相關(guān)概念

推薦閱讀

會(huì)話管理

頂層的 Alamofire 接口例如 Alamofire.request 使用了默認(rèn)的 Alamofire.SessionManager 會(huì)話管理對(duì)象發(fā)起網(wǎng)絡(luò)請(qǐng)求。該會(huì)話管理對(duì)象默認(rèn)使用了 URLSessionConfiguration 進(jìn)行配置。

因此下面兩段代碼的是等效的:

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

您可以為應(yīng)用創(chuàng)建會(huì)后臺(tái)任務(wù)會(huì)話管理對(duì)象,臨時(shí)會(huì)話管理對(duì)象,同時(shí)也可以修改默認(rèn)的會(huì)話配置,比如默認(rèn)的請(qǐng)求頭 (httpAdditionalHeaders) 或者請(qǐng)求超時(shí)時(shí)間 (timeoutIntervalForRequest)。

創(chuàng)建默認(rèn)配置會(huì)話管理對(duì)象

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

創(chuàng)建后臺(tái)任務(wù)會(huì)話管理對(duì)象

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

創(chuàng)建臨時(shí)配置會(huì)話管理對(duì)象

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

修改會(huì)話配置

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

let configuration = URLSessionConfiguration.default
configuration.httpAdditionalHeaders = defaultHeaders

let sessionManager = Alamofire.SessionManager(configuration: configuration)

推薦使用這種方式修改 AuthorizationContent-Type 等請(qǐng)求頭信息。推薦使用 Alamofire.request 接口中的 headers 參數(shù), URLRequestConvertibleParameterEncoding 等方式修改請(qǐng)求頭信息。

會(huì)話代理

Alamofire 的會(huì)話管理對(duì)象默認(rèn)創(chuàng)建了一個(gè)會(huì)話代理對(duì)象來(lái)處理 URLSession 產(chǎn)生的各種代理回調(diào)事件。這些代理方法實(shí)現(xiàn)的功能能夠應(yīng)付絕大部分的使用場(chǎng)景并且為隱藏了復(fù)雜的內(nèi)部調(diào)用為用戶提供了簡(jiǎn)單的上層接口。然而,您仍有可能會(huì)因?yàn)楦鞣N各樣的需求而重載這些代理方法的實(shí)現(xiàn)。

重載閉包

第一種自定義 SessionDelegate 行為的方式是重載閉包。通過(guò)閉包您可以重載對(duì)應(yīng)的 SessionDelegate 接口,并且其他接口的實(shí)現(xiàn)將保持不變。這讓實(shí)現(xiàn)一個(gè)自定義的代理方法集合變得很容易。下面是一些可用的可重載的閉包:

/// 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?)?

下面的例子通過(guò)重載 taskWillPerformHTTPRedirection 閉包來(lái)避免請(qǐng)求重定向到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
}

繼承

另一種重載 SessionDelegate 默認(rèn)實(shí)現(xiàn)的方式是繼承。通過(guò)繼承您可以實(shí)現(xiàn)完全的自定義或者仍然使用默認(rèn)實(shí)現(xiàn)僅為接口創(chuàng)建一個(gè)代理。通過(guò)為接口創(chuàng)建代理,您可以在調(diào)用接口默認(rèn)實(shí)現(xiàn)的前后增加日志消息,派發(fā)通知等功能。下面的例子繼承了 SessionDelegate,并且當(dāng)發(fā)生重定向時(shí)打印消息日志。

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
        )
    }
}

請(qǐng)求

request,download,upload,stream 等方法的返回值 DataRequest, DownloadRequest, UploadRequestStreamRequest 均是繼承于 Request。所有的 Request 實(shí)例都是由自己的會(huì)話管理對(duì)象創(chuàng)建,并且不會(huì)直接初始化。

每個(gè)子類都有一些特殊的方法比如 authenticate, validate, responseJSONuploadProgress,這些方法均返回調(diào)用者以便可以進(jìn)行鏈?zhǔn)秸{(diào)用。

請(qǐng)求可以被掛起,恢復(fù),取消:

  • suspend(): 掛起底層任務(wù)和派發(fā)隊(duì)列。
  • resume(): 恢復(fù)任務(wù)和派發(fā)隊(duì)列。如果會(huì)話管理對(duì)象沒(méi)有設(shè)置 startRequestsImmediatelytrue,那么請(qǐng)求需要調(diào)用 resume() 才能開(kāi)始。
  • cancel(): 取消任務(wù),產(chǎn)生錯(cuò)誤信息并將錯(cuò)誤信息傳遞到響應(yīng)回調(diào)。

請(qǐng)求路由

隨著 App 變得復(fù)雜,使用通用模式創(chuàng)建你自己的網(wǎng)絡(luò)棧就變得非常重要了。其中一個(gè)重要的設(shè)計(jì)就是如何路由你的請(qǐng)求。遵循 URLConvertibleURLRequestConvertible 協(xié)議的 Router 就變得非常有用。

URLConvertible

遵循 URLConvertible 協(xié)議的類可以用來(lái)構(gòu)造 URLs,然后將 URLs 用來(lái)構(gòu)造 URL 請(qǐng)求。String, URL, 和 URLComponents 都遵循了 URLConvertible 協(xié)議,這三個(gè)類的對(duì)象均可以作為 url 參數(shù)傳遞給 request, upload, 和 download 方法:

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(.post, URLComponents)

與 web 服務(wù)器交互時(shí)推薦通過(guò)實(shí)現(xiàn) URLConvertible 協(xié)議來(lái)做域名型模型與服務(wù)器資源的映射。

類型安全路由
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

實(shí)現(xiàn)了 URLRequestConvertible 協(xié)議的類型可以用來(lái)構(gòu)造 URL 請(qǐng)求。URLRequest 默認(rèn)實(shí)現(xiàn)了 URLRequestConvertible 協(xié)議,這使得 URLRequest 可直接傳遞給 request,upload,download等方法(推薦使用這種方式實(shí)現(xiàn)自定義 HTTP body)

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 服務(wù)器交互時(shí)推薦通過(guò)實(shí)現(xiàn) URLRequestConvertible 協(xié)議以確保請(qǐng)求端點(diǎn)的一致性。這種方法可以用于抽象出服務(wù)器端不一致并提供類型安全路由,以及管理認(rèn)證憑證和其他狀態(tài)

API 抽象參數(shù)
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

當(dāng)今的很多 web 服務(wù)都可以通過(guò)授權(quán)系統(tǒng)進(jìn)行訪問(wèn)。其中最常用的是 OAuth。OAuth 會(huì)生成一個(gè)訪問(wèn)令牌來(lái)授權(quán)你的應(yīng)用訪問(wèn)權(quán)限內(nèi)的 web 服務(wù)。創(chuàng)建令牌可能會(huì)很麻煩,令牌過(guò)期需要考慮很多線程安全的問(wèn)題,這會(huì)讓情況變得更復(fù)雜。

RequestAdapterRequestRetrier 協(xié)議讓創(chuàng)建線程安全的授權(quán)系統(tǒng)變得容易。

RequestAdapter

RequestAdapter 協(xié)議允許 SessionManager 在創(chuàng)建 Request 前為 Request 做額外的檢查和適配工作。比較常用的應(yīng)用場(chǎng)景是為請(qǐng)求拼接授權(quán)參數(shù)。

class AccessTokenAdapter: RequestAdapter {
  private let accessToken: String

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

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

      if urlRequest.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 協(xié)議允許網(wǎng)絡(luò)請(qǐng)求發(fā)生錯(cuò)誤時(shí)重新發(fā)起請(qǐng)求。通過(guò)同時(shí)實(shí)現(xiàn) RequestAdapterRequestRetrier 協(xié)議,您可以為 OAuth1,OAuth2,基本授權(quán),重試策略創(chuàng)建一個(gè)證書(shū)刷新系統(tǒng)。您能實(shí)現(xiàn)的功能不局限于此。下面的例子展示了 OAuth2 令牌的刷新流程。

免責(zé)聲明:不是一個(gè)全局的 OAuth2 解決方案。下面的代碼僅作為簡(jiǎn)單示例展示了如何通過(guò) RequestAdapterRequestRetrier 協(xié)議來(lái)實(shí)現(xiàn)線程安全的刷新系統(tǒng)。

重申,不要拷貝下面的示例代碼到您的產(chǎn)品中。該代碼片段僅能作為示例。每一個(gè)授權(quán)系統(tǒng)應(yīng)該基于平臺(tái)和授權(quán)類型做相應(yīng)的修改。

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 url = urlRequest.url, url.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)
}

SessionManageradapterretrier 被設(shè)置為 OAuth2Handler后,當(dāng)令牌失效時(shí),便會(huì)自動(dòng)刷新令牌并嘗試按失敗的順序重新發(fā)起請(qǐng)求。

如果您想按創(chuàng)建網(wǎng)絡(luò)請(qǐng)求的順序重新發(fā)起請(qǐng)求,您可以通過(guò)網(wǎng)絡(luò)請(qǐng)求任務(wù)的 id 進(jìn)行排序。

該示例僅檢查了響應(yīng)的 401 狀態(tài)碼,作為檢測(cè)失效令牌的例子這已經(jīng)足夠。O在實(shí)際產(chǎn)品中,您應(yīng)該還要檢測(cè)響應(yīng)頭中的 reamlwww-authenticate 等字段。

還需要注意的是該授權(quán)系統(tǒng)可以在多個(gè)會(huì)話管理對(duì)象間共享。比如,您可以為同一個(gè) web 服務(wù)集同時(shí)使用 defaultephemeral 會(huì)話配置。上面的例子允許 oauthHandler 實(shí)例對(duì)象在多個(gè)會(huì)話管理對(duì)象間共享并管理各自的刷新流程。

自定義響應(yīng)序列化器

錯(cuò)誤處理

過(guò)去在實(shí)現(xiàn)自定義響應(yīng)序列化器或?qū)ο笮蛄谢椒〞r(shí)著重考慮的是錯(cuò)誤信息的處理。這里有兩個(gè)可選項(xiàng):對(duì)錯(cuò)誤信息不做任何處理直接向下傳遞,由用戶在響應(yīng)回調(diào)處處理;或者為您的應(yīng)用定義一個(gè)包含所有錯(cuò)誤類型的 Error 枚舉類。

下面的 BackendError 枚舉類在后面的例子中也會(huì)出現(xiàn):

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)
}

自定義響應(yīng)序列化器

Alamofire 為 strings,JSON,property lsits 提供了內(nèi)置的響應(yīng)序列化器,您也可以為 Alamofire.DataRequestAlamofire.DownloadRequest 進(jìn)行擴(kuò)展。

下面的例子展示了響應(yīng)序列化器使用 Ono 的實(shí)現(xiàn)方式:

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
        )
    }
}

通用響應(yīng)對(duì)象序列化

通用序列化可以進(jìn)行自動(dòng),類型安全的對(duì)象序列化。

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) }")
    }
}

相同的方法也可以用于處理終端返回的對(duì)象集合:

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)") }
    }
}

請(qǐng)求適配器

安全性

在與 web 服務(wù)器交互傳輸敏感數(shù)據(jù)時(shí)應(yīng)該使用安全的 HTTPS 連接。默認(rèn)情況下,Alamofire 會(huì)使用蘋果提供的 Security 框架對(duì)服務(wù)器提供的證書(shū)串進(jìn)行驗(yàn)證。這樣僅僅能確保服務(wù)器端證書(shū)是否有效,并不能防止中間人攻擊 man-in-the-middle(MITM) 或其他潛在的漏洞。為了降低遭受中間人攻擊的可能性,應(yīng)用在處理敏感用戶的數(shù)據(jù)或金融信息時(shí)應(yīng)該配合使用證書(shū)或 ServerTrustPolicy 提供的公鑰鎖定

ServerTrustPolicy

Server Trust Policy Manager

繼承 Server Trust Policy Manager

驗(yàn)證主機(jī)

驗(yàn)證證書(shū)串

App Transport Security

<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>

網(wǎng)絡(luò)可用性

NetworkReachabilityManager 可用于監(jiān)聽(tīng) WWAN 和 WiFi 網(wǎng)絡(luò)到指定主機(jī)或 IP 地址的連接狀態(tài)。

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

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

manager?.startListening()

請(qǐng)確保對(duì) 網(wǎng)絡(luò)狀態(tài)監(jiān)聽(tīng)對(duì)象 有強(qiáng)引用,否則不會(huì)監(jiān)聽(tīng)到任何網(wǎng)絡(luò)狀態(tài)。

在監(jiān)聽(tīng)網(wǎng)絡(luò)狀態(tài)時(shí)需要注意以下幾點(diǎn):

  • 不要根據(jù)網(wǎng)絡(luò)狀態(tài)來(lái)決定是否發(fā)送網(wǎng)絡(luò)請(qǐng)求。
    • 只管發(fā)送就行
  • 當(dāng)網(wǎng)絡(luò)恢復(fù)連接,對(duì)失敗的網(wǎng)絡(luò)請(qǐng)求重新發(fā)起請(qǐng)求。
    • 盡管重新發(fā)起請(qǐng)求仍有可能失敗,但您仍應(yīng)該嘗試。
  • 網(wǎng)絡(luò)狀態(tài)有助于分析出請(qǐng)求失敗原因。
    • 如果網(wǎng)絡(luò)請(qǐng)求失敗,提示用戶網(wǎng)絡(luò)處于離線狀態(tài)要比更具體的錯(cuò)誤信息比如"請(qǐng)求超時(shí)"等更友好。

更多信息請(qǐng)參考 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

Alamofire 名字由來(lái)

Alamofire 花,矢車菊的一種,是德克薩斯州的官方州花。

請(qǐng)求路由和請(qǐng)求適配器的區(qū)別

資源路徑,請(qǐng)求參數(shù),公共請(qǐng)求頭這些靜態(tài)數(shù)據(jù)屬于 路由 范疇。認(rèn)證 頭這類會(huì)隨著認(rèn)證系統(tǒng)發(fā)生變化的動(dòng)態(tài)數(shù)據(jù)屬于 請(qǐng)求適配器 范疇。

致謝

Alamofire 由 Alamofire 軟件基金會(huì) 所有并維護(hù)。您可以通過(guò)關(guān)注我們的 Twitter 官方賬號(hào) @AlamofireSF 來(lái)獲取最新的更新發(fā)布消息。

捐款

開(kāi)源協(xié)議

Alamofire 在 MIT 開(kāi)源協(xié)議下發(fā)布。更多信息請(qǐng)查看 LICENSE 文件。


歡迎關(guān)注我的簡(jiǎn)書(shū),我會(huì)定期做一些技術(shù)分享:)

最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請(qǐng)聯(lián)系作者
【社區(qū)內(nèi)容提示】社區(qū)部分內(nèi)容疑似由AI輔助生成,瀏覽時(shí)請(qǐng)結(jié)合常識(shí)與多方信息審慎甄別。
平臺(tái)聲明:文章內(nèi)容(如有圖片或視頻亦包括在內(nèi))由作者上傳并發(fā)布,文章內(nèi)容僅代表作者本人觀點(diǎn),簡(jiǎn)書(shū)系信息發(fā)布平臺(tái),僅提供信息存儲(chǔ)服務(wù)。

相關(guān)閱讀更多精彩內(nèi)容

友情鏈接更多精彩內(nèi)容