Alamofire网络库进阶教程

时间:2023-06-19 22:32:34

Alamofire网络库进阶教程

本章节由CocoaChina翻译组成员星夜暮晨(博客)翻译自raywenderlich:Intermediate Alamofire Tutorial,敬请勘误。

欢迎回到我们的 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
@objc public protocol ResponseObjectSerializable {
  init(response: NSHTTPURLResponse, representation: AnyObject)
}
extension Alamofire.Request {
  public func responseObject(completionHandler: (NSURLRequest, NSHTTPURLResponse?, T?, NSError?) -> Void) -> Self {
    let serializer: Serializer = { (request, response, data) in
      let JSONSerializer = Request.JSONResponseSerializer(options: .AllowFragments)
      let (JSON: AnyObject?, serializationError) = JSONSerializer(request, response, data)
      if response != nil && JSON != nil {
        return (T(response: response!, representation: JSON!), nil)
      else {
        return (nil, serializationError)
      }
    }
    return response(serializer: serializer, completionHandler: { (request, response, object, error) in
      completionHandler(request, response, object as? T, error)
    })
  }
}

在上述的代码中,我们再一次给 Alamofire 创建了一个扩展,添加了新的响应序列化方法。这次,我们添加了.responseObject()函数。作为一个通用函数,它能够序列化所有符合ResponseObjectSerializable协议的数据对象。

这意味着,如果我们定义一个含有init(response:representation:)初始化方法的新类,那么 Alamofire 就能够自行从服务器返回该类型的对象。这时候,我们已经将序列化逻辑封装进了自定义类的内部。哈哈,是不是一个很赞的面向对象设计?

Alamofire网络库进阶教程

图片查看器使用的是PhotoInfo类,这个类遵守了ResponseObjectSerializable协议,并实现了所需的方法。不过您仍需要让这个类正式遵守ResponseObjectSerializable协议。

打开Five100px.swift,并且修改PhotoInfo类的声明来让其明确遵守ResponseObjectSerializable协议,如下所示:

1
class PhotoInfo: NSObject, ResponseObjectSerializable {

提示:

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

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

1
import Alamofire

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

1
loadPhoto()

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

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

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
func loadPhoto() {
  Alamofire.request(Five100px.Router.PhotoInfo(self.photoID, .Large)).validate().responseObject() {
    (_, _, photoInfo: PhotoInfo?, error) in
    if error == nil {
      self.photoInfo = photoInfo
      dispatch_async(dispatch_get_main_queue()) {
        self.addButtomBar()
        self.title = photoInfo!.name
      }
      Alamofire.request(.GET, photoInfo!.url).validate().responseImage() {
        (_, _, image, error) in
        if error == nil && image != nil {
          self.imageView.image = image
          self.imageView.frame = self.centerFrameFromImage(image)
          self.spinner.stopAnimating()
          self.centerScrollViewContents()
        }
      }
    }
  }
}

这时,我们在其他 Alamofire 请求的完成处理方法中发出了 Alamofire 请求。第一个请求接收到了一个 JSON 响应数据,然后它使用我们新建的通用响应序列化方法,在JSON 数据之外创建了一个PhotoInfo实例。

(_, _, photoInfo: PhotoInfo?, error) in是响应序列化方法的参数。刚开始的两个下划线"_"意味着我们忽略了前两个参数,我们无需明确地将它们明确命名为request和response。

第三个参数明确被声明为了我们的PhotoInfo实例,因此通用序列化方法将自行初始化,并返回该类型的一个对象,其包含有图片 URL。第二个 Alamofire 请求使用您先前创建过的图片序列化方法,将NSData转化为UIImage,以便之后我们在图片视图中显示它。

注意:

我们不在这里使用路由,因为我们已经有了图片的绝对 URL 地址,我们无需自行构造 URL。

在请求响应对象之前调用的.validate()函数是另一个易用的 Alamofire 特性。将其与请求和响应链接,以确认响应的状态码在默认可接受的范围(200到299)内。如果认证失败,响应处理方法将出现一个相关错误,您可以在完成处理方法中处理这个错误。

即使没有发生错误,完成处理方法仍然还是会被调用。第四个参数error是NSError的一个实例,它的值让我们能够在自定义方法中响应这个错误。

生成并运行您的应用,单击其中一副图片后您应当看到它将覆盖全屏幕,如下所示:

Alamofire网络库进阶教程

好的!图片查看器正常工作了,双击该图片可以放大图片。

当类型安全的通用响应序列化方法初始化PhotoInfo的时候,您不仅仅只是设置了id和url属性,还有一些属性您并没有看见。

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

Alamofire网络库进阶教程

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

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

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

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

打开Five100px.swift,然后在import Alamofire声明下添加以下代码:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
@objc public protocol ResponseCollectionSerializable {
  class func collection(#response: NSHTTPURLResponse, representation: AnyObject) -> [Self]
}
extension Alamofire.Request {
  public func responseCollection(completionHandler: (NSURLRequest, NSHTTPURLResponse?, [T]?, NSError?) -> Void) -> Self {
    let serializer: Serializer = { (request, response, data) in
      let JSONSerializer = Request.JSONResponseSerializer(options: .AllowFragments)
      let (JSON: AnyObject?, serializationError) = JSONSerializer(request, response, data)
      if response != nil && JSON != nil {
        return (T.collection(response: response!, representation: JSON!), nil)
      else {
        return (nil, serializationError)
      }
    }
    return response(serializer: serializer, completionHandler: { (request, response, object, error) in
      completionHandler(request, response, object as? [T], error)
    })
  }
}

这段代码看起来很眼熟,它和我们之前创建的通用响应序列化方法相似。

唯一的不同点是,这个协议定义了返回集合的一个类方法(而不是单个元素)。完成处理方法将集合作为其第三个参数,即[T],接着调用类型上的collection而不是调用初始化方法。

仍然是在同一个文件当中,将整个Comment类替换为以下形式:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
final class Comment: ResponseCollectionSerializable {
  class func collection(#response: NSHTTPURLResponse, representation: AnyObject) -> [Comment] {
    var comments = [Comment]()
    for comment in representation.valueForKeyPath("comments") as [NSDictionary] {
      comments.append(Comment(JSON: comment))
    }
    return comments
  }
  let userFullname: String
  let userPictureURL: String
  let commentBody: String
  init(JSON: AnyObject) {
    userFullname = JSON.valueForKeyPath("user.fullname") as String
    userPictureURL = JSON.valueForKeyPath("user.userpic_url") as String
    commentBody = JSON.valueForKeyPath("body") as String
  }
}

这段代码让Comment遵守ResponseCollectionSerializable协议,因此它将和上面的响应序列化方法协同工作。

现在您需要做的就是使用它。打开PhotoCommentsViewController.swift然后在文件顶部加入以下必要的导入声明:

1
import Alamofire

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

1
2
3
4
5
6
7
Alamofire.request(Five100px.Router.Comments(photoID, 1)).validate().responseCollection() {
  (_, _, comments: [Comment]?, error) in
  if error == nil {
    self.comments = comments
    self.tableView.reloadData()
  }
}

这段代码使用了您新建的响应序列化方法来解序列化位于Comment集合中响应的NSData,然后将它们保存在属性当中,最后在表视图中重新加载它们。

接下来,向tableView(_:cellForRowAtIndexPath)中添加以下代码(在return cell语句上面):

1
2
3
4
5
6
7
8
9
10
11
12
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(.GET, imageURL).validate().responseImage() {
  (request, _, image, error) in
  if error == nil {
    if request.URLString.isEqual(imageURL) {
      cell.userImageView.image = image
    }
  }
}

这段代码在表视图单元格中显示了评论信息,同样它还接连提交了第二个 Alamofire 请求来加载图片(这和我们在教程第一部分所做的相类似)。

生成并运行您的应用,找到一个有评论的图片。您可以通过评论图标上的数字来了解该图片的评论数目。单击评论按钮,然后该图片的评论界面就会显示出来,如下所示:

Alamofire网络库进阶教程

现在,您可能已经发现了几张您想下载的图片(也有可能是几百张哈),下一节我们将带领大家如何实现下载功能。

显示下载进度

图片查看器的底栏中间有一个动作按钮。它显示出一个UIActionSheet控件来让您选择是否下载照片,但是现在它还没有任何功能。到目前为止,我们所做的仅仅只是从 500px.com 加载到内存中。那么我们要如何下载并保存文件呢?

打开PhotoViewerViewController.swift,将空函数downloadPhoto()用以下代码替换:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
func downloadPhoto() {
  // 1
  Alamofire.request(Five100px.Router.PhotoInfo(photoInfo!.id, .XLarge)).validate().responseJSON() {
    (_, _, JSON, error) in
    if error == nil {
      let jsonDictionary = (JSON as NSDictionary)
      let imageURL = jsonDictionary.valueForKeyPath("photo.image_url") as String
      // 2
      let destination = Alamofire.Request.suggestedDownloadDestination(directory: .DocumentDirectory, domain: .UserDomainMask)
      // 3
      Alamofire.download(.GET, imageURL, destination)
    }
  }
}

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

我们首先请求一个新的PhotoInfo,此时请求的是XLarge大小。

获取要保存文件的默认存储地址,我们将会将其存放在您应用的 Documents 目录的一个子目录中。该子目录的名字将和服务器建议的名字相同。destination是一个变相的闭包——虽然只是短短的一瞬间。

Alamofire.download(_:_:_)方法和Alamofire.request(_:_)方法有很大的不同。Alamofire.download(_:_:_)方法不需要响应处理方法,也不需要响应序列化方法来对数据进行处理。因为它已经知道如何去处理这些数据了,就是把它们保存在硬盘上!destination闭包将返回要保存图片的路径。

生成并运行您的应用,然后找到某个您最喜欢的图片,单击动作按钮,接着单击Save按钮。目前您还不会看到任何的回应,但是回到应用主界面来,然后单击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
7
let destination: (NSURL, NSHTTPURLResponse) -> (NSURL) = {
  (temporaryURL, response) in
  if let directoryURL = NSFileManager.defaultManager().URLsForDirectory(.DocumentDirectory, inDomains: .UserDomainMask)[0] as? NSURL {
    return directoryURL.URLByAppendingPathComponent("\(self.photoInfo!.id).\(response.suggestedFilename)")
  }
  return temporaryURL
}

再次说明,let destination是一个闭包,但是我们这次实现了自己的命名逻辑。我们使用在闭包外捕获的图片id,然后将其和服务器建议的名称相连接,这两者之间使用“.”来分隔。

生成并运行,然后现在您就可以保存多个图片了:

Alamofire网络库进阶教程

文件保存功能目前工作得很好,但是如果给用户显示下载进度的话那岂不是更好?Alamofire 可以很容易地实现显示下载进度指示器。

用以下代码替换掉downloadPhoto()方法中的注释 //3 下的语句:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
// 4
let progressIndicatorView = UIProgressView(frame: CGRect(x: 0.0, y: 80.0, width: self.view.bounds.width, height: 10.0))
progressIndicatorView.tintColor = UIColor.blueColor()
self.view.addSubview(progressIndicatorView)
// 5
Alamofire.download(.GET, imageURL, destination).progress {
  (_, totalBytesRead, totalBytesExpectedToRead) in
  dispatch_async(dispatch_get_main_queue()) {
    // 6
    progressIndicatorView.setProgress(Float(totalBytesRead) / Float(totalBytesExpectedToRead), animated: true)
    // 7
    if totalBytesRead == totalBytesExpectedToRead {
      progressIndicatorView.removeFromSuperview()
    }
  }
}

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

我们使用标准UIProgressView控件来显示图片下载进度。对其进行配置并将其添加到view视图上。

借助 Alamofire,我们可以将.progress和三个参数进行链接。.progress将定期调用一个闭包,而且这三个参数分别是:bytesRead、totalBytesRead以及totalBytesExpectedToRead。

简单用totalBytesExpectedToRead除以totalBytesRead,我们就可以得到一个0到1之间的数字,这个数字代表这下载进度。如果下载时间不是瞬时的话,那么这个闭包可能会多次运行。每次运行我们都能够更新屏幕上的进度条。

一旦下载结束,我们只需从view 视图上移去进度条。

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

Alamofire网络库进阶教程

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

我们注意到,downloadPhoto方法仍然是使用在教程第一部分中用过的.resposneJSON()。我们要确保您能够理解响应序列化方法是如何工作的。因此请自行创建一个通用的响应序列化方法.responseObject()来替代.resposneJSON()。如果您想要检查您的解决方案,那么您可以继续向下查看我们是如何解决的:

downloadPhoto()方法开头的几个语句需要变为以下的形式:

1
2
3
4
5
6
7
Alamofire.request(Five100px.Router.PhotoInfo(photoInfo!.id, .XLarge)).validate().responseObject() {
  (_, _, photoInfo: PhotoInfo?, error) in
  if error == nil && photoInfo != nil {
    let imageURL = photoInfo!.url
.
.
.

这里您应当使用自定义的响应序列化方法,而不是手动解析 JSON 数据。这样可以让您的代码更为简洁。

优化和刷新

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

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

1
2
3
4
5
6
7
8
func handleRefresh() {
  refreshControl.beginRefreshing()
  self.photos.removeAllObjects()
  self.currentPage = 1
  self.collectionView.reloadData()
  refreshControl.endRefreshing()
  populatePhotos()
}

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

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

Alamofire网络库进阶教程

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

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

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

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

1
let imageCache = NSCache()

它创建了一个用于缓存图片的NSCache对象。

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

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
override func collectionView(collectionView: UICollectionView, cellForItemAtIndexPath indexPath: NSIndexPath) -> UICollectionViewCell {
  let cell = collectionView.dequeueReusableCellWithReuseIdentifier(PhotoBrowserCellIdentifier, forIndexPath: indexPath) as PhotoBrowserCollectionViewCell
  let imageURL = (photos.objectAtIndex(indexPath.row) as PhotoInfo).url
  // 1
  if cell.request?.request.URLString != imageURL {
    cell.request?.cancel()
  }
  // 2
  if let image = self.imageCache.objectForKey(imageURL) as? UIImage {
    cell.imageView.image = image
  else {
    // 3
    cell.imageView.image = nil
    // 4
    cell.request = Alamofire.request(.GET, imageURL).validate(contentType: ["image/*"]).responseImage() {
      (request, _, image, error) in
      if error == nil && image != nil {
        // 5
        self.imageCache.setObject(image!, forKey: request.URLString)
        // 6
        if request.URLString == cell.request?.request.URLString {
          cell.imageView.image = image
        }
      else {
        /*
        If the cell went off-screen before the image was downloaded, we cancel it and
        an NSURLErrorDomain (-999: cancelled) is returned. This is a normal behavior.
        */
      }
    }
  }
  return cell
}

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

1.出队的单元可能已经有一个连带的 Alamorire 请求。检查这个请求是否相关,也就是说,检查该请求的 URL 是否和要显示的图片 URL 相匹配,否则就取消请求。

2.使用可选值绑定来检查该图片是否有缓存版本。如果有的话,使用该缓存版本而不是再次下载。

3.如果没有相应的缓存版本的话,那么就下载它。然后,出列单元可能已经显示出了另一幅图像。这样子的话,就将其设置为nil,因此当图片在下载时该单元将为空。

4.从服务器下载图片,然后验证返回响应的content-type。如果返回的不是图片,那么返回值就是为error。因此您就不会再对这个无效的图片进行操作了。这里的关键是我们在单元中存储了 Alamofire 请求对象,当网络异步调用返回时使用。

5.如果我们没有接收到错误信息,那么就下载相应的图片,并在随后缓存它。

6.检查单元是否出列以显示新的图片。如果没有的话,则相应地设置单元的图片。

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

注意:

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

接下来该何去何从?

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

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

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

Alamofire 目前并没有实现 AFNetworking 的全部功能。但是如果您使用 Swift 来开始一个新项目,那么使用 Alamofire 则是最佳选择,因为它已经涵盖了最常用的网络操作。AFNetworking 的最受欢迎的特征之--UIKit 扩展还没有在 Alamofire 当中实现。但是这两个库是可以在同一个项目中并存的。

如果您先前已经使用过 AFNetworking,并且受不了没有 UIKit 上的类别方法setImageWithURL的话,那么您可能就还得在您的项目中继续使用 AFNetworking。例如,您可以用 Alamofire 调用服务器的 API,然后使用 AFNetworking 来异步显示图像。 AFNetworking 有一个共享的缓存,这样您无需手动管理缓存或者取消网络请求。

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

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