Alamofire网络库进阶教程


 原文链接     by: 星夜暮晨

原文 Intermediate Alamofire Tutorial已失效

原文作者:Essan Parto
译者:星夜暮晨

CocoaChina 对应地址:
http://www.cocoachina.com/ios/20141203/10514.html

2014年12月2日

2016年9月27日,更新至 Xcode 8.0、Swift 3.0 及 Alamofire 4.0.1
2015年11月24日,更新至 Xcode 7.1、Swift 2.1 及 Alamofire 3.1.3
2015年4月15日:更新至 Xcode 6.3 及 Swift 1.2



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

欢迎回到我们的 Alamofire 网络库使用教程,本文是此教程的第二部分,同时也是最后一个部分。

在教程的第一部分中,我们学习了 Alamofire 的一些基本用法,比如说发送 GET 请求、传递参数、创建请求路由以及创建自定义响应序列化方法。

在学习的过程中,我们也生成了一个很赞的名为 Photomania 的图片库应用。

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

  • 照片查看器
  • 查看评论以及其他信息的功能
  • 下载照片功能,附带有一个圆列进度条
  • 优化网络访问以及图片缓存
  • 下拉刷新操作

让我们开始吧

您可以使用您在教程第一部分所完成的项目来开始本章教程。但是如果您跳过了第一部分的教程或者对自己的项目没有信心的话,那么您也可以使用我们提供的标准项目

提示:

如果您跳过了第一部分的教程,那么请不要忘记您首先应当从 500px.com 网站上获取消费者密钥,然后在 Five100px.swift 中用其替换必要的部分。关于如何获取该密钥,以及在何处使用它,都在本教程的第一部分:Alamofire 网络库基础教程中有详细说明。

生成并运行起始项目,以确定我们应用运行正常。图片预览功能能够正常工作,但是单击图片并不会将其以全屏打开。这就是我们所要解决的问题!

创建图片查看器

说句老实话,范型可以说是包括 Swift 在内的高级编程语言中最强大的特性之一。一般情况下,在我们这个项目中最好使用范型这个功能。

打开Five100px.swift,然后在文件顶部,即 import Alamofire 语句下方修改以及添加以下代码:

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 { ... } 下面添加如下所示的代码:

extension PhotoInfo: ResponseObjectSerializable { }

提示:

虽然毋需详细了解 representation 参数在 PhotoInfo 对象中是如何序列化的,但是感兴趣的读者可以去浏览 init(response:representation:) 方法来了解其工作原理。

打开 PhotoViewerViewController.swift,注意不是 PhotoBrowserCollectionViewController.swift,然后在文件顶部加入一个必要的导入声明:

import Alamofire

接着,在viewDidLoad()方法内的底部加入以下代码:

loadPhoto()

您会得到一个找不到loadPhoto()的错误,但是不必担心,我们接下来就要实现这个函数。

仍然是在同一个文件当中,在setupView()方法前加入以下代码:

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 的时候,您不仅仅只是设置了idurl属性,还有一些属性您并没有看见。

单击应用左下方的 Menu 按钮,然后您就可以看到该照片的详细信息:


单击屏幕上的任意位置就可以关闭照片详细信息。

如果您对 500px.com很熟悉的话,您就知道用户往往会在网站上给极佳的照片留下很多评论信息。现在既然我们的图片查看器正常工作了,那么现在我们就要开始搭建评论查看器了。

为显示注释创建集合序列化方法

对于有评论的图片来说,图片查看器会显示一个包含评论数的评论按钮。单击这个评论按钮会弹出一个评论列表。

打开 Five100px.swift,然后在 struct Five100px 结构体声明上方添加以下代码:

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 结构体下方增加以下代码:

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 然后在文件顶部加入以下必要的导入声明:

import Alamofire

现在在viewDidLoad()方法中的底部加入以下代码:

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语句上面):

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() 用以下代码替换:

fileprivate func downloadPhoto() {
  // 1
  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 }

    // 2
    let destination = DownloadRequest.suggestedDownloadDestination(for: .documentDirectory, in: .userDomainMask)

    // 3
    let _ = Alamofire.download(imageURL, to: destination)
  }
}

