[譯] Alamofire Tutorial: Getting Started

[譯] Alamofire Tutorial: Getting Started

轉載請註明出處:http://leejunhui.com/2017/01/23/Alamofire-Tutorial/html

本文翻譯自RaywenderlichAlamofire Tutorial: Getting Startedios

注:已更新到Alamofire 4, Xcode 8.2, iOS 10以及Swift 3.git

Alamofire是一個爲iOSmacOS打造的並基於Swift的網絡庫.它在Apple的基礎網絡架構上提供了更加優雅的接口來簡化繁重而經常使用的網絡請求任務。
Alamofire提供了鏈式的request/response方法,JSON的傳參和響應序列化,身份認證和其餘特性。在這篇Alamofire教程中,你將使用Alamofire來執行像從第三方提供的RESTfulapi接口上傳文件,請求數據等基本網絡操做。Alamofire的優雅之處在於它完徹底全是由Swift寫成的,而且沒有從它的Objective-C版本-AFNetworking那繼承任何特性。github

你應該對於HTTP網絡有一個概念性的理解,而且還應該接觸過Apple的網絡類好比URLSession.固然Alamofire中的一些細節會有點晦澀難懂,若是你有曾經解決過網絡請求方面的經驗也是極好的。你還須要使用CocoaPods來將Alamofire集成到項目中。web

Getting Started

下載項目代碼 摸我json

下面介紹的項目名爲PhotoTagger,當咱們完成該項目後,能夠實現:從相冊中選擇一張照片(若是你用的是真機測試的話,能夠拍取一張照片),而後上傳這張照片到一個第三方平臺,平臺會對該照片進行圖像識別,而後返回一個tag的列表和這張圖片的主色。swift

img

編譯並運行該項目,你就能夠看到以下界面:api

img

點擊Select Photo,而後選擇一張照片,接着界面背景圖就會變成你選擇的那張照片。打開Main.stroyboard,顯示tags和colors的界面已經爲你準備好了。剩下的工做就是上傳照片和獲取tags以及colors了。數組

The Imagga API

Imagga是一個爲開發者和企業提供構建可伸縮的,以圖片爲主的雲產品的圖像識別服務的網站。你能夠先嚐試下官方的自動標記服務demo:地址.瀏覽器

你須要再Imagga爲本文的項目建立一個免費的開發者帳號。由於Imagga對於每一個HTTP請求都進行了權限認證,因此只有擁有該網站帳號的用戶纔可使用他們的服務。來到 https://imagga.com/auth/signup/hacker,而後註冊。完成註冊後,檢查下是否以下圖所示:
img

Authorization區域列出了一個等會你將用到的token。該token將會被包含在每一個發往Imagga的請求的頭部。

提示:請確認你拷貝了整個token字符串,確認滑動了最右邊並檢查是否拷貝完整。

稍後你將會用到Imagga的api來上傳圖片,taggingapi實現圖像識別,colorsapi用來顏色識別。你能夠在http://docs.imagga.com上查看全部的api。

Installing Dependencies 安裝依賴

在項目主目錄裏建立Podfile文件,內容以下:

platform :ios, '10.0'

inhibit_all_warnings!
use_frameworks!
 
target 'PhotoTagger' do
  pod 'Alamofire', '~> 4.2.0'
end

而後打開終端,來到項目主目錄下,執行pod install命令。若是你的Mac上沒有安裝過CocoaPods的話,能夠查看How to Use CocoaPods with Swift教程。

確保你使用的是CocoaPods的最新版本,不然可能會在安裝第三方庫時報錯。本文撰寫時,最新版本爲1.1.1。

關閉Xcode項目,而後打開新生成的PhotoTagger.xcworkspace文件。編譯而後運行,跑起來的效果應該和以前保持一致。你下一步的任務是添加一些HTTP請求來從RESTful服務獲取一些JSON數據。

REST, HTTP, JSON — 是什麼呢?

若是你對使用HTTP不是特別有經驗的話,那麼你可能會很好奇這些縮寫詞究竟是什麼含義。
HTTP是一種應用層協議,或者能夠理解爲一套網站用來從web服務器傳輸數據到你的電腦屏幕上的規範。你應該看到了你在瀏覽器中所輸入的每一個URL前面都會有HTTPHTTPS做爲前綴。你可能也聽過其它的應用層協議,好比FTP,TelnetSSHHTTP定義了一些客戶端(你的瀏覽器或者app)用來指明所需的行爲,請求的方法或者說動做:
GET: 獲取數據,好比一個WEB頁面。可是不更改服務器上的任何內容。
HEAD: 與GET相同,可是服務器只會返回頭部,並不會返回實際的數據。
POST: 發送數據到服務器,一般用於表單提交。
PUT: 發送數據到指定的路徑。
DELETE: 刪除指定路徑的數據。

