在可滚动视图(如UITableView)中异步加载大量图片是一个很常见的任务。 然而,在图片正在下载的同时又要保持应用程序流畅滚动,可能有点挑战。
许多开发人员依靠像Alamofire和SDWebImage这样的库来避免背景图像加载最终的麻烦和缓存管理的麻烦。 但是,如果您想自己用纯代码而不是依赖第三方库, 那该怎么写呢?
嗯,一下就是我所要讲的:
在这篇文章中,您将使用您的技能构建一个流畅的应用程序,从iTunes API中提取游戏标题和图标的列表。 一路上,您将了解GCD(Grand Central Dispatch)和NSCache是如何协同工作以管理网络图片并生成一个整洁的缓存管理系统。
好,让我们开始吧:
打开Xcode,从菜单中选择“File \ New \ Project”,选择Single View应用程序模板并将项目命名为“ImagesDownloadAndCache”。
创建UI
您即将构建的应用程序包含单个界面,带有刷新控件的表视图,以加载内容。
Xcode storyboard文件带有一个默认的UIViewController,让我们删除它并替换为一个UITableViewController,默认情况下UITableViewController有一个刷新控件属性。
从项目导航器视图中选择Main.storyboard,将UITableViewController对象从对象库拖动到画布。 接下来,选择初始的View Controller,然后从键盘中点击delete。
注意:选择一个iPhone模型,这里我选择了iPhone 6s模型。
从项目导航器视图中选择ViewController.swift并更改类声明,使其成为一个UITableViewController子类。
class ViewController: UITableViewController {
切换回storyboard,选中TableViewController,然后在Identity inspector 中将类设置为ViewController。
然后我们设置好 identifier
界面搭建好了,我们可以开始编码了
首先,我们做好属性声明
var refreshCtrl: UIRefreshControl!
var tableData:[AnyObject]!
var task: URLSessionDownloadTask!
var session: URLSession!
var cache:NSCache!
数据将使用 URLSessionDownloadTask 类下载,这就解释了为什么你声明了上面的 task 和 session 属性。 此外,tableData 属性将用作表视图数据源,缓存变量是缓存字典的参考,APP将在下载图片之前使用缓存字典请求缓存中的图片(如果存在)。
接下来,找到viewDidLoad方法,并在super.viewDidLoad调用之后执行以下代码:
session = URLSession.shared
task = URLSessionDownloadTask()
self.refreshCtrl = UIRefreshControl()
self.refreshCtrl.addTarget(self, action: #selector(ViewController.refreshTableView), for: .valueChanged)
self.refreshControl = self.refreshCtrl
self.tableData = []
self.cache = NSCache()
上面的代码将初始化 session、task,tableData,以及 cache 缓存对象。 还添加了一个刷新的调用方法,以便在每次提取表视图时运行。 让我们继续实现“refreshTableView”选择器。
func refreshTableView(){
let url:URL! = URL(string: "https://itunes.apple.com/search?term=flappy&entity=software")
task = session.downloadTask(with: url, completionHandler: { (location: URL?, response: URLResponse?, error: Error?) -> Void in
if location != nil{
let data:Data! = try? Data(contentsOf: location!)
do{
let dic = try JSONSerialization.jsonObject(with: data, options: .mutableLeaves) as AnyObject
self.tableData = dic.value(forKey : "results") as? [AnyObject]
DispatchQueue.main.async(execute: { () -> Void in
self.tableView.reloadData()
self.refreshControl?.endRefreshing()
})
}catch{
print("something went wrong, try again")
}
}
})
task.resume()
}
基本上,上述方法将异步请求iTunes搜索API返回其标题或描述。 一旦接收到数据,tableData 数据源数组就被记录填充,并且表视图会从主线程重新加载。
然后,实现两个强制性数据源协议方法来填充表视图行中下载的数据,特别是图片数据。
将以下代码复制到类中:
override func tableView(_ tableView: UITableView, numberOfRowsInSection section: Int) -> Int {
return self.tableData.count
}
override func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell {
// 1
let cell = tableView.dequeueReusableCell(withIdentifier: "GameCell", for: indexPath)
let dictionary = self.tableData[(indexPath as NSIndexPath).row] as! [String:AnyObject]
cell.textLabel!.text = dictionary["trackName"] as? String
cell.imageView?.image = UIImage(named: "placeholder")
if (self.cache.object(forKey: (indexPath as NSIndexPath).row as AnyObject) != nil){
// 2
// Use cache
print("Cached image used, no need to download it")
cell.imageView?.image = self.cache.object(forKey: (indexPath as NSIndexPath).row as AnyObject) as? UIImage
} else {
// 3
let artworkUrl = dictionary["artworkUrl100"] as! String
let url:URL! = URL(string: artworkUrl)
task = session.downloadTask(with: url, completionHandler: { (location, response, error) -> Void in
if let data = try? Data(contentsOf: url){
// 4
DispatchQueue.main.async(execute: { () -> Void in
// 5
// Before we assign the image, check whether the current cell is visible
if let updateCell = tableView.cellForRow(at: indexPath) {
let img:UIImage! = UIImage(data: data)
updateCell.imageView?.image = img
self.cache.setObject(img, forKey: (indexPath as NSIndexPath).row as AnyObject)
}
})
}
})
task.resume()
}
return cell
}
刚刚实现的应用程序的主要部分,让我分解上面的代码,以便更好地了解:
1:这里,tableView 将使 cell 出列以便重用。 如果没有分配 cell,则 tableVeiw 将分配,调整大小并返回一个新的 cell。 接下来,在字典对象中提取数据源数组中的当前缓存记录。 游戏标题被设置为 cell 的文本标签,并且 cell 被临时分配给占位图片,同时等待其下载。
2:cache 是一个类似集合的容器,非常类似于 NSDictionary 实例。 这里你使用它作为 UIImage 对象的集合,其中关键是行索引(这是非常重要的,以便跟踪对应于每个 cell 的正确的缓存图片)。 所以基本上,你首先检查是否有当前图片的缓存副本。 如果副本已经存在,则将其加载到 cell 中。
3:如果没有给定的缓存副本,那么就会从服务器去异步下载它。
4:假设图片下载成功,将切换到主线程,以更新视图。 这很重要,因为所有 UI 任务都应该在主线程中执行,而不是在后台线程中执行。
5:这是棘手的部分,在更新图片之前检查 cell 是否在屏幕上可见。 否则,图片将在滚动时在每个 cell 上重复使用。 如果相关 cell 是可见的,那么只需将图片分配给 cell,并将其添加到缓存中以供以后使用。
此外,禁用 APP 的ATS(应用程序传输安全),以便能够执行网络操作。 要禁用ATS,请从项目导航器视图中选择Info.plist文件,并将其更改为以下内容:
更多开源,尽在 https://github.com/CNKCQ