我们来依次解释一下这些代码的作用:

  1. 我们首先请求一个新的 PhotoInfo,此时请求的是 xLarge 大小。
  2. 获取要保存文件的默认存储地址,我们将会将其存放在您应用的 Documents 目录的一个子目录中。该子目录的名字将和服务器建议的名字相同。destination 是一个变相的闭包——虽然只是短短的一瞬间。
  3. Alamofire.download(_:method:to:)方法和Alamofire.request(_:method:)方法有很大的不同。Alamofire.download(_:method:to)方法不需要响应处理方法,也不需要响应序列化方法来对数据进行处理。因为它已经知道如何去处理这些数据了,就是把它们保存在硬盘上!destination 闭包将返回要保存图片的路径。

生成并运行您的应用,然后找到某个您最喜欢的图片,单击动作按钮,接着单击 Download Photo 按钮。目前您还不会看到任何的回应,但是回到应用主界面来,然后单击 Downloads 标签,这时您就可以看到您下载的照片了。

您或许会问了:“为什么不直接使用同一个文件路径来保存呢?”。原因是在我们下载图片之前我们并不知道图片的名字。对于 500px.com 来说,服务器始终只会根据图片的尺寸返回1.jpg2.jpg3.jpg4.jpg或者5.jpg这样的名字。我们不能够将相同名字的图片保存在同样的文件夹内。

我们使用闭包作为 Alamofire.download 的第三个参数,而不是传递进一个固定的字符串路径。Alamofire 接着就会在一个合适的时间调用该闭包,然后将 temporaryURLNSHTTPURLResponse 传递进来作为参数,然后返回一个 URL 的实例,指向硬盘上您有权访问的路径。

试着保存一些图片,然后返回到下载标签,这时候您会发现,诶?!为啥只有一个文件?闹哪样嘛!

这是因为文件名并不是独一无二的,因此我们需要实现自己的命名逻辑。用以下代码替换掉downloadPhoto()方法中的注释 //2 下的语句:

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
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)

// 2
let _ = Alamofire.download(imageURL, to: destination).downloadProgress { progress in
  DispatchQueue.main.async {
    // 3
    progressIndicatorView.setProgress(Float(progress.fractionCompleted), animated: true)

    // 4
    if progress.fractionCompleted == 1 {
      progressIndicatorView.removeFromSuperview()
    }
  }
}

我们来依次解释一下这些代码的作用:

  1. 我们使用标准 UIProgressView 控件来显示图片下载进度。对其进行配置并将其添加到 view 视图上。
  2. 借助 Alamofire,我们可以通过 .downloadProgress() 来获取下载进度。.downloadProgress() 将定期调用一个闭包,这个闭包将会回调 NSProgress 参数。
  3. 通过调用 NSProgressfractionCompleted 属性,我们就可以得到一个0到1之间的数字,这个数字代表这下载进度。如果下载时间不是瞬时的话,那么这个闭包可能会多次运行。每次运行我们都能够更新屏幕上的进度条。
  4. 一旦下载结束,我们只需从view 视图上移去进度条。

生成并运行您的应用,找到某个图片并下载它,这是您就可以看到下载进度条。


当下载完成时,进度条消失,因此如果您的网络很快的话那么您很有可能会看不到这个进度条。

优化和刷新

好的,现在是时候来实现下拉刷新功能了。(强迫症患者们总是在不停地刷新……刷新……再刷新,对吧?泪一般的事实)

打开 PhotoBrowserCollectionViewController.swift,然后用以下代码替换 private dynamic func handleRefresh()

private dynamic func handleRefresh() {
  refreshControl.beginRefreshing()

  photos.removeAll()
  currentPage = 1

  collectionView?.reloadData()

  refreshControl.endRefreshing()

  populatePhotos()
}

上述的代码清空您当前的集合(photos),然后重置 currentPage,最后刷新 UI。

生成并运行您的应用,下拉刷新,这时候您就可以看到新的图片出现了:


当您快速滑动照片浏览页面的时候,您可能会注意到可以将仍然在请求图片的单元送出屏幕。实际上,图片请求在其结束前会一直运行,但是下载的照片以及相关数据则会被丢弃。

