(Swift) iOS Apps with REST APIs(六) -- 使用PINRemoteImage实现图片加载及缓存

本小节将会介绍有关报头的一些处理方式,并尝试进行最原始的图片加载、缓存等功能处理。最后使用PINRemoteImage组件完成图片的加载、显示、缓存等功能。

重要说明: 这是一个系列教程,非本人原创,而是翻译国外的一个教程。本人也在学习Swift,看到这个教程对开发一个实际的APP非常有帮助,所以翻译共享给大家。原教程非常长,我会陆续翻译并发布,欢迎交流与分享。

自定义报头(Header)

在Alamofire的请求中加入自定义报头(Header)有三种方式:第一种是通过Alamofire的管理器配置(Manager)为整个会话添加自定义报头(Header),第二种是通过报头参数为单独的某一个请求添加自定义报头,最后一种方式是通过URLRequestConvertible添加自定义报头。下面我们将逐个进行讲解。

会话报头

有些报头(Header)在每次进行API请求的时候都需要提交,如:API键值,或者Accept等。因此这些报头(Header)最好是在全局会话中进行设置,而不是每次请求的时候重复设置。对于这种情况,我们可以通过Alamofire的管理器(Manager)进行设置:

let manager = Alamofire.Manager.sharedInstance   
manager.session.configuration.HTTPAdditionalHeaders = 
  ["Accept": "application/json"]

这样设置后,后面我们在请求的是否就需要通过Alamofire的管理器来,而不是Alamofire类方法,API键值就不需要在每次请求的时候再进行设置了(译者注:这里应该是说的Accept,不过API Key也是一样)。请求时,使用:

let manager = Alamofire.Manager.sharedInstance 
manager.request(...)

替换原来的方式:

Alamofire.request(...)

Alamofire的管理器是可以被复用,因此我们在GitHubAPIManager中增加一个变量来保存它:

class GitHubAPIManager {
  static let sharedInstance = GitHubAPIManager() 
  var alamofireManager:Alamofire.Manager
    
  let clientID: String = "1234567890"
  let clientSecret: String = "abcdefghijkl"
  init () {
    let configuration = NSURLSessionConfiguration.defaultSessionConfiguration()    
    alamofireManager = Alamofire.Manager(configuration: configuration)
  }
  ...
}

在你的API管理器中增加Alamofire.Manager的实例。

一次性请求的报头(Header)

那如何为一个单独的请求设置报头(Header)呢?Alamofire(1.3版本之后)为我们提供了很方便的方法来达到这个目的。首先我们为自定义的报头创建一个字典(Dictionary):

let headers = ["Accept": "application/json"]

然后传递给我们要调用的manager.request(如果不想在请求中包含前面设置的全局报头则可以使用Alamofire.request):

let urlString = "https://api.github.com/gists" 
manager.request(.GET, urlString, headers: headers).responseJSON(...)

或:

let urlString = "https://api.github.com/gists" 
Alamofire.request(.GET, urlString, headers: headers).responseJSON(...)  

通过URLRequestConvertible设置报头

因为我们使用Alamofire路由器(Router)来构建我们的URL请求,因此我们可以在NSMutableURLRequest中直接添加报头(Header)。如:

let URL = NSURL(string: GistRouter.baseURLString)!
let URLRequest = NSMutableURLRequest(URL:
  URL.URLByAppendingPathComponent(result.path))

URLRequest.setValue("application/json", forHTTPHeaderField: "Accept") 

这样当URL进行请求的时候就会使用这些报头。我们后面会使用这种方式将OAuth的访问令牌添加到报头中。

报头小结

许多功能中都会使用到报头(Header),如:缓存、cookies等。后面我们所要实现的三种验证方式都会使用到,在那里我们会更进一步详细说明使用它。

检查你的开发文档中关于报头要求的章节,如果需要把它们添加到API管理器中。先不要担心有关认证的报头,我们会在认证章节中来处理。

在实现认证功能之前,我们还是先继续完善表格视图中的基本功能。接下来,我们会实现拥有者头像加载及显示,视图的滚动加载,及下拉刷新等功能。

图像的异步加载及缓存

到目前为止,我们构建了一个Swift的App,能够通过GitHub的API获取gists。我们已经实现如下功能:

  • 通过GitHub的gists API获取公共的gists列表
  • 使用Alamofire的响应序列化器将JSON转换为Swift对象数组
  • 解析web服务返回JSON数据中的一些字段
  • 将列表在表格视图中显示

