最近,AFNetworking 的做者Mattt Thompson提交了一个新的相似于 AFNetworking 的网络 基础库,而且是专门使用最新的 Swift 语言来编写的,名为:Alamofiregit
AFNetwork 的前缀 AF 是 Alamofire 的缩写,所以这个新的库名称实际上是根据 Swift 的约定来进行命名的。github
在本教程的第一部分中,咱们将带领你们使用 Alamofire 来制做一个图片库应用,其来源是500px.com。在这个过程当中,您能够学习到如何使用 Alamofire 中的重要组件,以及了解在应用中处理网络请求的某些重要的知识点。swift
本教程的第二部分将基于第一部分所制做的应用,并为其增长一些好用的功能。您能够经过这个过程学习到更多高级的 Alamofire 用法。api
本教程假定您已熟悉 Swift 语言以及 iOS 开发。若是不是的话,请参阅咱们的其余教程。还有,本教程使用 Xcode 6.1 做为开发环境(而不是 Xcode 6.2 beta)。数组
> 提示:若是您已经熟悉了 Alamofire 的基本用法,那么您能够直接跳到本文的第二部分。可是请确保您已拥有消费者密钥(cunsumer key),而后参照下文在应用中替换它。xcode
让咱们开始吧服务器
首先下载本次教程的初始项目。这个项目中提供了在本教程中须要的所有 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文件,而后在文件顶部添加以下代码:
1
|
import Alamofire
|
您须要在每一个使用了 Alamofire 类以及函数的文件中添加这条`import`语句。
接下来,在`setupView()`下方的`viewDidLoad()`方法中添加以下代码:
1
2
3
4
|
(_, _, data, _)
in
println(data)
}
|
过会儿我会对其作出详细解释,可是首先您须要生成并运行该应用,这个时候您会在控制台中看到以下信息:
1
2
3
4
|
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,而后用以下代码替换掉目前为止您惟一添加的代码:
1
2
3
4
|
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 数据中包含了一些图片集的属性、一些页面信息,以及一个图片数组。这里是我获得的搜索结果(您的可能会略有不一样):
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
|
{
"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)方法:
1
2
3
4
5
6
7
8
9
|
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前加上以下的代码:
1
2
3
4
5
6
7
8
|
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 以前,您须要在文件顶部添加下述的必要声明:
1
|
import Alamofire
|
如今,在struct Five100px中的enum ImageSize上方添加下述代码:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
|
enum Router: URLRequestConvertible {
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。
这里有一个示例代码片断,说明了上述的逻辑关系:
1
2
3
4
|
Five100px.Router.PhotoInfo(10000, Five100px.ImageSize.Large)
// 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`来讲,返回元组的第一个元素能够用如下的字符串形式返回:
1
|
"/photos/\(photoID)"
// "/photos/10000"
|
和响应解析相似,请求参数能够被编码为 JSON、属性列表或者是字符串。一般状况下使用简单的字符串参数,和上面咱们所作的相似。
若是您打算在您本身的项目中使用路由,您必须对它的运行机制至关熟悉。为此,请尝试搞清楚要如何构造出如下的 URL:
https://api.foursquare.com/v2/users/{USER_ID}/lists?v=20131016&group=created
您是怎么作的呢?若是您不是百分百肯定答案,请花一点时间来分析下面的代码,直到您彻底搞明白其工做原理:
> 解决方案:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
|
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()语句下方添加以下代码:
1
2
|
var
populatingPhotos =
false
var
currentPage = 1
|
这里咱们定义了两个变量,来记录当前是否在更新照片,以及当前咱们正在浏览的是哪个照片页面。
接下来,用如下代码替换当前`viewDidLoad()`的声明:
1
2
3
4
5
6
7
|
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 中被类型命名,其接收三个参数并返回所示的两个参数,以下所示:
1
|
public typealias Serializer = (NSURLRequest, NSHTTPURLResponse?, NSData?) -> (AnyObject?, NSError?)
|
类 方法(例如imageResponseSerializer())接收底层的NSURLSession请求以及和响应对象一块儿的基本NSData数据实现 方法(从服务器传来的),来做为参数。该方法接下来使用这些对象来序列化,并将其输入到一个有意义的数据结构中,而后将其从方法中返回,它一样也会返回在 这个过程当中发生的错误。在咱们的示例中,咱们使用UIImage来将数据转化为图片对象。
通 常状况下,当您建立了一个响应序列化方法后,您可能还会向建立一个新的响应处理方法来对其进行处理,并让其更加易用。咱们使 用.responseImage()方法来完成这项任务。这个方法的操做很简单:它使用completionHandler,一个以闭包形式的代码块。一 旦咱们从服务器中序列化了数据,那么这个代码块将会运行。咱们所须要作的就是在响应处理方法中调用 Alamofire 本身的通用.response()响应处理方法。
让咱们开始让它工做起来。打开PhotoBrowserCollectionViewController.swift,而后在PhotoBrowserCollectionViewCell中的imageView属性下面,添加以下一个属性:
1
|
var
request: Alamofire.Request?
|
这个属性会为这个单元存储 Alamofire 的请求来加载图片
如今将collectionView(_: cellForItemAtIndexPath:)的内容替换为下面所示的代码:
生成并运行您的应用,再次滚动图片,您会发现滚动变得流畅了。
为何会流畅呢?
那么咱们到底作了些什么来让滚动变得流畅了呢?其关键就是collectionView(_: cellForItemAtIndexPath:)中的代码。可是在咱们解释这段代码以前,咱们须要向您解释网络调用的异步性。
Alamofire 的网络调用是异步请求方式。这意味着提交网络请求不会阻止剩余代码的执行。网络请求可能会执行很长时间才能获得返回结果,可是您不会但愿在等待图片下载的时候 UI 被冻结。
也就是说,实现异步请求是一个极大的挑战。若是在发出请求以后到从服务器接收到响应的这段时间中,UI 发生了改变的话怎么办?
例如,UICollectionView拥有内部的单元出列机制。建立新的单元对系统来讲开销很大,所以集合视图将重用不在屏幕上显示的现有单元,而不是不停建立新的单元。
这意味着同一个单元对象,会被不停地重复使用。所以在发出 Alamofire 请求以后到接收到图片信息响应以前,用户将单元滚动出屏幕而且删除图片的操做将成为可能。单元可能会出列,而且准备显示另外一个图片。
在 上述的代码中,咱们完成了两件事来解决这个问题。第一件事是,当一个单元出列后,咱们经过设值为nil的方法来清除图片。这个操做确保咱们不会显示原先的 图片;第二件事是,咱们的请求完成处理方法将检查单元的 URL 是否和请求的 URL 相等。若是不相等的话,显然单元已经拥有了另外的图片,那么完成处理方法将不会浪费其生命周期来为单元设置错误的图片。
接下来该何去何从?
您能够在这里下载本教程第一部分的最终版本项目。
> 提示:若是您打算直接使用上面的的最终版本,那么千万不要忘记在前面的教程中所说的,用您的消费者密钥酌情替换Five100px.swift中的响应内容。
如今,多亏了 Alamofire,您的应用拥有了基本的照片浏览功能。如今您已经学会了如何使用 Alamofire 发送 GET 请求、传递参数、建立请求路由,甚至学会了建立您本身的响应序列化方法。