欢迎回到我们的 Alamofire 网络库使用教程,本文是此教程的第二部分,同时也是最后一个部分。
在教程的第一部分中,我们学习了 Alamofire 的一些基本用法,比如说发送 GET 请求、传递参数、创建请求路由以及创建自定义响应序列化方法。
在学习的过程中,我们也生成了一个很赞的名为 Photomania 的图片库应用。
在本教程的第二部分,您将会增加以下功能:
- 照片查看器
- 查看评论以及其他信息的功能
- 下载照片功能,附带有一个圆列进度条
- 优化网络访问以及图片缓存
- 下拉刷新操作
##让我们开始吧
您可以使用您在教程第一部分所完成的项目来开始本章教程。但是如果您跳过了第一部分的教程或者对自己的项目没有信心的话,那么您也可以使用我们提供的标准项目。
提示:
如果您跳过了第一部分的教程,那么请不要忘记您首先应当从 500px.com 网站上获取消费者密钥,然后在 Five100px.swift 中用其替换必要的部分。关于如何获取该密钥,以及在何处使用它,都在本教程的第一部分:Alamofire 网络库基础教程中有详细说明。
生成并运行起始项目,以确定我们应用运行正常。图片预览功能能够正常工作,但是单击图片并不会将其以全屏打开。这就是我们所要解决的问题!
##创建图片查看器
说句老实话,范型可以说是包括 Swift 在内的高级编程语言中最强大的特性之一。一般情况下,在我们这个项目中最好使用范型这个功能。
打开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 31 32 33 34 35 36
| enum BackendError: Error { case network(error: Error) case dataSerialization(error: Error) case jsonSerialization(error: Error) case objectSerialization(error: String) case imageSerialization(error: String) } protocol ResponseObjectSerializable { init?(response: HTTPURLResponse, representation: Any) } extension DataRequest { @discardableResult func responseObject<T: ResponseObjectSerializable>(queue: DispatchQueue? = nil, completionHandler: @escaping (DataResponse<T>) -> Void) -> Self { let responseSerializer = DataResponseSerializer<T> { request, response, data, error in guard error == nil else { return .failure(BackendError.network(error: error!)) } let jsonResponseSerializer = DataRequest.jsonResponseSerializer(options: .allowFragments) let result = jsonResponseSerializer.serializeResponse(request, response, data, nil) guard case let .success(jsonObject) = result else { return .failure(BackendError.jsonSerialization(error: result.error!)) } guard let response = response, let responseObject = T(response: response, representation: jsonObject) else { let reason = "Response object could not be serialized due to nil response." return .failure(BackendError.objectSerialization(error: reason)) } return .success(responseObject) } return response(queue: queue, responseSerializer: responseSerializer, completionHandler: completionHandler) } }
|
在上述的代码中,我们再一次给 Alamofire 创建了一个扩展,添加了新的响应序列化方法。这次,我们添加了 .responseObject()
函数。作为一个通用函数,它能够序列化所有符合 ResponseObjectSerializable
协议的数据对象。
这意味着,如果我们定义一个含有 init?(response:representation:)
初始化方法的新类,那么 Alamofire 就能够自行从服务器返回该类型的对象。这时候,我们已经将序列化逻辑封装进了自定义类的内部。哈哈,是不是一个很赞的面向对象设计?
图片查看器使用的是 PhotoInfo
结构体,我们需要让这个结构体遵守 ResponseObjectSerializable
协议。
打开 Five100px.swift
,在 extension PhotoInfo: Hashable { ... }
下面添加如下所示的代码:
1
| extension PhotoInfo: ResponseObjectSerializable { }
|
提示:
虽然毋需详细了解 representation
参数在 PhotoInfo
对象中是如何序列化的,但是感兴趣的读者可以去浏览 init(response:representation:)
方法来了解其工作原理。
打开 PhotoViewerViewController.swift,注意不是 PhotoBrowserCollectionViewController.swift,然后在文件顶部加入一个必要的导入声明:
接着,在viewDidLoad()
方法内的底部加入以下代码:
您会得到一个找不到loadPhoto()
的错误,但是不必担心,我们接下来就要实现这个函数。
仍然是在同一个文件当中,在setupView()
方法前加入以下代码:
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
| private func loadPhoto() { Alamofire.request(Five100px.Router.photoInfo(photoID, .large)).validate().responseObject { (response: DataResponse<PhotoInfo>) in guard let info = response.result.value else { return } self.photoInfo = info DispatchQueue.main.async { self.addButtomBar() self.title = info.url } Alamofire.request(info.url, method: .get).validate().responseImage { response in guard let image = response.result.value else { return } self.imageView.image = image self.imageView.frame = self.centerFrameFromImage(image) self.spinner.stopAnimating() self.centerScrollViewContents() } } }
|
这时,我们在其他 Alamofire 请求的完成处理方法中发出了 Alamofire 请求。第一个请求接收到了一个 JSON 响应数据,然后它使用我们新建的通用响应序列化方法,在 JSON 数据之外创建了一个 PhotoInfo
实例。
(response: DataResponse<PhotoInfo>) in
是响应序列化方法的参数。我们可以从 Response 结构体中获取到所需的数据。这个参数中明确声明了我们的 PhotoInfo
实例,因此通用序列化方法将自行初始化,并返回该类型的一个对象,其包含有图片 URL。
第二个 Alamofire 请求使用您先前创建过的图片序列化方法,将 NSData
转化为 UIImage
,以便之后我们在图片视图中显示它。
注意:
我们不在这里使用路由,因为我们已经有了图片的绝对 URL 地址,我们无需自行构造 URL。
在请求响应对象之前调用的 .validate()
函数是另一个易用的 Alamofire 特性。将其与请求和响应链接,以确认响应的状态码在默认可接受的范围(200到299)内。如果认证失败,响应处理方法将出现一个相关错误,您可以在完成处理方法中处理这个错误。
即使没有发生错误,完成处理方法仍然还是会被调用。
生成并运行您的应用,单击其中一副图片后您应当看到它将覆盖全屏幕,如下所示:
好的!图片查看器正常工作了,双击该图片可以放大图片。
当类型安全的通用响应序列化方法初始化 PhotoInfo
的时候,您不仅仅只是设置了id
和url
属性,还有一些属性您并没有看见。
单击应用左下方的 Menu 按钮,然后您就可以看到该照片的详细信息:
单击屏幕上的任意位置就可以关闭照片详细信息。
如果您对 500px.com很熟悉的话,您就知道用户往往会在网站上给极佳的照片留下很多评论信息。现在既然我们的图片查看器正常工作了,那么现在我们就要开始搭建评论查看器了。
##为显示注释创建集合序列化方法
对于有评论的图片来说,图片查看器会显示一个包含评论数的评论按钮。单击这个评论按钮会弹出一个评论列表。
打开 Five100px.swift,然后在 struct Five100px
结构体声明上方添加以下代码:
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
| protocol ResponseCollectionSerializable { static func collection(from response: HTTPURLResponse, withRepresentation representation: Any) -> [Self] } extension DataRequest { @discardableResult func responseCollection<T: ResponseCollectionSerializable>(queue: DispatchQueue? = nil, completionHandler: @escaping (DataResponse<[T]>) -> Void) -> Self { let responseSerializer = DataResponseSerializer<[T]> { request, response, data, error in guard error == nil else { return .failure(BackendError.network(error: error!)) } let jsonSerializer = DataRequest.jsonResponseSerializer(options: .allowFragments) let result = jsonSerializer.serializeResponse(request, response, data, nil) guard case let .success(jsonObject) = result else { return .failure(BackendError.jsonSerialization(error: result.error!)) } guard let response = response else { let reason = "Response collection could not be serialized due to nil response." return .failure(BackendError.objectSerialization(error: reason)) } return .success(T.collection(from: response, withRepresentation: jsonObject)) } return response(responseSerializer: responseSerializer, completionHandler: completionHandler) } }
|
这段代码看起来很眼熟,它和我们之前创建的通用响应序列化方法相似。
唯一的不同点是,这个协议定义了返回集合的一个类方法(而不是单个元素),在本例中,返回的是 [self]
。完成处理方法将集合作为 Response 中的参数,即 [T]
,接着调用类型上的 collection
方法而不是调用初始化方法。
仍然是在同一个文件当中,在 Comment
结构体下方增加以下代码:
1 2 3 4 5 6 7 8 9 10 11 12 13 14
| extension Comment: ResponseCollectionSerializable { static func collection(from response: HTTPURLResponse, withRepresentation representation: Any) -> [Comment] { var comments = [Comment]() guard let represences = (representation as AnyObject).value(forKey: "comments") as? [[String: Any]] else { return comments } represences.forEach { if let comment = Comment(JSON: $0 as AnyObject) { comments.append(comment) } } return comments } }
|
这段代码让 Comment
遵守 ResponseCollectionSerializable
协议,因此它将和上面的响应序列化方法协同工作。
现在您需要做的就是使用它。打开 PhotoCommentsViewController.swift 然后在文件顶部加入以下必要的导入声明:
现在在viewDidLoad()
方法中的底部加入以下代码:
1 2 3 4 5 6 7 8 9
| Alamofire.request(Five100px.Router.comments(photoID, 1)).validate().responseCollection { (response: DataResponse<[Comment]>) in if let comments = response.result.value { self.comments = comments } self.tableView.reloadData() }
|
这段代码使用了您新建的响应序列化方法来解序列化位于 Comment
集合中响应的 NSData
,然后将它们保存在属性当中,最后在表视图中重新加载它们。
接下来,向tableView(_:cellForRowAt:)
中添加以下代码(在return cell
语句上面):
1 2 3 4 5 6 7 8 9 10 11 12 13
| cell.userFullnameLabel.text = comments[indexPath.row].userFullname cell.commentLabel.text = comments[indexPath.row].commentBody cell.userImageView.image = nil let imageURL = comments[indexPath.row].userPictureURL Alamofire.request(imageURL, method: .get).validate().responseImage { response in if let image = response.result.value, response.request?.url?.absoluteString == imageURL { cell.userImageView.image = image } }
|
这段代码在表视图单元格中显示了评论信息,同样它还接连提交了第二个 Alamofire 请求来加载图片(这和我们在教程第一部分所做的相类似)。
生成并运行您的应用,找到一个有评论的图片。您可以通过评论图标上的数字来了解该图片的评论数目。单击评论按钮,然后该图片的评论界面就会显示出来,如下所示:
现在,您可能已经发现了几张您想下载的图片(也有可能是几百张哈),下一节我们将带领大家如何实现下载功能。
##显示下载进度
图片查看器的底栏中间有一个动作按钮。它显示出一个 .actionSheet
样式的 UIAlertController
控件来让您选择是否下载照片,但是现在它还没有任何功能。到目前为止,我们所做的仅仅只是从 500px.com 加载到内存中。那么我们要如何下载并保存文件呢?
打开 PhotoViewerViewController.swift,将空函数 downloadPhoto()
用以下代码替换:
1 2 3 4 5 6 7 8 9 10 11 12 13 14
| fileprivate func downloadPhoto() { Alamofire.request(Five100px.Router.photoInfo(photoInfo!.id, .xLarge)).validate().responseJSON { response in guard let jsonDictionary = response.result.value as? [String: Any], let imageURL = (jsonDictionary as AnyObject).value(forKeyPath: "photo.image_url") as? String else { return } let destination = DownloadRequest.suggestedDownloadDestination(for: .documentDirectory, in: .userDomainMask) let _ = Alamofire.download(imageURL, to: destination) } }
|
我们来依次解释一下这些代码的作用:
- 我们首先请求一个新的
PhotoInfo
,此时请求的是 xLarge
大小。
- 获取要保存文件的默认存储地址,我们将会将其存放在您应用的 Documents 目录的一个子目录中。该子目录的名字将和服务器建议的名字相同。
destination
是一个变相的闭包——虽然只是短短的一瞬间。
Alamofire.download(_:method:to:)
方法和Alamofire.request(_:method:)
方法有很大的不同。Alamofire.download(_:method:to)
方法不需要响应处理方法,也不需要响应序列化方法来对数据进行处理。因为它已经知道如何去处理这些数据了,就是把它们保存在硬盘上!destination
闭包将返回要保存图片的路径。
生成并运行您的应用,然后找到某个您最喜欢的图片,单击动作按钮,接着单击 Download Photo 按钮。目前您还不会看到任何的回应,但是回到应用主界面来,然后单击 Downloads 标签,这时您就可以看到您下载的照片了。
您或许会问了:“为什么不直接使用同一个文件路径来保存呢?”。原因是在我们下载图片之前我们并不知道图片的名字。对于 500px.com 来说,服务器始终只会根据图片的尺寸返回1.jpg、2.jpg、3.jpg、4.jpg或者5.jpg这样的名字。我们不能够将相同名字的图片保存在同样的文件夹内。
我们使用闭包作为 Alamofire.download
的第三个参数,而不是传递进一个固定的字符串路径。Alamofire 接着就会在一个合适的时间调用该闭包,然后将 temporaryURL
和 NSHTTPURLResponse
传递进来作为参数,然后返回一个 URL 的实例,指向硬盘上您有权访问的路径。
试着保存一些图片,然后返回到下载标签,这时候您会发现,诶?!为啥只有一个文件?闹哪样嘛!
这是因为文件名并不是独一无二的,因此我们需要实现自己的命名逻辑。用以下代码替换掉downloadPhoto()
方法中的注释 //2
下的语句:
1 2 3 4 5 6
| let destination: DownloadRequest.DownloadFileDestination = { _, response in let documentsURL = FileManager.default.urls(for: .documentDirectory, in: .userDomainMask)[0] let fileURL = documentsURL.appendingPathComponent("\(self.photoInfo!.id).\(response.suggestedFilename)") return (fileURL, [.removePreviousFile, .createIntermediateDirectories]) }
|
再次说明,let destination
是一个闭包,但是我们这次实现了自己的命名逻辑。我们使用在闭包外捕获的图片id
,然后将其和服务器建议的名称相连接,这两者之间使用“.”来分隔。
生成并运行,然后现在您就可以保存多个图片了:
文件保存功能目前工作得很好,但是如果给用户显示下载进度的话那岂不是更好?Alamofire 可以很容易地实现显示下载进度指示器。
用以下代码替换掉 downloadPhoto()
方法中的注释 //3
下的语句:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17
| let progressIndicatorView = UIProgressView(frame: CGRect(x: 0.0, y: 80.0, width: self.view.bounds.width, height: 10.0)) progressIndicatorView.tintColor = .blue self.view.addSubview(progressIndicatorView) let _ = Alamofire.download(imageURL, to: destination).downloadProgress { progress in DispatchQueue.main.async { progressIndicatorView.setProgress(Float(progress.fractionCompleted), animated: true) if progress.fractionCompleted == 1 { progressIndicatorView.removeFromSuperview() } } }
|
我们来依次解释一下这些代码的作用:
- 我们使用标准
UIProgressView
控件来显示图片下载进度。对其进行配置并将其添加到 view 视图上。
- 借助 Alamofire,我们可以通过
.downloadProgress()
来获取下载进度。.downloadProgress()
将定期调用一个闭包,这个闭包将会回调 NSProgress
参数。
- 通过调用
NSProgress
的 fractionCompleted
属性,我们就可以得到一个0到1之间的数字,这个数字代表这下载进度。如果下载时间不是瞬时的话,那么这个闭包可能会多次运行。每次运行我们都能够更新屏幕上的进度条。
- 一旦下载结束,我们只需从view 视图上移去进度条。
生成并运行您的应用,找到某个图片并下载它,这是您就可以看到下载进度条。
当下载完成时,进度条消失,因此如果您的网络很快的话那么您很有可能会看不到这个进度条。
##优化和刷新
好的,现在是时候来实现下拉刷新功能了。(强迫症患者们总是在不停地刷新……刷新……再刷新,对吧?泪一般的事实)
打开 PhotoBrowserCollectionViewController.swift,然后用以下代码替换 private dynamic func handleRefresh()
:
1 2 3 4 5 6 7 8 9 10 11 12
| private dynamic func handleRefresh() { refreshControl.beginRefreshing() photos.removeAll() currentPage = 1 collectionView?.reloadData() refreshControl.endRefreshing() populatePhotos() }
|
上述的代码清空您当前的集合(photos
),然后重置 currentPage
,最后刷新 UI。
生成并运行您的应用,下拉刷新,这时候您就可以看到新的图片出现了:
当您快速滑动照片浏览页面的时候,您可能会注意到可以将仍然在请求图片的单元送出屏幕。实际上,图片请求在其结束前会一直运行,但是下载的照片以及相关数据则会被丢弃。
此外,当您返回到之前的单元,您就必须还得为显示图片而发送网络请求,即使您刚才下载了那幅图片。我们需要改善这个设计,以防止浪费带宽。
我们可以利用缓存来保存加载过的图像,这样就可以不必再次加载。还有,如果某个单元在网络请求结束前出列了,我们可以取消其所有的网络请求。
打开 PhotoBrowserCollectionViewController.swift,然后在 private let refreshControl
语句上添加以下代码:
1
| private let imageCache = NSCache<NSString, UIImage>()
|
它创建了一个用于缓存图片的NSCache
对象,由于 NSCache 只能缓存 AnyObject 类型,所以这里我们只能使用 NSString 作为 Key 的类型。
接下来,用以下代码替换collectionView(_:cellForItemAt:)
:
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
| override func collectionView(_ collectionView: UICollectionView, cellForItemAt indexPath: IndexPath) -> UICollectionViewCell { 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.request?.cancel() if let image = imageCache.object(forKey: imageURL as NSString) { cell.imageView.image = image } else { cell.imageView.image = nil cell.request = Alamofire.request(imageURL, method: .get).responseImage { response in guard let image = response.result.value, response.result.error == nil else { return } self.imageCache.setObject(image, forKey: response.request!.url!.absoluteString as NSString) cell.imageView.image = image } } 如果单元格在图片下载完成之前离开了屏幕,那么我们将暂停下载, 并且返回一个NSURLErrorDomain (-999: cancelled)对象。这是业界普遍的处理方式。 */ return cell }
|
我们来依次解释一下这些代码的作用:
- 出队的单元可能已经有一个连带的 Alamorire 请求。检查这个请求是否相关,也就是说,检查该请求的 URL 是否和要显示的图片 URL 相匹配,否则就取消请求。
- 使用可选值绑定来检查该图片是否有缓存版本。如果有的话,使用该缓存版本而不是再次下载。
- 如果没有相应的缓存版本的话,那么就下载它。然后,出列单元可能已经显示出了另一幅图像。这样子的话,就将其设置为
nil
,因此当图片在下载时该单元将为空。
- 从服务器下载图片,这里的关键是我们在单元中存储了 Alamofire 请求对象,当网络异步调用返回时使用。
- 如果我们没有接收到错误信息,那么就下载相应的图片,并在随后缓存它。
- 检查单元是否出列以显示新的图片。如果没有的话,则相应地设置单元的图片。
生成并运行您的应用。您会注意到随着在照片浏览器中来回滚动,图像的加载速度快了许多。我们已经砍掉了不必要的请求,并缓存了已下载的图片以便重复使用,这些操作让我们的网络请求优化了不少。良好的网络处理和灵活的用户界面能够提高用户体验的哦!
注意:
某些单元可能会显示为空,而当它全屏显示时又不为空。这并不是您的原因,这是因为 500px.com 并没有这些图片的缩略图。
##接下来该何去何从?
这里是本系列教程的完整项目。您会看到本教程没有涉及到的很多 UI 元素的设计细节。
如果您跟随我们的教程完成了学习,那么您现在已经能够很好的使用最受欢迎的第三方库—— Alamofire 了。我们学习了可链接的请求和响应方法、生成自定义响应序列化方法、创建路由并将 URL 参数进行编码、下载文件、使用进度条,以及响应验证。恭喜您获得了一系列成就!当当当!
Alamofire 同样能验证使用不同方案的服务器。同样,它还可以上传文件和流数据(并未在本教程中提到)。但是您目前对 Alamofire 的了解,相信您能够很快了解如何完成这些任务。
如果您使用 Swift 来开始一个新项目,那么使用 Alamofire 则是最佳选择,因为它已经涵盖了最常用的网络操作。
您可以用我们的教程项目完成更多更多的事情。您可以通过使用用户名和密码来避免使用消费者密钥,这样您就可以在 500px.com 上给自己的喜爱的照片投票,或者您可以为评论页面添加页面,让用户体验更加美好~我相信您还能想出更多的点子!
我希望你能够喜欢这个教程,如果您对本教程或者 Alamofire 有任何疑问,请加入到我们的讨论当中来!