本小节将会介绍有关报头的一些处理方式,并尝试进行最原始的图片加载、缓存等功能处理。最后使用
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界面看起来如下:
如果你前面没有一直跟着本教程编写代码,你可以从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拥有者的头像,如果他们设置了自己头像的话:
如果你的API中也有图片,参考本小节来加载它们。
优化
我们为每一个单元格发起图片请求,但有时候当我们得到图片的时候已经不需要了,因为它已经滚出了屏幕的显示区域。一种优化方式就是当单元格滚出屏幕时取消Alamofire请求。但假如你的App中需要处理大量的图片,我估计你会不想这么干(因为你知道,这样会造成滚动不平滑)。现在我们的滚动看起来还是非常平滑的,并且我们还打算增加图片的缓存处理,因此刚才所说的那个问题已经不是现在急需处理的问题了。
即使对于这么简单的应用,图片缓存的优化还是非常值得的,这样就不用每次都去请求了。接下来我们将快速的,使用一个简单的,只能在单次运行中有效的缓存,来看看如何工作的。然后,我们将使用PINRemoeImage替换我们的缓存,PINRemoeImage是一个非常智能的具有持久化的缓存。
图片缓存
在MasterViewController
中我们添加一个字典用来存放这些图像,并使用它们的URL作为key:
var imageCache: Dictionary<String, UIImage?> = Dictionary<String, UIImage?>()
这样我们就可以在获取图片后将它存放字典中(在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
}
为了测试这些代码,需要设置一个断点,并单步执行看看代码是如何执行的。在需要设置断点的前面的行号中点击一下就可以设置断点了:
然后运行。当运行到断点时会停止。这时你可以通过Xcode底部的面板查看变量的值,并逐步调试:
点击step over
按钮执行下一句:
通过查看高亮显示的代码行,可以知道哪个代码路径被执行。如果头像URL有值,那么将尝试加载:
如果没有,单元格将被设置为nil
:
如果想继续执行直到下一个断点,可以点击continue
按钮:
当你每次运行App的时候,所有的图像在第一次显示的时候都将通过URL进行加载,因为这里我们并没有将缓存持久化。在下个小节我们将解决这个问题。当我们在屏幕上来回滚动时,表格视图将重复利用了已缓存的图像。你可以在我们从缓存中加载图像的代码处设置一个断点,这样就可以很方便的查看缓存是如何被复用的啦:
当单元格初始化完成后,上下滚动时,你将看到断点处的代码被执行。
更好的缓存: PINRemoteImage
我们通过Alamofire的请求从JSON数据中解析出图像的URL,通过异步请求的方式把加载的图片设置给UITableViewCell
。然后我们建立一个初级的缓存,在单次运行中让图片可以被复用,从而帮我们避免了重复请求。当然,在发行版的APP中还是需要建立一个可以持久化的缓存机制,以便在多次运行中图片都可以复用。
PINRemoteImage是一个支持图片持久化的缓存处理库,那么接下来我们使用它,使得所要加载的图片只需要加载一次,并可以被重复使用。而且PINRemoteImage要比我们的非持久化缓存更容易集成和使用。
使用CocoaPods将PINRemoteImage v1.2
加入到你的工程中。并在MasterViewController
中引入,然后删除我们刚才写的缓存代码:
import UIKit
import PINRemoteImage
class MasterViewController: UITableViewController {
...
~~var imageCache: Dictionary<String, UIImage?> = Dictionary<String, UIImage?>()~~
...
}
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
}
小结
保存并运行。你会发现如果用户设置了头像的话将被加载显示,否则显示我们所设置的占位符图像:
点击这里可以下载本章的最终代码。