本章節由CocoaChina翻譯組成員星夜暮晨(博客)翻譯自raywenderlich:Beginning Alamofire Tutorial,敬請勘誤。
AFNetworking是 iOS 和 OS X 上最受歡迎的第三方庫之一。它曾在我們的2012年的讀者評選中榮獲2012年度最佳 iOS 庫稱號。它同樣也在 Github 上面獲得了14000多個 stars 和4000多個 forks,是使用最廣的開源項目之一。
最近,AFNetworking 的作者Mattt Thompson提交了一個新的類似於 AFNetworking 的網絡 基礎庫,並且是專門使用最新的 Swift 語言來編寫的,名為:Alamofire
AFNetwork 的前綴 AF 是 Alamofire 的縮寫,因此這個新的庫名稱其實是根據 Swift 的約定來進行命名的。
在本教程的第一部分中,我們將帶領大家使用 Alamofire 來制作一個圖片庫應用,其來源是500px.com。在這個過程中,您可以學習到如何使用 Alamofire 中的重要組件,以及了解在應用中處理網絡請求的某些重要的知識點。
本教程的第二部分將基於第一部分所制作的應用,並為其增加一些好用的功能。您可以通過這個過程學習到更多高級的 Alamofire 用法。
本教程假定您已熟悉 Swift 語言以及 iOS 開發。如果不是的話,請參閱我們的其他教程。還有,本教程使用 Xcode 6.1 作為開發環境(而不是 Xcode 6.2 beta)。
> 提示:如果您已經熟悉了 Alamofire 的基本用法,那麼您可以直接跳到本文的第二部分。但是請確保您已擁有消費者密鑰(cunsumer key),然後參照下文在應用中替換它。
讓我們開始吧
首先下載本次教程的初始項目。這個項目中提供了在本教程中需要的全部 UI。這有助於您能夠將注意力集中到學習 Alamofire 的使用上來,而不是花費大量時間來研究 UI。
在 Xcode 中打開這個項目,並跳轉到Main.storyboard來:
我們應用的主屏幕使用UITabBarController這個常用的 UI 樣式。我們的標簽控制器中包含有兩個標簽,每個標簽都有它們自己的UINavigationController頁面。第一個標簽讓用戶浏覽熱門圖片。第二個標簽讓用戶浏覽他們已保存的文件。兩個標簽都使用`UICollectionViewController`來向用戶展示圖片。故事板中同樣也包含了兩個獨立的視圖控制器,在接下來的教程中我們將會用到它們。
生成並運行該應用,您首先會看到一個不停在加載的加載控件:
這看起來一點也不高端大氣上檔次……可以說沒什麼可看的。但是很快我們將會借助 Alamofire 來讓他逼格高起來。
> 提示:如果您很熟悉 AFNetworking 的使用,那麼您可能會覺得下一節我們將談論 CocoaPods。但是就目前來說,還沒有一個通過 CocoaPods 來整合 Swift 庫的簡單方法。
雖然說可能有人發現了該問題的解決方法,並將其上傳到網上,但是下面的步驟仍然還是使用手動復制代碼到項目中的這樣一個可靠的方法。
要獲取最新版本的 Alamofire,前往https://github.com/Alamofire/Alamofire然後單擊網頁右邊的Download ZIP按鈕。接著在 Finder 中打開起始項目文件夾,然後將Alamofire-master文件夾拖入到您的主項目文件夾中。
打開Alamofire-master文件夾(現在它位於您的項目文件夾中),然後將Alamofire.xcodeprij文件(注意是藍色圖標!不是白色圖標!)直接拖進 Xcode 中的 Photomania 項目下面,如下圖所示:
接下來,單擊Photomania項目,進入General窗口。滾動到Embedded Binaries項,然後單擊其下方的 + 號。選擇Alamofire.framework,最後點擊Add完成添加。
生成並運行您的項目以確保沒有任何錯誤出現,然後就可以進入到下一節內容了。
使用 Alamofire 來檢索數據
您可能會覺得,為什麼我們要使用 Alamofire 呢?明明蘋果已經提供了NSURLSession類以及其相關類,以便讓我們通過 HTTP 來下載相應內容。為什麼我們還要使用第三方庫來讓這件事情變得復雜呢?
簡單來說,Alamofire 其實是基於`NSURLSession`的,但是它可以免去您寫樣板(boilerplate)代碼的麻煩,並且可以讓網絡模塊的代碼更為簡單易用。您可以通過一些非常簡單的操作來訪問 Internet 上的數據,並且寫出來的代碼也會更加清晰明了、簡單易讀。
要使用 Alamofire 的話,首先需要導入它。請打開PhotoBrowserCollectionViewController.swift文件,然後在文件頂部添加如下代碼:
import Alamofire
您需要在每個使用了 Alamofire 類以及函數的文件中添加這條`import`語句。
接下來,在`setupView()`下方的`viewDidLoad()`方法中添加如下代碼:
Alamofire.request(.GET, "https://api.500px.com/v1/photos").responseJSON() { (_, _, data, _) in println(data) }
過會兒我會對其做出詳細解釋,但是首先您需要生成並運行該應用,這個時候您會在控制台中看到如下信息:
Optional({ error = "Consumer key missing."; status = 401; })
您可能不明白它說了什麼鬼,不過您已經成功地使用 Alamofire 來實現網絡請求了!您向 Internet 上的資源發出了一個請求,然後返回了一個JSON 數據。
下面來解釋一下那些代碼到底做了些什麼:
· Alamofire.request(_:_)接收兩個參數:method和URLString。其中,method 通常是.GET、.POST;URLString通常是您想要訪問的內容的 URL。其將返回一個Alamofire.Request對象。
· 通常情況下,您只需將請求對象鏈接到響應方法上。例如,在上面的代碼中,請求對象簡單地調用了responseJSON()方法。當網絡請求完畢後,responseJSON()方法會調用我們所提供的閉包。在我們的示例中,我們只是簡單的將經過解析的 JSON 輸出到控制台中。
· 調用responseJSON方法意味著您期望獲得一個 JSON 數據。在我們的示例中,Alamofire 試圖解析響應數據並返回一個 JSON 對象。或者,您可以使用responsePropertyList來請求獲得一個屬性列表,也可以使用responseString來請求獲得一個初始字符串。在本教程後面,您將了解更多關於響應序列化方法的使用方式。
您可以從控制台中看到輸出的響應數據,服務器報告您需要一個名為consumer key的東西。在我們繼續使用 Alamofire 之前,我們需要從 500px 網站的 API 中獲取一個密鑰。
獲取消費者密鑰
前往https://500px.com/signup,然後使用您的郵箱免費注冊,或者使用您的 Facebook 、Twitter 或者 Google 帳號登錄。
一旦您完成了注冊流程,那麼前往https://500px.com/settings/applications並單擊"Register your application"。
您會看到如下所示的對話框:
紅色大箭頭指向的那些文本框裡面的內容都是必填的。使用Alamofire Tutorial作為Application Name,然後使用iOS App作為Description。目前您的應用還沒有Application URL,但是您可以隨意輸一個有效的網址來完成應用注冊,您可以使用raywenderlich.com^_^。
最後,在Developer’s Email中輸入您的郵箱地址,然後單擊復選框來接受使用協議。
接著,單擊 Register按鈕,您會看到一個如下所示的框:
單擊See application details鏈接,然後它會彈出詳細信息,這時候您就可以定義您的消費者密鑰了,如下所示:
從該頁面中復制出您的消費者密鑰,然後返回 Xcode,然後用如下代碼替換掉目前為止您唯一添加的代碼:
Alamofire.request(.GET, "https://api.500px.com/v1/photos", parameters: ["consumer_key": "PASTE_YOUR_CONSUMER_KEY_HERE"]).responseJSON() { (_, _, JSON, _) in println(JSON) }
請確保您已經用復制的消費者密鑰來替換掉PASTE_YOUR_CONSUMER_KEY_HERE。
生成並運行您的應用,這時您會在控制台中看見海量的輸出:
上述所有的輸出意味著您成功地下載到了含有一些照片信息的 JSON。
JSON 數據中包含了一些圖片集的屬性、一些頁面信息,以及一個圖片數組。這裡是我得到的搜索結果(您的可能會略有不同):
{ "feature": "popular", "filters": { "category": false, "exclude": false }, "current_page": 1, "total_pages": 250, "total_items": 5000, "photos": [ { "id": 4910421, "name": "Orange or lemon", "description": "", . . . } }, { "id": 4905955, "name": "R E S I G N E D", "description": "From the past of Tagus River, we have History and memories, some of them abandoned and disclaimed in their margins ...", . . . } ] }
現在我們已經擁有了 JSON 數據,接下來我們就可以好好地利用它了。
使用如下代碼替換掉viewDidLoad()中的 println(JSON)方法:
let photoInfos = (JSON!.valueForKey("photos") as [NSDictionary]).filter({ ($0["nsfw"] as Bool) == false }).map { PhotoInfo(id: $0["id"] as Int, url: $0["image_url"] as String) } self.photos.addObjectsFromArray(photoInfos) self.collectionView.reloadData()
上述代碼將 JSON 數據轉變為了更易於管理的`PhotoInfo`數組對象。這些對象只是簡化掉了圖片 ID 和 URL 屬性的存儲桶(bucket)。您同樣可以發現代碼過濾掉了一些……呃……您不希望出現的一些圖片。
上述代碼同樣也重新加載了集合視圖。初始項目的示例代碼基於我們剛剛填充的`photos`,來創建集合視圖的單元。
生成並運行您的應用,這時加載控件加載一會兒便消失。如果您仔細觀察的話,您會發現一堆灰黑色方形單元:
離我們的目標越來越接近了,加油!
我們仍然定位到PhotoBrowserCollectionViewController.swift,在`collectionView(_: cellForItemAtIndexPath:)`方法中的return cell前加上如下的代碼:
let imageURL = (photos.objectAtIndex(indexPath.row) as PhotoInfo).url Alamofire.request(.GET, imageURL).response() { (_, _, data, _) in let image = UIImage(data: data! as NSData) cell.imageView.image = image }
上述的代碼為`photos`數組中的圖片創建了另外的 Alamofire 請求。由於這是一個圖片請求,因此我們使用的是一個簡單的request方法,其在NSData的blob 中返回響應。接下來我們直接把數據放入到一個UIImage的實例中,然後反過來將實例放入早已存在於示例項目中的imageView 當中。
再一次生成並運行您的應用,這時應當出現一個圖片集合,與下圖相似:
對於 Alamofire 的工作效果想必您現在已經心中有數,但是您不會想在每次從服務器請求數據的時候,要不停的復制、粘貼 API 地址,以及添加消費者密鑰。除了這一點非常讓人不爽外,如果 API 地址發生了改變,那麼您可能不得不再次創建一個新的消費者密鑰。
幸運的是,Alamofire對於這個問題有著良好的解決方案。
創建請求路由
打開Five100px.swift,然後找到struct Five100px,其中定義了enum ImageSize。這是一個簡單的基於 500px.com 的 API 文件的結構體。
在使用 Alamofire 之前,您需要在文件頂部添加下述的必要聲明:
import Alamofire
現在,在struct Five100px中的enum ImageSize上方添加下述代碼:
enum Router: URLRequestConvertible { static let baseURLString = "https://api.500px.com/v1" static let consumerKey = "PASTE_YOUR_CONSUMER_KEY_HERE" case PopularPhotos(Int) case PhotoInfo(Int, ImageSize) case Comments(Int, Int) var URLRequest: NSURLRequest { let (path: String, parameters: [String: AnyObject]) = { switch self { case .PopularPhotos (let page): let params = ["consumer_key": Router.consumerKey, "page": "\(page)", "feature": "popular", "rpp": "50", "include_store": "store_download", "include_states": "votes"] return ("/photos", params) case .PhotoInfo(let photoID, let imageSize): var params = ["consumer_key": Router.consumerKey, "image_size": "\(imageSize.rawValue)"] return ("/photos/\(photoID)", params) case .Comments(let photoID, let commentsPage): var params = ["consumer_key": Router.consumerKey, "comments": "1", "comments_page": "\(commentsPage)"] return ("/photos/\(photoID)/comments", params) } }() let URL = NSURL(string: Router.baseURLString) let URLRequest = NSURLRequest(URL: URL!.URLByAppendingPathComponent(path)) let encoding = Alamofire.ParameterEncoding.URL return encoding.encode(URLRequest, parameters: parameters).0 } }
這就是我們所創建的路由,它為我們的 API 調用方法創建合適的URLString實例。它是一個簡單的遵守URLRequestConertible協議的enum類型,這個協議是在 Alamofire 當中定義的。當有枚舉類型采用該協議的時候,該類型就必須含有一個名為URLRequest的NSURLRequest類型變量。
這個路由含有兩個靜態常量:API 的baseURLString以及consumerKey。(最後一次聲明,請`PASTE_YOUR_CONSUMER_KEY_HERE`替換為您自己的消費者密鑰)現在,這個路由可以在必要的時候向最終的`URLString`中添加消費者密鑰。
您的應用擁有三個 API 終結點(endpoint):一個用來取出熱門照片列表,一個用來取出某個特定照片的具體信息,一個用來取出某個照片的評論。路由將會借助三個相應的`case`聲明來處理這三個終結點,每個終結點都會接收一到兩個參數。
我們已經定義了`var URLRequest: NSURLRequest`作為計算(computed)屬性。這意味著每次我們使用`enum`的時候,它都會構造出基於特定`case`和其參數的最終 URL。
這裡有一個示例代碼片段,說明了上述的邏輯關系:
Five100px.Router.PhotoInfo(10000, Five100px.ImageSize.Large) // URL: https://api.500px.com/v1/photos/10000?consumer_key=xxxxxx&image_size=4 // https://api.500px.com/v1 + /photos/10000 + ?consumer_key=xxxxxx&image_size=4 // = baseURLString + path + encoded parameters
在上面的示例中,代碼路由通過照片信息 API 的終結點來尋找一個 ID 為10000的大尺寸圖片。注釋行將 URL 的結構進行了拆分。在這個示例中,URL 由三個部分組成:`baseURLString`、`path`(?前面的那一部分)以及`[String: AnyObject]`字典,其中包含有傳遞給 API 終結點的參數。
對於`path`來說,返回元組的第一個元素可以用以下的字符串形式返回:
"/photos/\(photoID)" // "/photos/10000"
和響應解析類似,請求參數可以被編碼為 JSON、屬性列表或者是字符串。通常情況下使用簡單的字符串參數,和上面我們所做的類似。
如果您打算在您自己的項目中使用路由,您必須對它的運行機制相當熟悉。為此,請嘗試搞清楚要如何構造出以下的 URL:
https://api.foursquare.com/v2/users/{USER_ID}/lists?v=20131016&group=created
您是怎麼做的呢?如果您不是百分百確定答案,請花一點時間來分析下面的代碼,直到您完全搞明白其工作原理:
> 解決方案:
static let baseURLString = "https://api.foursquare.com/v2" case UserLists(Int) var URLRequest: NSURLRequest { let (path: String, parameters: [String: AnyObject]) = { switch self { case . UserLists (let userID): let params = ["v": "20131016", "group": "created"] return ("/users/\(userID)/lists", params) } }() . . .
這裡您需要為枚舉添加其他的 case,比如說用戶列表,它們都設置有合適的參數和路徑。
加載更多圖片
好的,現在應用目前顯示的照片只有一個頁面,但是我們想要浏覽更多照片以找到我們心儀的內容。多多益善對吧?
打開PhotoBrowserCollectionViewController.swift,然後在 let refreshControl = UIRefreshControl()語句下方添加如下代碼:
var populatingPhotos = false var currentPage = 1
這裡我們定義了兩個變量,來記錄當前是否在更新照片,以及當前我們正在浏覽的是哪一個照片頁面。
接下來,用以下代碼替換當前`viewDidLoad()`的聲明:
override func viewDidLoad() { super.viewDidLoad() setupView() populatePhotos() }
這裡我們用populatePhotos()函數來替換了先前的 Alamofire 請求。之後我們就要實現populatePhotos()函數的聲明。
同樣的,在`handleRefresh()`上方添加兩個函數,如下所述:
啊……好長一段代碼,對吧?下面是對每個注釋部分的詳細解釋:
1. 一旦您滾動超過了 80% 的頁面,那麼scrollViewDidScroll()方法將會加載更多的圖片。
2.populatePhotos()方法在currentPage當中加載圖片,並且使用populatingPhotos作為標記,以防止還在加載當前界面時加載下一個頁面。
3. 這裡我們首次使用了我們創建的路由。只需將頁數傳遞進去,它將為該頁面構造 URL 字符串。500px.com 網站在每次 API 調用後返回大約50張圖片,因此您需要為下一批照片的顯示再次調用路由。
4. 要注意,.responseJSON()後面的代碼塊:completion handler(完成處理方法)必須在主線程運行。如果您正在執行其他的長期運行操作,比如說調用 API,那麼您必須使用 GCD 來將您的代碼調度到另一個隊列運行。在本示例中,我們使用`DISPATCH_QUEUE_PRIORITY_HIGH`來運行這個操作。
5. 您可能會關心 JSON 數據中的photos關鍵字,其位於數組中的字典中。每個字典都包含有一張圖片的信息。
6. 我們使用 Swift 的filter函數來過濾掉 NSFW 圖片(Not Safe For Work)
7. map函數接收了一個閉包,然後返回一個PhotoInfo對象的數組。這個類是在Five100px.swift當中定義的。如果您查看這個類的源碼,那麼就可以看到它重寫了isEqual和hash這兩個方法。這兩個方法都是用一個整型的id屬性,因此排序和唯一化(uniquing)PhotoInfo對象仍會是一個比較快的操作
8. 接下來我們會在添加新的數據前存儲圖片的當前數量,使用它來更新collectionView.
9. 如果有人在我們滾動前向 500px.com 網站上傳了新的圖片,那麼您所獲得的新的一批照片將可能會包含一部分已下載的圖片。這就是為什麼我們定義var photos = NSMutableOrderedSet()為一個組。由於組內的項目必須唯一,因此重復的圖片不會再次出現
10. 這裡我們創建了一個NSIndexPath對象的數組,並將其插入到collectionView.
11. 在集合視圖中插入項目,請在主隊列中完成該操作,因為所有的 UIKit 操作都必須運行在主隊列中
生成並運行您的應用,然後緩慢向下滑動圖片。您可以看到新的圖片將持續加載:
不斷加快滑動的速度,注意到問題沒有?對的,滾動操作不是很穩定,有些許遲鈍的感覺。這並不是我們想要提供給用戶的體驗,但是我們在下一節中就可以修正這個問題了。
創建自定義響應序列化方法(Serializer)
您已經看到,我們在 Alamofire 中使用所提供的 JSON、字符串,以及屬性列表序列化方法是一件非常簡單的事情。但是有些時候,您可能會想要創建自己的自定義相應序列化。例如,您可以寫一個響應序列化方法來直接接收UIIMage,而不是將UIImage轉化為NSData來接收。
在本節中,您將學習如何創建自定義響應序列化方法。
打開Five100px.swift,然後在靠近文件頂部的地方,也就是import Alamofire語句下面添加如下代碼:
要創建一個新的響應序列化方法,我們首先應當需要一個類方法,其返回Serializer閉包(比如說上面所寫的imageResponseSerializer())。這個閉包在 Alamofire 中被類型命名,其接收三個參數並返回所示的兩個參數,如下所示:
public typealias Serializer = (NSURLRequest, NSHTTPURLResponse?, NSData?) -> (AnyObject?, NSError?)
類方法(例如imageResponseSerializer())接收底層的NSURLSession請求以及和響應對象一起的基本NSData數據實現方法(從服務器傳來的),來作為參數。該方法接下來使用這些對象來序列化,並將其輸入到一個有意義的數據結構中,然後將其從方法中返回,它同樣也會返回在這個過程中發生的錯誤。在我們的示例中,我們使用UIImage來將數據轉化為圖片對象。
通常情況下,當您創建了一個響應序列化方法後,您可能還會向創建一個新的響應處理方法來對其進行處理,並讓其更加易用。我們使用.responseImage()方法來完成這項任務。這個方法的操作很簡單:它使用completionHandler,一個以閉包形式的代碼塊。一旦我們從服務器中序列化了數據,那麼這個代碼塊將會運行。我們所需要做的就是在響應處理方法中調用 Alamofire 自己的通用.response()響應處理方法。
讓我們開始讓它工作起來。打開PhotoBrowserCollectionViewController.swift,然後在PhotoBrowserCollectionViewCell中的imageView屬性下面,添加如下一個屬性:
var request: Alamofire.Request?
這個屬性會為這個單元存儲 Alamofire 的請求來加載圖片
現在將collectionView(_: cellForItemAtIndexPath:)的內容替換為下面所示的代碼:
生成並運行您的應用,再次滾動圖片,您會發現滾動變得流暢了。
為什麼會流暢呢?
那麼我們到底做了些什麼來讓滾動變得流暢了呢?其關鍵就是collectionView(_: cellForItemAtIndexPath:)中的代碼。但是在我們解釋這段代碼之前,我們需要向您解釋網絡調用的異步性。
Alamofire 的網絡調用是異步請求方式。這意味著提交網絡請求不會阻止剩余代碼的執行。網絡請求可能會執行很長時間才能得到返回結果,但是您不會希望在等待圖片下載的時候 UI 被凍結。
也就是說,實現異步請求是一個極大的挑戰。如果在發出請求之後到從服務器接收到響應的這段時間中,UI 發生了改變的話怎麼辦?
例如,UICollectionView擁有內部的單元出列機制。創建新的單元對系統來說開銷很大,因此集合視圖將重用不在屏幕上顯示的現有單元,而不是不停創建新的單元。
這意味著同一個單元對象,會被不停地重復使用。因此在發出 Alamofire 請求之後到接收到圖片信息響應之前,用戶將單元滾動出屏幕並且刪除圖片的操作將成為可能。單元可能會出列,並且准備顯示另一個圖片。
在上述的代碼中,我們完成了兩件事來解決這個問題。第一件事是,當一個單元出列後,我們通過設值為nil的方法來清除圖片。這個操作確保我們不會顯示原先的圖片;第二件事是,我們的請求完成處理方法將檢查單元的 URL 是否和請求的 URL 相等。如果不相等的話,顯然單元已經擁有了另外的圖片,那麼完成處理方法將不會浪費其生命周期來為單元設置錯誤的圖片。
接下來該何去何從?
您可以在這裡下載本教程第一部分的最終版本項目。
> 提示:如果您打算直接使用上面的的最終版本,那麼千萬不要忘記在前面的教程中所說的,用您的消費者密鑰酌情替換Five100px.swift中的響應內容。
現在,多虧了 Alamofire,您的應用擁有了基本的照片浏覽功能。現在您已經學會了如何使用 Alamofire 發送 GET 請求、傳遞參數、創建請求路由,甚至學會了創建您自己的響應序列化方法。