Alamofire网络库基础教程

Author Avatar
星夜暮晨 12月 01, 2014

学习如何使用 Alamofire 来轻松地实现 Swift 的网络请求处理

AFNetworking 是 iOS 和 macOS 上最受欢迎的第三方库之一。它曾在我们的2012年的读者评选中荣获2012年度最佳 iOS 库称号。它同样也在 Github 上面获得了27000 多个 stars 和 8000 多个 forks,是使用最广的开源项目之一。

自2014年 Swift 推出之后,AFNetworking 的作者 Mattt Thompson 便提交了一个新的类似于 AFNetworking 的网络基础框架,并且是专门使用最新的 Swift 语言来编写的,其名为:Alamofire

AFNetwork 的前缀 AF 是 Alamofire 的缩写,因为这个新的框架名称是根据 Swift 的约定来进行命名的。

在本教程的第一部分中,我们将带领大家使用 Alamofire 来制作一个网络图片库,图片的来源是 500px.com。在这个过程中,您可以学习到如何使用 Alamofire 中的重要组件,以及了解关于在应用中处理网络请求的某些重要的知识点。

本教程的第二部分将基于第一部分所制作的应用,并为其增加一些好用的功能。您可以通过这个过程学习到更多高级的 Alamofire 用法。

本教程假定您已熟悉 Swift 语言以及 iOS 开发。如果不是的话,请参阅我们的其他教程。还有,本教程使用 Xcode 8.0 作为开发环境。

提示:

如果您已经熟悉了 Alamofire 的基本用法,那么您可以直接跳到本文的第二部分。但是请确保您已拥有消费者密钥 (cunsumer key),然后参照下文在应用中替换它。

##让我们开始吧

首先下载本次教程的初始项目。这个项目中提供了在本教程中需要的全部 UI。这有助于您能够将注意力集中到学习 Alamofire 的使用上来,而不是花费大量时间来研究 UI。

在 Xcode 中打开这个项目,并定位到 Main.storyboard 文件:

Main.storyboard

我们应用的主屏幕使用UITabBarController这个常用的 UI 样式。我们的标签控制器中包含有两个标签,每个标签都有它们自己的UINavigationController页面。第一个标签让用户浏览热门图片。第二个标签让用户浏览他们已保存的文件。两个标签都使用UICollectionViewController来向用户展示图片。故事板中同样也包含了两个独立的视图控制器,在接下来的教程中我们将会用到它们。

生成并运行该应用,您首先会看到一个不停在加载的加载控件:

初始项目的运行效果

这看起来一点也不高端大气上档次……可以说没什么可看的。但是很快我们将会借助 Alamofire 来让他逼格高起来。

提示:

如果您很熟悉 AFNetworking 的使用,那么您可能会期待下一节我们谈论 CocoaPods 相关用法。借助 CocoaPods 0.36 及以上版本,您便可以在 iOS 8.0 以上环境中使用 Swift 版本的内置框架了。

在本教程当中,我们将会使用另外一个不需要 CocoaPods 的方法来导入 Alamofire。当然,如果您对 CocoaPods 很熟悉的话,那么也可以尽情地使用它来导入,这两者基本没有任何区别。

要获取最新版本的 Alamofire,请前往 https://github.com/Alamofire/Alamofire 然后单击网页右边的 Clone or download 按钮,在弹出的菜单中选择 Download ZIP。接着在 Finder 中打开起始项目文件夹,然后将 Alamofire-master 文件夹拖入到您的主项目文件夹中。

提示:

目前 Alamofire 的最新版本是 4.0.1,这意味着它只支持 iOS 9.0 及其以上版本,如果您需要支持 iOS 8 甚至更低版本的话,请选用 3.0 甚至 2.0 版本的 Alamofire。

打开 Alamofire-master 文件夹(现在它位于您的项目文件夹中),然后将 Alamofire.xcodeprij 文件(注意是蓝色图标!不是白色图标!)直接拖进 Xcode 中的 Photomania 项目下面,如下图所示:

接下来,单击 Photomania 项目,进入 General 选项卡。向下滚动到 Embedded Binaries 项,然后单击其下方的 + 号。选择 Alamofire.framework (iOS),最后点击 Add 完成添加。

生成并运行您的项目以确保没有任何错误出现,然后就可以进入到下一节内容了。

##使用 Alamofire 来检索数据