REST, 或者表徵狀態轉移,是用來設計可持續的,易於使用的以及易於維護的WEB API的一套規範。REST有幾個體系結構規則,用於強制執行某些操做,例如不在請求之間保持狀態,使請求可緩存,並提供統一的接口。這樣,像您這樣的應用開發者就能夠輕鬆地將API集成到您的應用中,而無需跟蹤請求之間的數據狀態。
JSON表明JavaScript Object Notation; 它提供了一個用於在兩個系統之間用於傳輸數據的直接的,人類可讀的和便攜的機制。JSON的數據類型有:string,boolean,array,object / dictionary,null和number; 整數和小數之間沒有區別。Apple提供JSONSerialization類來幫助將內存中的對象轉換爲JSON,反之亦然。

HTTPRESTJSON的組合構成了做爲開發人員可用的Web服務的很好的一部分。試圖理解每一個細枝末節如何工做多是使人難以應對的。 像Alamofire這樣的庫能夠幫助減小使用這些服務的複雜性,而且在沒有幫助的狀況下讓您的運行速度更快。

Alamofire適合作什麼?

你爲何須要Alamofire?蘋果已經提供了URLSession和其餘類來經過HTTP獲取內容,因此爲何要使用一個第三方庫來增長複雜度呢?
簡單的答案是Alamofire是基於URLSession開發而成的,但它可讓你免於編寫樣板代碼,使編寫網絡代碼更容易。你能夠花費不多的精力就可讓從網絡上獲取數據的操做代碼變得更加簡潔和易於閱讀。
Alamofire有幾個主要功能:

  • .upload:以multipart,流,文件或數據方法上傳文件。

  • .download:下載文件或恢復正在進行的下載。

  • .request:每一個不與文件傳輸相關聯的HTTP請求。

這些Alamofire函數做用於模塊,而不是類或結構體。 Alamofire的基礎部分是類和結構體,如SessionManagerDataRequestDataResponse; 可是,您不須要徹底瞭解Alamofire的整個結構便可開始使用它。
下面是使用Apple的URLSession和Alamofire的請求函數進行相同網絡操做的示例:

// With URLSession
public func fetchAllRooms(completion: @escaping ([RemoteRoom]?) -> Void) {
  let url = URL(string: "http://localhost:5984/rooms/_all_docs?include_docs=true")!
 
  var urlRequest = URLRequest(
    url: url,
    cachePolicy: .reloadIgnoringLocalAndRemoteCacheData,
    timeoutInterval: 10.0 * 1000)
  urlRequest.httpMethod = "GET"
  urlRequest.addValue("application/json", forHTTPHeaderField: "Accept")
 
  let task = urlSession.dataTask(with: urlRequest)
  { (data, response, error) -> Void in
    guard error == nil else {
      print("Error while fetching remote rooms: \(error)")
      completion(nil)
      return
    }

guard let data = data,
  let json = try? JSONSerialization.jsonObject(with: data) as? [String: Any] else {
    print("Nil data received from fetchAllRooms service")
    completion(nil)
    return
}

guard let rows = json["rows"] as? [[String: Any]] else {
  print("Malformed data received from fetchAllRooms service")
  completion(nil)
  return
}

let rooms = rows.flatMap({ (roomDict) -> RemoteRoom? in
  return RemoteRoom(jsonData: roomDict)
})

completion(rooms)
  }
 
  task.resume()
}

對比:

// With Alamofire
func fetchAllRooms(completion: @escaping ([RemoteRoom]?) -> Void) {
  Alamofire.request(
    URL(string: "http://localhost:5984/rooms/_all_docs")!,
    method: .get,
    parameters: ["include_docs": "true"])
  .validate()
  .responseJSON { (response) -> Void in
    guard response.result.isSuccess else {
      print("Error while fetching remote rooms: \(response.result.error)")
      completion(nil)
      return
    }

guard let value = response.result.value as? [String: Any],
  let rows = value["rows"] as? [[String: Any]] else {
    print("Malformed data received from fetchAllRooms service")
    completion(nil)
    return
}

let rooms = rows.flatMap({ (roomDict) -> RemoteRoom? in
  return RemoteRoom(jsonData: roomDict)
})

completion(rooms)
  }
}