在本章我们将添加一个新的功能,就是:为表格视图的每一个行显示gists拥有者的头像。我们将使用异步的方式,通过解析到的图像URL地址来加载图像。此外,我们还需要处理表格视图中单元格的复用问题,把图片加入到缓存中,并在显示的时候从中获取,而不是在每次需要显示的时候都从服务器中重复下载。

如果你的API中也需要显示图片,那么请跟随本章节来完成它。如果没有,那么你可以先跳过本章,等后面需要时,即使不是在视图表格中显示,再返回来。

当本章完成后,App界面看起来如下:

(Swift) iOS Apps with REST APIs(六) -- 使用PINRemoteImage实现图片加载及缓存_第1张图片

如果你前面没有一直跟着本教程编写代码,你可以从GitHub上下载前一个章节的完整代码。

如果你不想敲代码,你可以从GitHub上下载本章的代码。

通过URL加载图片

Gist类中的init(json:)方法中我们已经解析了gist拥有者头像的URL地址:

self.ownerAvatarURL = json["owner"]["avatar_url"].string

现在我们已经为从URL中获取图片准备好了,接下来让我们完成它。首先,在GitHubAPIManager中添加下面的函数:

class GitHubAPIManager 
{
  ...
   
  func imageFromURLString(imageURLString: String, completionHandler: 
    (UIImage?, NSError?) -> Void) {
    alamofireManager.request(.GET, imageURLString)
      .response { (request, response, data, error) in
        // use the generic response serializer that returns NSData 
        if data == nil {
          completionHandler(nil, nil)
          return
        }
        let image = UIImage(data: data! as NSData) 
        completionHandler(image, nil)
    }
  }
}

我们使用imageURLString发起一个GET请求。一旦我们得到响应(注意这里返回的类型是NSData,因为我们使用的.response方法),先对数据进行检查,并尝试转换成图片。如果转换成功,那么将它传给完成处理程序。如果失败(或者我们的请求根本不是图片),我们将退出,并返回一个错误给完成处理程序。

将图像设置给UITableViewCell

现在我们终于可以显示图片了。修改MasterViewController.cellForRowAtIndexPath:

override func tableView(tableView: UITableView,
  cellForRowAtIndexPath indexPath: NSIndexPath) -> UITableViewCell {
  let cell = tableView.dequeueReusableCellWithIdentifier("Cell", forIndexPath: indexPath)
      
  let gist = gists[indexPath.row] 
  cell.textLabel!.text = gist.description 
  cell.detailTextLabel!.text = gist.ownerLogin 
  if let urlString = gist.ownerAvatarURL {
    GitHubAPIManager.sharedInstance.imageFromURLString(urlString, completionHandler: { 
      (image, error) in
      if let error = error {
        print(error)
      }
      if let cellToUpdate = self.tableView?.cellForRowAtIndexPath(indexPath) { 
        cellToUpdate.imageView?.image = image // will work fine even if image is nil 
        // need to reload the view, which won't happen otherwise
        // since this is in an async call
        cellToUpdate.setNeedsLayout()
      }
    })
  } else { 
    cell.imageView?.image = nil
  }
    
  return cell 
}

我们通过if let urlString = gist.ownerAvatarURL检查图像的URL是否有值。如果有值那么调用前面写的方法请求图片数据。否则,我们将清空图片单元格:

GitHubAPIManager.sharedInstance.imageFromURLString(urlString, completionHandler: {
  ...
} else { 
  cell.imageView?.image = nil
}

在完成处理程序中,我们先检查是否有错误,如果有把它打印出来。这里,我们没有使用guard那是因为在这种情况下,我们得先清空原来存在的图片,而guard将强迫我们返回,从而无法清除原来已经存在的图片:

completionHandler: { (image, error) in 
  if let error = error {
    print(error)
  }
  if let cellToUpdate = self.tableView?.cellForRowAtIndexPath(indexPath) { 
    cellToUpdate.imageView?.image = image // will work fine even if image is nil 
    // need to reload the view, which won't happen otherwise
    // since this is in an async call
    cellToUpdate.setNeedsLayout()
  }
})

一旦我们得到图片就把它设置给单元格。这里会稍微有些复杂,因为imageFromURLString方法是异步的,而且UITableView又会复用单元格。因为在这里我们使用了dequeueReusableCellWithIdentifier,表格视图显示的时候会复用那些已经在不在显示区域的单元格。举个例子,假如我们有20个gists需要显示,但在一屏中仅能显示10个,这时候表格视图则大概只会创建12~14个。假如一个单于格已经滚出了屏幕,我们将无法给它设置图片,因为这时候有可能它已经被其它gist对象复用了,从而会造成数据错位的错误。