您可能会想问,我们为什么要使用 Alamofire 呢?明明苹果已经提供了 NSURLSession 类以及相关类,以便让我们通过 HTTP(s) 来下载相应内容。为什么我们还要伤精费神地去使用第三方库呢?

简单来说,Alamofire 其实是基于 NSURLSession 的,但是它可以免去您写样板 (boilerplate) 代码的麻烦,并且可以让网络模块的代码更为简单易用。您可以通过一些非常简单的操作来访问 Internet 上的数据,并且写出来的代码也会更加清晰明了、简单易读。

要使用 Alamofire 的话,首先需要导入它。请打开 PhotoBrowserCollectionViewController.swift 文件,然后在文件顶部添加如下代码:

1
import Alamofire

您需要在每个使用了 Alamofire 类及其方法的文件中添加这条 import 语句。

接下来,在 viewDidLoad() 方法中的 setupView() 下方添加如下代码:

1
2
3
4
5
Alamofire.request("https://api.500px.com/v1/photos", method: .get).responseJSON {
response in
guard let JSON = response.result.value else { return }
print("JSON: \(JSON)")
}

过会儿我会对其做出详细解释,但是首先您需要生成并运行该应用,这个时候您会在控制台中看到如下信息:

1
2
3
4
JSON: {
error = "Consumer key missing.";
status = 401;
}

提示

在 Xcode 8 当中,iOS 模拟器会输出大量的系统调试数据,为了停用这个功能,您可以打开对应 Target 的 Scheme,定位到 Run(Debug) 一栏的 Arguments 当中,在 Environment Variables 中加入 OS_ACTIVITY_MODE = DISABLE 这一条数据,即可屏蔽模拟器输出系统调试数据,如下图所示。

如果您是使用我们的示例项目的话,那么就不必进行这步操作了,我们已经帮您做好了。

您可能不明白它说了什么鬼,不过实际上您已经成功地使用 Alamofire 来实现网络请求了!您向 Internet 上的资源发出了一个请求,然后返回了一个 JSON 数据。

下面来解释一下那些代码到底做了些什么:

  • 通常情况下,您只需将请求对象链接到响应方法上。例如,在上面的代码中,请求对象简单地调用了 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
5
Alamofire.request("https://api.500px.com/v1/photos", method: .get, parameters: ["consumer_key": "请输入您的消费者密钥"]).responseJSON {
response in
guard let JSON = response.result.value else { return }
print("JSON: \(JSON)")
}

请确保您已经用复制的消费者密钥来替换掉消费者密钥

生成并运行您的应用,这时您会在控制台中看见海量的输出:

上述所有的输出意味着您成功地下载到了含有一些照片信息的 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() 中的 print("JSON: \(JSON)") 方法:

1
2
3
4
5
6
7
8
9
10
11
guard let photoJsons = (JSON as AnyObject).value(forKey: "photos") as? [[String: Any]] else { return }
photoJsons.forEach {
guard let nsfw = $0["nsfw"] as? Bool,
let id = $0["id"] as? Int,
let url = $0["image_url"] as? String,
nsfw == false else { return }
self.photos.insert(PhotoInfo(id: id, url: url))
}
self.collectionView?.reloadData()

上述代码将 JSON 数据转变为了更易于管理的PhotoInfo对象集合。这些对象只是简单存储了图片 ID 和 URL 属性的存储桶 (bucket)。您同样可以发现代码过滤掉了一些……呃……您不希望出现的一些图片。

上述代码同样也重新加载了集合视图。初始项目的示例代码基于我们刚刚填充的 photos,来创建集合视图的单元。

生成并运行您的应用,这时加载控件加载一会儿便消失。如果您仔细观察的话,您会发现一堆灰黑色的方形单元格:

离我们的目标越来越接近了,加油!

我们仍然选择 PhotoBrowserCollectionViewController.swift 文件,在collectionView(_: cellForItemAt:)方法中的 return cell 前加上如下的代码:

1
2
3
4
5
6
7
8
let photoInfo = photos[photos.index(photos.startIndex, offsetBy: indexPath.item)]
Alamofire.request(photoInfo.url, method: .get).response {
dataResponse in
guard let data = dataResponse.data else { return }
let image = UIImage(data: data)
cell.imageView.image = image
}