此外,当您返回到之前的单元,您就必须还得为显示图片而发送网络请求,即使您刚才下载了那幅图片。我们需要改善这个设计,以防止浪费带宽。

我们可以利用缓存来保存加载过的图像,这样就可以不必再次加载。还有,如果某个单元在网络请求结束前出列了,我们可以取消其所有的网络请求。

打开 PhotoBrowserCollectionViewController.swift,然后在 private let refreshControl 语句上添加以下代码:

private let imageCache = NSCache<NSString, UIImage>()

它创建了一个用于缓存图片的NSCache对象,由于 NSCache 只能缓存 AnyObject 类型,所以这里我们只能使用 NSString 作为 Key 的类型。

接下来,用以下代码替换collectionView(_:cellForItemAt:)

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

    // 1
    cell.request?.cancel()

    // 2
    if let image = imageCache.object(forKey: imageURL as NSString) {
      cell.imageView.image = image
    } else {
      // 3
      cell.imageView.image = nil

      // 4
      cell.request = Alamofire.request(imageURL, method: .get).responseImage {
        response in
        guard let image = response.result.value, response.result.error == nil else { return }
        // 5
        self.imageCache.setObject(image, forKey: response.request!.url!.absoluteString as NSString)
        // 6
        cell.imageView.image = image
      }
    }
    /*
    如果单元格在图片下载完成之前离开了屏幕,那么我们将暂停下载,
    并且返回一个NSURLErrorDomain (-999: cancelled)对象。这是业界普遍的处理方式。
    */

    return cell
  }

我们来依次解释一下这些代码的作用:

  1. 出队的单元可能已经有一个连带的 Alamorire 请求。检查这个请求是否相关,也就是说,检查该请求的 URL 是否和要显示的图片 URL 相匹配,否则就取消请求。
  2. 使用可选值绑定来检查该图片是否有缓存版本。如果有的话,使用该缓存版本而不是再次下载。
  3. 如果没有相应的缓存版本的话,那么就下载它。然后,出列单元可能已经显示出了另一幅图像。这样子的话,就将其设置为nil,因此当图片在下载时该单元将为空。
  4. 从服务器下载图片,这里的关键是我们在单元中存储了 Alamofire 请求对象,当网络异步调用返回时使用。
  5. 如果我们没有接收到错误信息,那么就下载相应的图片,并在随后缓存它。
  6. 检查单元是否出列以显示新的图片。如果没有的话,则相应地设置单元的图片。

生成并运行您的应用。您会注意到随着在照片浏览器中来回滚动,图像的加载速度快了许多。我们已经砍掉了不必要的请求,并缓存了已下载的图片以便重复使用,这些操作让我们的网络请求优化了不少。良好的网络处理和灵活的用户界面能够提高用户体验的哦!

注意:

某些单元可能会显示为空,而当它全屏显示时又不为空。这并不是您的原因,这是因为 500px.com 并没有这些图片的缩略图。

接下来该何去何从?

这里是本系列教程的完整项目。您会看到本教程没有涉及到的很多 UI 元素的设计细节。

如果您跟随我们的教程完成了学习,那么您现在已经能够很好的使用最受欢迎的第三方库—— Alamofire 了。我们学习了可链接的请求和响应方法、生成自定义响应序列化方法、创建路由并将 URL 参数进行编码、下载文件、使用进度条,以及响应验证。恭喜您获得了一系列成就!当当当!

Alamofire 同样能验证使用不同方案的服务器。同样,它还可以上传文件和流数据(并未在本教程中提到)。但是您目前对 Alamofire 的了解,相信您能够很快了解如何完成这些任务。

如果您使用 Swift 来开始一个新项目,那么使用 Alamofire 则是最佳选择,因为它已经涵盖了最常用的网络操作。

您可以用我们的教程项目完成更多更多的事情。您可以通过使用用户名和密码来避免使用消费者密钥,这样您就可以在 500px.com 上给自己的喜爱的照片投票,或者您可以为评论页面添加页面,让用户体验更加美好~我相信您还能想出更多的点子!

我希望你能够喜欢这个教程,如果您对本教程或者 Alamofire 有任何疑问,请加入到我们的讨论当中来!

相关代码:

Alamofire