解决这个问题的方法就是使用索引路径,一旦我们得到图片,通过该索引路径获取表格视图的单元格:

if let cellToUpdate = self.tableview?.cellForRowAtIndexPath(indexPath)

这样我们就可以把图片设置给单元格了。因为图片的加载是异步进行的,假如相应的单元格还是显示的,那么我们就可以使用cellToUpdate.setNeedsLayout()告诉视图我们已经改变了部分东西,需要进行重新绘制。

需要注意的一点是,这里我们没有检查所要设置给单元格的图片是否是为空。如果因为某种原因图像为空,我们还得需要清除单元格中原来的图像,因此最好使用cellToUpdate.imageView?.image = result.value来设置。

保存并运行。你应该可以看到每个gist拥有者的头像,如果他们设置了自己头像的话:

(Swift) iOS Apps with REST APIs(六) -- 使用PINRemoteImage实现图片加载及缓存_第2张图片

如果你的API中也有图片,参考本小节来加载它们。

优化

我们为每一个单元格发起图片请求,但有时候当我们得到图片的时候已经不需要了,因为它已经滚出了屏幕的显示区域。一种优化方式就是当单元格滚出屏幕时取消Alamofire请求。但假如你的App中需要处理大量的图片,我估计你会不想这么干(因为你知道,这样会造成滚动不平滑)。现在我们的滚动看起来还是非常平滑的,并且我们还打算增加图片的缓存处理,因此刚才所说的那个问题已经不是现在急需处理的问题了。

即使对于这么简单的应用,图片缓存的优化还是非常值得的,这样就不用每次都去请求了。接下来我们将快速的,使用一个简单的,只能在单次运行中有效的缓存,来看看如何工作的。然后,我们将使用PINRemoeImage替换我们的缓存,PINRemoeImage是一个非常智能的具有持久化的缓存。

图片缓存

MasterViewController中我们添加一个字典用来存放这些图像,并使用它们的URL作为key:

var imageCache: Dictionary = Dictionary()

这样我们就可以在获取图片后将它存放字典中(在cellForRowAtIndexPath函数中):

if let urlString = gist.ownerAvatarURL { 
  GitHubAPIManager.sharedInstance.imageFromURLString(urlString, completionHandler: {
    (image, error) in
    if let error = error {
      print(error)
    }
    // Save the image so we won't have to keep fetching it if they scroll
    self.imageCache[urlString] = image
    if let cellToUpdate = self.tableView?.cellForRowAtIndexPath(indexPath) {
      cellToUpdate.imageView?.image = image // will work fine even if image is nil 
      // need to reload the view, which won't happen otherwise
      // since this is in an async call
      cellToUpdate.setNeedsLayout()
    }
  })
} else { 
  cell.imageView?.image = nil
}

在获取图片前我们先检查所要加载的图片是否已经在缓存中:

if let urlString = gist.ownerAvatarURL {
  if let cachedImage = imageCache[urlString] {
    cell.imageView?.image = cachedImage // will work fine even if image is nil 
  } else {
    GitHubAPIManager.sharedInstance.imageFromURLString(urlString, completionHandler: {
      ...
    })
  }
} else { 
  cell.imageView?.image = nil
}

最终代码为:

override func tableView(tableView: UITableView, cellForRowAtIndexPath
  indexPath: NSIndexPath) -> UITableViewCell {
  let cell = tableView.dequeueReusableCellWithIdentifier("Cell", forIndexPath: indexPath)
    
  let gist = gists[indexPath.row]
  cell.textLabel!.text = gist.description 
  cell.detailTextLabel!.text = gist.ownerLogin
  // set cell.imageView to display image at gist.ownerAvatarURL 
  if let urlString = gist.ownerAvatarURL {
    if let cachedImage = imageCache[urlString] {
      cell.imageView?.image = cachedImage // will work fine even if image is nil
    } else {
      GitHubAPIManager.sharedInstance.imageFromURLString(urlString, completionHandler: {
        (image, error) in
        if let error = error {
          print(error)
        }
        // Save the image so we won't have to keep fetching it if they scroll
        self.imageCache[urlString] = image
        if let cellToUpdate = self.tableView?.cellForRowAtIndexPath(indexPath) {
          cellToUpdate.imageView?.image = image // will work fine even if image is nil 
          // need to reload the view, which won't happen otherwise
          // since this is in an async call
          cellToUpdate.setNeedsLayout()
        }
      })
    }
  } else {
    cell.imageView?.image = nil 
  }

  return cell 
}