上述的代码为 photos 集合中的对象创建了另外的 Alamofire 请求。由于这是一个图片请求,因此我们使用的是一个简单的 request 方法,其在闭包中返回 DefaultDataResponse 响应。接下来我们直接把其中的数据放入到一个 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
enum Router: URLRequestConvertible {
static let baseURLString = "https://api.500px.com/v1"
static let consumerKey = "消费者密钥"
case popularPhotos(Int)
case photoInfo(Int, ImageSize)
case comments(Int, Int)
func asURLRequest() throws -> URLRequest {
let result: (path: String, parameters: Parameters) = {
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):
let params = ["consumer_key": Router.consumerKey, "image_size": "\(imageSize.rawValue)"]
return ("/photos/\(photoID)", params)
case .comments(let photoID, let commentsPage):
let params = ["consumer_key": Router.consumerKey, "comments": "1", "comments_page": "\(commentsPage)"]
return ("/photos/\(photoID)/comments", params)
}
}()
let url = try Router.baseURLString.asURL()
let urlRequest = URLRequest(url: url.appendingPathComponent(result.path))
return try URLEncoding.default.encode(urlRequest, with: result.parameters)
}
}

这就是我们所创建的路由,它为我们的 API 调用方法创建合适的 URLRequest 实例。它是一个简单的遵守 URLRequestConertible 协议的 enum 类型,这个协议是在 Alamofire 当中定义的。当有枚举类型采用该协议的时候,该类型就必须实现一个名为 asURLRequest() -> URLRequest的方法。

这个路由含有两个静态常量:API 的 baseURLString 以及 consumerKey。(最后一次声明,请将消费者密钥替换为您自己的消费者密钥)现在,这个路由可以在必要的时候向最终的 URLRequest 中添加消费者密钥。

您的应用拥有三个 API 终端(endpoint):一个用来取出热门照片列表,一个用来取出某个特定照片的具体信息,一个用来取出某个照片的评论。路由将会借助三个相应的case声明来处理这三个终端,每个终端都会接收一到两个参数。

我们已经定义了 func asURLRequest() throws -> URLRequest 这个会抛出异常的方法。这意味着每次我们使用enum的时候,它都会构造出基于特定的 case 和其参数的最终 URLRequest。

这里有一个示例代码片段,说明了上述的逻辑关系:

1
2
3
4
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 由三个部分组成:baseURLStringpath(?前面的那一部分)以及Alamofire.Paramters 字典,其中包含有传递给 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
16
> static let baseURLString = "https://api.foursquare.com/v2"
>
> case userLists(Int)
>
> func asURLRequest() throws -> URLRequest {
> let result: (path: String, parameters: Parameters) = {
> switch self {
> case .userlISTS(let userID):
> let params = ["v": "20131016", "group": "created"]
> return ("/users/\(userID)/lists", params)
> }
> }()
> .
> .
> .
>

>

这里您需要为枚举添加其他的 case,比如说用户列表,它们都设置有合适的参数和路径。

##加载更多图片

好的,现在应用目前显示的照片只有一个页面,但是我们想要浏览更多照片以找到我们心仪的内容。多多益善,对吧?

打开 PhotoBrowserCollectionViewController.swift,然后在 private let refreshControl = UIRefreshControl()语句下方添加如下代码:

1
2
private var populatingPhotos = false
private var currentPage = 1

这里我们定义了两个变量,来记录当前是否在更新照片,以及当前我们正在浏览的是哪一个照片页面。

接下来,用以下代码替换当前viewDidLoad()的声明:

1
2
3
4
5
6
7
override func viewDidLoad() {
super.viewDidLoad()
setupView()
populatePhotos()
}

这里我们用 populatePhotos() 函数来替换了先前的 Alamofire 请求。之后我们就要实现 populatePhotos() 函数的声明。

这次,我们在 handleRefresh() 上方添加两个函数,如下所述:

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
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
// 1
override func scrollViewDidScroll(_ scrollView: UIScrollView) {
if scrollView.contentOffset.y + view.frame.height > scrollView.contentSize.height * 0.8 {
populatePhotos()
}
}
private func populatePhotos() {
// 2
if populatingPhotos { return }
populatingPhotos = true
// 3
Alamofire.request(Five100px.Router.popularPhotos(currentPage)).responseJSON {
response in
guard let JSON = response.result.value, response.result.error == nil else {
self.populatingPhotos = false
return
}
// 4
DispatchQueue.global(qos: .userInitiated).async {
// 5
guard let photoJsons = (JSON as AnyObject).value(forKey: "photos") as? [[String: Any]] else { return }
// 6
let lastItemCount = self.photos.count
// 7
photoJsons.forEach {
guard let nsfw = $0["nsfw"] as? Bool,
let id = $0["id"] as? Int,
let url = $0["image_url"] as? String,
nsfw == false else { return }
// 8
self.photos.insert(PhotoInfo(id: id, url: url))
}
// 9
let indexPaths = (lastItemCount..<self.photos.count).map { IndexPath(item: $0, section: 0) }
// 10
DispatchQueue.main.async {
self.collectionView?.insertItems(at: indexPaths)
}
self.currentPage += 1
}
self.populatingPhotos = false
}
}

啊……好长一段代码,对吧?下面是对每个注释部分的详细解释:

  1. 一旦您滚动超过了 80% 的页面,那么 scrollViewDidScroll() 方法将会加载更多的图片。
  2. populatePhotos() 方法在 currentPage 当中加载图片,并且使用 populatingPhotos 作为标记,以防止还在加载当前界面时加载下一个页面。
  3. 这里我们首次使用了我们创建的路由。只需将页数传递进去,它将为该页面构造 URL 字符串。500px.com 网站在每次 API 调用后返回大约50张图片,因此您需要为下一批照片的显示再次调用路由。
  4. 要注意,.responseJSON() 后面的代码块:completion handler(完成处理方法)必须在主线程运行。如果您正在执行其他的长期运行操作,比如说调用 API,那么您必须使用 GCD 来将您的代码调度到另一个队列运行。在本示例中,我们使用QoSClass.userInitiated来运行这个操作,这就是以前的 DISPATCH_QUEUE_PRIORITY_HIGH
  5. 您可能会关心 JSON 数据中的 photos 关键字,其位于数组中的字典中。每个字典都包含有一张图片的信息。
  6. 接下来我们会在添加新的数据前存储图片的当前数量,使用它来更新 collectionView
  7. forEach 函数将遍历获取到的 photoJsons 字典数组,筛除掉 nsfw(Not Safe For Work) 图片,然后将 PhotoInfo 对象插入到 photos 集合当中。这个结构体是在 Five100px.swift 当中定义的。如果您查看这个结构体的源码,那么就可以看到它实现了 HashableEquatable 两个协议,因此排序和唯一化(uniquing)PhotoInfo 对象仍会是一个比较快的操作。
  8. 如果有人在我们滚动前向 500px.com 网站上传了新的图片,那么您所获得的新的一批照片将可能会包含一部分已下载的图片。这就是为什么我们定义 var photos = Set<PhotoInfo>() 为一个集合。由于集合内的项目必须唯一,因此重复的图片不会再次出现。
  9. 这里我们创建了一个 IndexPath 对象的数组,并将其插入到 collectionView 当中。
  10. 在集合视图中插入项目,请在主队列中完成该操作,因为所有的 UIKit 操作都必须运行在主队列中。

生成并运行您的应用,然后缓慢向下滑动图片。您可以看到新的图片将持续加载:

不断加快滑动的速度,注意到问题没有?对的,滚动操作不是很稳定,有些许迟钝的感觉。这并不是我们想要提供给用户的体验,但是我们在下一节中就可以修正这个问题了。

##创建自定义响应序列化方法(Serializer)

您已经看到,我们在 Alamofire 中使用所提供的 JSON、字符串,以及属性列表序列化方法是一件非常简单的事情。但是有些时候,您可能会想要创建自己的自定义相应序列化。例如,您可以写一个响应序列化方法来直接接收 UIIMage,而不是将 UIImage 转化为 NSData 来接收。

在本节中,您将学习如何创建自定义响应序列化方法。

打开 Five100px.swift,然后在靠近文件顶部的地方,也就是 import Alamofire 语句下面添加如下代码:

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 BackendError: Error {
case network(error: Error)
case dataSerialization(error: Error)
case imageSerialization(error: String)
}
extension DataRequest {
static func imageResponseSerializer() -> DataResponseSerializer<UIImage> {
return DataResponseSerializer { request, response, data, error in
guard error == nil else { return .failure(BackendError.network(error: error!)) }
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))
}
guard let image = UIImage(data: validData, scale: UIScreen.main.scale) else {
return .failure(BackendError.imageSerialization(error: "数据无法被序列化,因为接收到的数据为空"))
}
return .success(image)
}
}
@discardableResult
func responseImage(queue: DispatchQueue? = nil, completionHandler: @escaping (DataResponse<UIImage>) -> Void) -> Self {
return response(queue: queue, responseSerializer: DataRequest.imageResponseSerializer(), completionHandler: completionHandler)
}
}