你能夠看到Alamofire所須要的設置更加簡單,函數可讀性也更高。你使用responseJSON(options:completionHandler:) 來反序列化相應結果,而後調用validate()方法來簡化錯誤處理。如今是時候來實踐使用Alamofire了。

上傳文件

打開ViewController.swift文件,而後在頂部添加如下代碼:

import Alamofire

這使你能夠在代碼中使用Alamofire模塊提供的功能,而後,在該文件中末尾處添加如下代碼:

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

將圖片上傳到Imagga的第一步是將圖片轉換爲適用於API的正確格式。上面的圖片選擇方法會返回一個轉化成JPEG格式的圖片實例。
而後,來到imagePickerController(_:didFinishPickingMediaWithInfo:)方法,而後在你設置imageView的後面添加如下代碼:

// 1
takePictureButton.isHidden = true
progressView.progress = 0.0
progressView.isHidden = false
activityIndicatorView.startAnimating()
 
upload(
  image: image,
  progressCompletion: { [unowned self] percent in
    // 2
    self.progressView.setProgress(percent, animated: true)
  },
  completion: { [unowned self] tags, colors in
    // 3
    self.takePictureButton.isHidden = false
    self.progressView.isHidden = true
    self.activityIndicatorView.stopAnimating()

self.tags = tags
self.colors = colors

// 4
self.performSegue(withIdentifier: "ShowResults", sender: self)
})

Alamofire的一切行爲都是異步的,這意味着你將以異步的方式更新UI。

1.隱藏上傳按鈕,並顯示進度條和活動視圖。
2.當文件上傳時,你能夠調用進度回調來獲取實時的進度百分比,而後能夠更新進度條上的進度。
3.當上傳完成後將執行完成處理回調,控件的狀態也將會恢復到初始狀態。
4.最後,在一個成功或者失敗的上傳後進入結果界面,用戶界面不會根據錯誤狀況變化。

而後,咱們回到upload(image:progressCompletion:completion:)方法,而後在轉化UIImage實例代碼後面添加以下代碼:

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

請確保從Imagga網站上獲取到你本身的Basic受權令牌,而後替換掉代碼中Basic xxx的部分。這裏你將JPEG數據轉換爲MIME multipart請求併發送到Imagga內容服務api。
下一步,添加以下代碼:

switch encodingResult {
case .success(let upload, _, _):
  upload.uploadProgress { progress in
    progressCompletion(Float(progress.fractionCompleted))
  }
  upload.validate()
  upload.responseJSON { response in
  }
case .failure(let encodingError):
  print(encodingError)
}

這兩段代碼經過調用Alamofireupload方法,而後隨着文件上傳,經過一個簡單的計算來更新進度條UI。以後驗證響應的狀態代碼是否在默承認接受的範圍內(在200和299之間)。

注意:在Alamofire 4以前,不能保證老是在主隊列上調用進度回調。而從Alamofire 4開始,新的進度回調API則老是在主隊列上調用。

接下來,在upload.responseJSON中添加如下代碼:

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

下面是上面代碼每一個步驟的解釋:
1.檢查響應是否成功,若是失敗,打印錯誤並調用完成回調函數。
2.檢查響應的每一個部分,並驗證預期的類型是否就是接收到的實際類型,而後從響應中檢索firstFileID,若是firstFileID沒法解析,則輸出錯誤信息並調用完成回調函數。
3.調用完成回調函數來更新UI。此時,你並無下載到任何標籤和顏色數據,因此只須要傳入空數據便可。

注意:每一個響應都有一個帶有值和類型的Result枚舉。 使用自動驗證,當結果返回200到299之間的有效HTTP代碼而且內容類型是在Accept HTTP header字段中指定的有效類型時,將認爲結果成功。

你能夠經過以下所示的添加.validate參數來使用手動驗證響應結果:

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

若是你在上傳中發生錯誤的話,UI界面是不會提示錯誤信息的,它僅僅告訴用戶沒有顏色和標籤返回。這不是最好的用戶體驗,可是對於這篇教程來講已經足夠了。

編譯並運行整個項目,選擇一張圖片而後靜觀其變。進度條漸進式的前進,你應該能夠在控制檯看到以下輸出:

img

恭喜你,你已經成功的經過網絡上傳了一張圖片了。

img

獲取數據