为了测试这些代码,需要设置一个断点,并单步执行看看代码是如何执行的。在需要设置断点的前面的行号中点击一下就可以设置断点了:

(Swift) iOS Apps with REST APIs(六) -- 使用PINRemoteImage实现图片加载及缓存_第3张图片

然后运行。当运行到断点时会停止。这时你可以通过Xcode底部的面板查看变量的值,并逐步调试:

(Swift) iOS Apps with REST APIs(六) -- 使用PINRemoteImage实现图片加载及缓存_第4张图片

点击step over按钮执行下一句:

通过查看高亮显示的代码行,可以知道哪个代码路径被执行。如果头像URL有值,那么将尝试加载:

(Swift) iOS Apps with REST APIs(六) -- 使用PINRemoteImage实现图片加载及缓存_第5张图片

如果没有,单元格将被设置为nil

(Swift) iOS Apps with REST APIs(六) -- 使用PINRemoteImage实现图片加载及缓存_第6张图片

如果想继续执行直到下一个断点,可以点击continue按钮:

当你每次运行App的时候,所有的图像在第一次显示的时候都将通过URL进行加载,因为这里我们并没有将缓存持久化。在下个小节我们将解决这个问题。当我们在屏幕上来回滚动时,表格视图将重复利用了已缓存的图像。你可以在我们从缓存中加载图像的代码处设置一个断点,这样就可以很方便的查看缓存是如何被复用的啦:

(Swift) iOS Apps with REST APIs(六) -- 使用PINRemoteImage实现图片加载及缓存_第7张图片

当单元格初始化完成后,上下滚动时,你将看到断点处的代码被执行。

更好的缓存: PINRemoteImage

我们通过Alamofire的请求从JSON数据中解析出图像的URL,通过异步请求的方式把加载的图片设置给UITableViewCell。然后我们建立一个初级的缓存,在单次运行中让图片可以被复用,从而帮我们避免了重复请求。当然,在发行版的APP中还是需要建立一个可以持久化的缓存机制,以便在多次运行中图片都可以复用。

PINRemoteImage是一个支持图片持久化的缓存处理库,那么接下来我们使用它,使得所要加载的图片只需要加载一次,并可以被重复使用。而且PINRemoteImage要比我们的非持久化缓存更容易集成和使用。

使用CocoaPods将PINRemoteImage v1.2加入到你的工程中。并在MasterViewController中引入,然后删除我们刚才写的缓存代码:

import UIKit
import PINRemoteImage
  
class MasterViewController: UITableViewController {
  ...
  
  ~~var imageCache: Dictionary = Dictionary()~~

  ...
}

PINRemoteImage最好和一个占位符图像一起使用。你可以把一个图像拖入到工程中,并把它改名为placeholder.png。这样如果用户没有设置头像的话,会显示我们刚才设置的占位符图像:

override func tableView(tableView: UITableView, cellForRowAtIndexPath
  indexPath: NSIndexPath) -> UITableViewCell {
  let cell = tableView.dequeueReusableCellWithIdentifier("Cell", forIndexPath: indexPath)
    
  let gist = gists[indexPath.row] 
  cell.textLabel!.text = gist.description 
  cell.detailTextLabel!.text = gist.ownerLogin 
  cell.imageView?.image = nil
   
  // set cell.imageView to display image at gist.ownerAvatarURL
  if let urlString = gist.ownerAvatarURL, url = NSURL(string: urlString) { 
    cell.imageView?.pin_setImageFromURL(url, placeholderImage:
      UIImage(named: "placeholder.png")) 
  } else {
    cell.imageView?.image = UIImage(named: "placeholder.png")
  }
  
  return cell 
}

小结

保存并运行。你会发现如果用户设置了头像的话将被加载显示,否则显示我们所设置的占位符图像:

(Swift) iOS Apps with REST APIs(六) -- 使用PINRemoteImage实现图片加载及缓存_第8张图片

点击这里可以下载本章的最终代码。

你可能感兴趣的:((Swift) iOS Apps with REST APIs(六) -- 使用PINRemoteImage实现图片加载及缓存)