Alamofire网络库基础教程
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 文件:
我们应用的主屏幕使用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 文件,然后在文件顶部添加如下代码:
|
|
您需要在每个使用了 Alamofire 类及其方法的文件中添加这条 import
语句。
接下来,在 viewDidLoad()
方法中的 setupView()
下方添加如下代码:
|
|
过会儿我会对其做出详细解释,但是首先您需要生成并运行该应用,这个时候您会在控制台中看到如下信息:
|
|
提示
在 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,然后用如下代码替换掉目前为止您唯一添加的代码:
|
|
请确保您已经用复制的消费者密钥来替换掉消费者密钥
。
生成并运行您的应用,这时您会在控制台中看见海量
的输出:
上述所有的输出意味着您成功地下载到了含有一些照片信息的 JSON 信息。
JSON 数据中包含了一些图片集的属性、一些页面信息,以及一个图片数组。这里是我得到的搜索结果(您的可能会略有不同):
|
|
现在我们已经拥有了 JSON 数据,接下来我们就可以好好地利用它了。
使用如下代码替换掉 viewDidLoad()
中的 print("JSON: \(JSON)")
方法:
|
|
上述代码将 JSON 数据转变为了更易于管理的PhotoInfo
对象集合。这些对象只是简单存储了图片 ID 和 URL 属性的存储桶 (bucket)。您同样可以发现代码过滤掉了一些……呃……您不希望出现的一些图片。
上述代码同样也重新加载了集合视图。初始项目的示例代码基于我们刚刚填充的 photos
,来创建集合视图的单元。
生成并运行您的应用,这时加载控件加载一会儿便消失。如果您仔细观察的话,您会发现一堆灰黑色的方形单元格:
离我们的目标越来越接近了,加油!
我们仍然选择 PhotoBrowserCollectionViewController.swift 文件,在collectionView(_: cellForItemAt:)
方法中的 return cell
前加上如下的代码:
|
|
上述的代码为 photos
集合中的对象创建了另外的 Alamofire 请求。由于这是一个图片请求,因此我们使用的是一个简单的 request
方法,其在闭包中返回 DefaultDataResponse
响应。接下来我们直接把其中的数据放入到一个 UIImage
的实例中,然后反过来将实例放入早已存在于示例项目中的 imageView
当中。
再一次生成并运行您的应用,这时应当出现一个图片集合,与下图相似:
对于 Alamofire 的工作效果想必您现在已经心中有数,但是您不会想在每次从服务器请求数据的时候,要不停的复制、粘贴 API 地址,以及添加消费者密钥。不仅这一点非常让人不爽,而且如果 API 地址发生了改变,那么您可能不得不再次创建一个新的消费者密钥。
幸运的是,Alamofire 对于这个问题有着良好的解决方案。
##创建请求路由
打开 Five100px.swift,然后找到 struct Five100px
,其中定义了 enum ImageSize
。这是一个简单的基于 500px.com API 文件的结构体。
在使用 Alamofire 之前,您需要在文件顶部添加下述的必要声明:
|
|
现在,在struct Five100px
中的enum ImageSize
代码段上方添加下述代码:
|
|
这就是我们所创建的路由,它为我们的 API 调用方法创建合适的 URLRequest
实例。它是一个简单的遵守 URLRequestConertible
协议的 enum
类型,这个协议是在 Alamofire 当中定义的。当有枚举类型采用该协议的时候,该类型就必须实现一个名为 asURLRequest() -> URLRequest
的方法。
这个路由含有两个静态常量:API 的 baseURLString
以及 consumerKey
。(最后一次声明,请将消费者密钥
替换为您自己的消费者密钥)现在,这个路由可以在必要的时候向最终的 URLRequest
中添加消费者密钥。
您的应用拥有三个 API 终端(endpoint):一个用来取出热门照片列表,一个用来取出某个特定照片的具体信息,一个用来取出某个照片的评论。路由将会借助三个相应的case
声明来处理这三个终端,每个终端都会接收一到两个参数。
我们已经定义了 func asURLRequest() throws -> URLRequest
这个会抛出异常的方法。这意味着每次我们使用enum
的时候,它都会构造出基于特定的 case
和其参数的最终 URLRequest。
这里有一个示例代码片段,说明了上述的逻辑关系:
|
|
在上面的示例中,代码路由通过照片信息 API 的终结点来寻找一个 ID 为10000的大尺寸图片。注释行将 URL 的结构进行了拆分。在这个示例中,URL 由三个部分组成:baseURLString
、path
(?前面的那一部分)以及Alamofire.Paramters
字典,其中包含有传递给 API 终结点的参数。
对于 path
来说,返回元组的第一个元素可以用以下的字符串形式返回:
|
|
和响应解析类似,请求参数可以被编码为 JSON、属性列表或者是字符串。通常情况下使用简单的字符串参数,和上面我们所做的类似。
如果您打算在您自己的项目中使用路由,您必须对它的运行机制相当熟悉。为此,请尝试搞清楚要如何构造出以下的 URL:
https://api.foursquare.com/v2/users/{USER_ID}/lists?v=20131016&group=created
您是怎么做的呢?如果您不是百分百确定答案,请花一点时间来分析下面的代码,直到您完全搞明白其工作原理:
解决方案:
12345678910111213141516 > 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()
语句下方添加如下代码:
|
|
这里我们定义了两个变量,来记录当前是否在更新照片,以及当前我们正在浏览的是哪一个照片页面。
接下来,用以下代码替换当前viewDidLoad()
的声明:
|
|
这里我们用 populatePhotos()
函数来替换了先前的 Alamofire 请求。之后我们就要实现 populatePhotos()
函数的声明。
这次,我们在 handleRefresh()
上方添加两个函数,如下所述:
|
|
啊……好长一段代码,对吧?下面是对每个注释部分的详细解释:
- 一旦您滚动超过了 80% 的页面,那么
scrollViewDidScroll()
方法将会加载更多的图片。 populatePhotos()
方法在currentPage
当中加载图片,并且使用populatingPhotos
作为标记,以防止还在加载当前界面时加载下一个页面。- 这里我们首次使用了我们创建的路由。只需将页数传递进去,它将为该页面构造 URL 字符串。500px.com 网站在每次 API 调用后返回大约50张图片,因此您需要为下一批照片的显示再次调用路由。
- 要注意,
.responseJSON()
后面的代码块:completion handler(完成处理方法)必须在主线程运行。如果您正在执行其他的长期运行操作,比如说调用 API,那么您必须使用 GCD 来将您的代码调度到另一个队列运行。在本示例中,我们使用QoSClass.userInitiated
来运行这个操作,这就是以前的DISPATCH_QUEUE_PRIORITY_HIGH
。 - 您可能会关心 JSON 数据中的
photos
关键字,其位于数组中的字典中。每个字典都包含有一张图片的信息。 - 接下来我们会在添加新的数据前存储图片的当前数量,使用它来更新
collectionView
forEach
函数将遍历获取到的 photoJsons 字典数组,筛除掉nsfw
(Not Safe For Work) 图片,然后将PhotoInfo
对象插入到photos
集合当中。这个结构体是在 Five100px.swift 当中定义的。如果您查看这个结构体的源码,那么就可以看到它实现了Hashable
和Equatable
两个协议,因此排序和唯一化(uniquing)PhotoInfo
对象仍会是一个比较快的操作。- 如果有人在我们滚动前向 500px.com 网站上传了新的图片,那么您所获得的新的一批照片将可能会包含一部分已下载的图片。这就是为什么我们定义
var photos = Set<PhotoInfo>()
为一个集合。由于集合内的项目必须唯一,因此重复的图片不会再次出现。 - 这里我们创建了一个
IndexPath
对象的数组,并将其插入到collectionView
当中。 - 在集合视图中插入项目,请在主队列中完成该操作,因为所有的 UIKit 操作都必须运行在主队列中。
生成并运行您的应用,然后缓慢向下滑动图片。您可以看到新的图片将持续加载:
不断加快滑动的速度,注意到问题没有?对的,滚动操作不是很稳定,有些许迟钝的感觉。这并不是我们想要提供给用户的体验,但是我们在下一节中就可以修正这个问题了。
##创建自定义响应序列化方法(Serializer)
您已经看到,我们在 Alamofire 中使用所提供的 JSON、字符串,以及属性列表序列化方法是一件非常简单的事情。但是有些时候,您可能会想要创建自己的自定义相应序列化。例如,您可以写一个响应序列化方法来直接接收 UIIMage
,而不是将 UIImage
转化为 NSData
来接收。
在本节中,您将学习如何创建自定义响应序列化方法。
打开 Five100px.swift,然后在靠近文件顶部的地方,也就是 import Alamofire
语句下面添加如下代码:
|
|
要创建一个新的响应序列化方法,我们首先应当需要一个静态方法,其返回 DataResponseSerializer
结构体(比如说上面所写的 imageResponseSerializer()
)。这个结构体是 Alamofire 中处理序列化的主体,结构如下所示:
|
|
这个静态方法(例如imageResponseSerializer()
)接收底层的NSURLSession
请求以及和响应对象一起传来的基础 NSData
数据处理代码块(从服务器传来的)来作为参数。该方法接下来使用这些对象来序列化,并将其输入到一个有意义的数据结构中,然后将其从方法中返回,它同样也会返回在这个过程中发生的错误。在我们的示例中,我们使用 UIImage
来将数据转化为图片对象。
通常情况下,当您创建了一个响应序列化方法后,您可能还会向创建一个新的响应处理方法来对其进行处理,并让其更加易用。我们使用 .responseImage()
方法来完成这项任务。这个方法的操作很简单:它使用 completionHandler
,一个以闭包形式的代码块。一旦我们从服务器中序列化了数据,那么这个代码块将会运行。我们所需要做的就是在响应处理方法中调用 Alamofire 自己的通用 .response()
响应处理方法。
让我们开始让它工作起来。打开 PhotoBrowserCollectionViewController.swift,然后在 PhotoBrowserCollectionViewCell
中的 imageView
属性下面,添加如下一个属性:
|
|
这个属性会为这个单元存储 Alamofire 的请求来加载图片。
现在将 collectionView(_: cellForItemAt:)
的内容替换为下面所示的代码:
|
|
生成并运行您的应用,再次滚动图片,您会发现滚动变得流畅了。
##为什么会流畅呢?
那么我们到底做了些什么来让滚动变得流畅了呢?其关键就是 collectionView(_: cellForItemAt:)
中的代码。但是在我们解释这段代码之前,我们需要向您解释网络调用的异步性。
Alamofire 的网络调用是异步请求方式。这意味着提交网络请求不会阻止剩余代码的执行。网络请求可能会执行很长时间才能得到返回结果,但是您不会希望在等待图片下载的时候 UI 被冻结。
也就是说,实现异步请求是一个极大的挑战。如果在发出请求之后到从服务器接收到响应的这段时间中,UI 发生了改变的话怎么办?
例如,UICollectionView
拥有内部的单元出列机制。创建新的单元对系统来说开销很大,因此集合视图将重用不在屏幕上显示的现有单元,而不是不停创建新的单元。
这意味着同一个单元对象,会被不停地重复使用。因此在发出 Alamofire 请求之后到接收到图片信息响应之前,用户将单元滚动出屏幕并且删除图片的操作将成为可能。单元可能会出列,并且准备显示另一个图片。
在上述的代码中,当一个单元出列后,我们通过设值为nil
的方法来清除图片。这个操作确保我们不会显示原先的图片。
##接下来该何去何从?
您可以在这里下载本教程第一部分的最终版本项目。
提示:
如果您打算直接使用上面的的最终版本,那么千万不要忘记在前面的教程中所说的,用您的消费者密钥酌情替换Five100px.swift中的相应内容。
本教程介绍了相当多的内容,现在您可以好好的休息一下了!现在,多亏了 Alamofire,您的应用拥有了基本的照片浏览功能。
在我们的学习过程中,您已经学会了如何使用 Alamofire 发送 GET 请求、传递参数、创建请求路由,甚至学会了创建您自己的响应序列化方法。
在本教程的第二部分,您将会增加以下功能:
- 照片查看器
- 查看评论以及其他信息的能力
- 下载照片的选项,附带有一个圆列进度条
- 下拉刷新操作
我们希望您能够喜欢我们这部分的教程,并且能够加入我们的第二部分的教程。如果您对 Alamofire 有任何看法或建议,快来加入我们的讨论吧!