要创建一个新的响应序列化方法,我们首先应当需要一个静态方法,其返回 DataResponseSerializer 结构体(比如说上面所写的 imageResponseSerializer())。这个结构体是 Alamofire 中处理序列化的主体,结构如下所示:

1
public struct DataResponseSerializer<Value>: DataResponseSerializerProtocol { ... }

这个静态方法(例如imageResponseSerializer())接收底层的NSURLSession请求以及和响应对象一起传来的基础 NSData 数据处理代码块(从服务器传来的)来作为参数。该方法接下来使用这些对象来序列化,并将其输入到一个有意义的数据结构中,然后将其从方法中返回,它同样也会返回在这个过程中发生的错误。在我们的示例中,我们使用 UIImage 来将数据转化为图片对象。

通常情况下,当您创建了一个响应序列化方法后,您可能还会向创建一个新的响应处理方法来对其进行处理,并让其更加易用。我们使用 .responseImage() 方法来完成这项任务。这个方法的操作很简单:它使用 completionHandler,一个以闭包形式的代码块。一旦我们从服务器中序列化了数据,那么这个代码块将会运行。我们所需要做的就是在响应处理方法中调用 Alamofire 自己的通用 .response() 响应处理方法。

让我们开始让它工作起来。打开 PhotoBrowserCollectionViewController.swift,然后在 PhotoBrowserCollectionViewCell 中的 imageView 属性下面,添加如下一个属性:

1
fileprivate var request: Request?

这个属性会为这个单元存储 Alamofire 的请求来加载图片。

现在将 collectionView(_: cellForItemAt:) 的内容替换为下面所示的代码:

1
2
3
4
5
6
7
8
9
10
11
12
13
guard let cell = collectionView.dequeueReusableCell(withReuseIdentifier: PhotoBrowserCellIdentifier, for: indexPath) as? PhotoBrowserCollectionViewCell else { return UICollectionViewCell() }
let imageURL = photos[photos.index(photos.startIndex, offsetBy: indexPath.item)].url
cell.imageView.image = nil
cell.request?.cancel()
cell.request = Alamofire.request(imageURL, method: .get).responseImage {
response in
guard let image = response.result.value, response.result.error == nil else { return }
cell.imageView.image = image
}
return cell

生成并运行您的应用,再次滚动图片,您会发现滚动变得流畅了。

##为什么会流畅呢?

那么我们到底做了些什么来让滚动变得流畅了呢?其关键就是 collectionView(_: cellForItemAt:) 中的代码。但是在我们解释这段代码之前,我们需要向您解释网络调用的异步性。

Alamofire 的网络调用是异步请求方式。这意味着提交网络请求不会阻止剩余代码的执行。网络请求可能会执行很长时间才能得到返回结果,但是您不会希望在等待图片下载的时候 UI 被冻结。

也就是说,实现异步请求是一个极大的挑战。如果在发出请求之后到从服务器接收到响应的这段时间中,UI 发生了改变的话怎么办?

例如,UICollectionView 拥有内部的单元出列机制。创建新的单元对系统来说开销很大,因此集合视图将重用不在屏幕上显示的现有单元,而不是不停创建新的单元。

这意味着同一个单元对象,会被不停地重复使用。因此在发出 Alamofire 请求之后到接收到图片信息响应之前,用户将单元滚动出屏幕并且删除图片的操作将成为可能。单元可能会出列,并且准备显示另一个图片。

在上述的代码中,当一个单元出列后,我们通过设值为nil的方法来清除图片。这个操作确保我们不会显示原先的图片。

##接下来该何去何从?

您可以在这里下载本教程第一部分的最终版本项目。

提示:

如果您打算直接使用上面的的最终版本,那么千万不要忘记在前面的教程中所说的,用您的消费者密钥酌情替换Five100px.swift中的相应内容。

本教程介绍了相当多的内容,现在您可以好好的休息一下了!现在,多亏了 Alamofire,您的应用拥有了基本的照片浏览功能。

在我们的学习过程中,您已经学会了如何使用 Alamofire 发送 GET 请求、传递参数、创建请求路由,甚至学会了创建您自己的响应序列化方法。

在本教程的第二部分,您将会增加以下功能:

  • 照片查看器
  • 查看评论以及其他信息的能力
  • 下载照片的选项,附带有一个圆列进度条
  • 下拉刷新操作

我们希望您能够喜欢我们这部分的教程,并且能够加入我们的第二部分的教程。如果您对 Alamofire 有任何看法或建议,快来加入我们的讨论吧!