將圖片上傳到Imagga以後,下一步則是從Imagga獲取到它分析以後的標籤數據了。在ViewController文件的擴展方法upload(image:progress:completion:):下添加如下代碼:

func downloadTags(contentID: String, completion: @escaping ([String]) -> Void) {
  Alamofire.request(
    "http://api.imagga.com/v1/tagging",
    parameters: ["content": contentID],
    headers: ["Authorization": "Basic xxx"]
  )
  .responseJSON { response in
    guard response.result.isSuccess else {
      print("Error while fetching tags: \(response.result.error)")
      completion([String]())
      return
    }

guard let responseJSON = response.result.value as? [String: Any] else {
  print("Invalid tag information received from the service")
  completion([String]())
  return
}

print(responseJSON)
completion([String]())
  }
}

請再一次確認替換的Basic xxx是你本身帳號的受權token。上述代碼執行了一個對標籤服務的GET請求,參數是經過上傳成功後返回的一個ID。接着,咱們回到upload(image:progress:completion:)方法,而後替換成功條件下的完成回調方法代碼以下:

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

上述代碼將得到到的標籤數據經過完成回調函數返回給上級調用者。
編譯而後運行整個項目,上傳照片而後控制檯會打印以下結果:
img

本教程裏你不用在乎返回結果裏的confidence score是什麼意思,只須要關心標籤名稱的數組便可。下一步,回到downloadTags(contentID:completion:)方法,而後替換裏面的.responseJSON方法,代碼以下:

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

咱們來逐步分解上面的代碼:
1.檢查返回結果是否成功,若是失敗,打印錯誤信息,而後調用完成回調函數。
2.檢查返回結果的每一個部分,驗證數據類型是否正確,從返回結果中獲取tagsAndConfidences,若是解析失敗,打印錯誤信息,而後調用完成回調函數。
3.遍歷tagsAndConfidences數組裏的每一個字典對象,從中以tag做爲key來提取對應的值。
4.將從服務器獲取的標籤數據返回給上層調用者。

注意:代碼裏使用Swift的flatMap方法來遍歷數組裏面的每一個字典。這個方法在遍歷到nil的值得時候並不會讓程序crash掉,而是直接將這些nil值移除掉,而後只返回正確的結果。因此經過使用可選解包(as?)來驗證字典中的值是否能夠被轉化成爲字符串。

編譯而後運行整個項目,上傳一張圖片,而後你能夠在界面上看到以下效果:

img

縱享絲滑,Imagga不愧是一個智能的api。下一步,你將要獲取的是圖片的顏色。添加以下代碼到ViewController中的downloadTags(contentID:completion:):方法下面:

func downloadColors(contentID: String, completion: @escaping ([PhotoColor]) -> Void) {
  Alamofire.request(
    "http://api.imagga.com/v1/colors",
    parameters: ["content": contentID],
    // 1.
    headers: ["Authorization": "Basic xxx"]
  )
  .responseJSON { response in
    // 2.
    guard response.result.isSuccess else {
      print("Error while fetching colors: \(response.result.error)")
      completion([PhotoColor]())
      return
    }

// 3.
guard let responseJSON = response.result.value as? [String: Any],
  let results = responseJSON["results"] as? [[String: Any]],
  let firstResult = results.first,
  let info = firstResult["info"] as? [String: Any],
  let imageColors = info["image_colors"] as? [[String: Any]] else {
    print("Invalid color information received from service")
    completion([PhotoColor]())
    return
}

// 4.
let photoColors = imageColors.flatMap({ (dict) -> PhotoColor? in
  guard let r = dict["r"] as? String,
    let g = dict["g"] as? String,
    let b = dict["b"] as? String,
    let closestPaletteColor = dict["closest_palette_color"] as? String else {
      return nil
  }

  return PhotoColor(red: Int(r),
                    green: Int(g),
                    blue: Int(b),
                    colorName: closestPaletteColor)
})

// 5.
completion(photoColors)
  }
}

下面按照編號來依次解析:
1.確保Basic xxx是你本身帳號所屬的認證token。
2.檢查返回結果是否成功,若是失敗,打印錯誤信息,而後調用完成回調函數。
3.檢查返回結果的每一個部分,驗證數據類型是否正確。從返回結果中獲取imageColors,若是解析失敗,打印錯誤信息,而後調用完成回調函數。
4.再次使用flatMap方法,對服務器返回的PhotoColors對象進行遍歷,將裏面的符合RGB格式的數據轉換字符串,而後封裝成PhotoColor對象。
5.調用完成回調函數,傳入服務器返回的圖片顏色數據。

最後,回到upload(image:progress:completion:)方法,而後替換掉成功條件下的調用完成回調函數:

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

上述代碼嵌套了上傳圖片,獲取標籤以及獲取顏色的操做。
編譯而後運行整個項目,這一次當你選擇Colors按鈕後,界面會有如下效果:

img

這塊主要使用了映射到PhotoColor結構體的RGB顏色來渲染UI。你已經成功的往Imagga上傳了一張圖片,以及調用了2個不一樣的api來獲取數據。你已經作得很不錯了,可是在如何使用Alamofire上,咱們還有改進的餘地。

改進 PhotoTagger

你應該注意到了咱們前面的代碼中有許多重複代碼。若是Imagga官方宣佈廢除掉v1版本的api,並推出v2版本的api。咱們的應用將再也不可用直到你將每一個方法裏面的URL都修改過來。一樣的道理,若是你的Basic認證token發生了變化,那麼你須要在全部用到的地方作出修改。
Alamofire提供了一個簡單的方法來消除代碼重複的問題並提供了集中式的配置方法。該技術涉及建立符合URLRequestConvertible協議的結構體,而後更新的上傳和請求的方法。
建立一個新的Swfit文件,命名爲ImaggaRouter.swift,而後在文件裏替換成如下代碼:

import Foundation
import Alamofire
 
public enum ImaggaRouter: URLRequestConvertible {
  static let baseURLPath = "http://api.imagga.com/v1"
  static let authenticationToken = "Basic xxx"
 
  case content
  case tags(String)
  case colors(String)
 
  var method: HTTPMethod {
    switch self {
    case .content:
      return .post
    case .tags, .colors:
      return .get
    }
  }
 
  var path: String {
    switch self {
    case .content:
      return "/content"
    case .tags:
      return "/tagging"
    case .colors:
      return "/colors"
    }
  }

  public func asURLRequest() throws -> URLRequest {
    let parameters: [String: Any] = {
      switch self {
      case .tags(let contentID):
        return ["content": contentID]
      case .colors(let contentID):
        return ["content": contentID, "extract_object_colors": 0]
      default:
        return [:]
      }
    }()

let url = try ImaggaRouter.baseURLPath.asURL()

var request = URLRequest(url: url.appendingPathComponent(path))
request.httpMethod = method.rawValue
request.setValue(ImaggaRouter.authenticationToken, forHTTPHeaderField: "Authorization")
request.timeoutInterval = TimeInterval(10 * 1000)

return try URLEncoding.default.encode(request, with: parameters)
  }
}

替換Basic xxx爲你本身帳號的認證Token。這個路由類經過提供三個不一樣的分類:.content, .tags(String),.colors(String)來實現建立多個URLRequest實例。如今你的重複代碼都集中到了一個地方,若是有須要在這裏更改便可。
回到ViewController.swift文件,而後替換upload(image:progress:completion:)方法:

Alamofire.upload(
  multipartFormData: { multipartFormData in
    multipartFormData.append(imageData,
                             withName: "imagefile",
                             fileName: "image.jpg",
                             mimeType: "image/jpeg")
  },
  to: "http://api.imagga.com/v1/content",
  headers: ["Authorization": "Basic xxx"],

替換爲:

Alamofire.upload(
  multipartFormData: { multipartFormData in
  multipartFormData.append(imageData,
                           withName: "imagefile",
                           fileName: "image.jpg",
                           mimeType: "image/jpeg")
  },
  with: ImaggaRouter.content,

而後替換掉downloadTags(contentID:completion:)方法裏的代碼:

Alamofire.request(ImaggaRouter.tags(contentID))

最後,替換掉downloadColors(contentID:completion:)方法裏的代碼:

Alamofire.request(ImaggaRouter.colors(contentID))

編譯而後運行項目,結果應與以前的效果保持一致。這意味着你已經在不破壞你的app的前提下完成了重構,幹得漂亮!

接下來作什麼?

全部的代碼文件都上傳到了github上,不要忘了替換你本身的Basic認證token。
這篇教程只涵蓋到了很是基礎的知識點。你能夠去Alamofire的官方網站https://github.com/Alamofire/Alamofire上進行更深刻的學習。
進一步的話,你能夠花一些時間來學習Alamofire底層使用的AppleURLSession類的內容:

相關文章
相關標籤/搜索