[Swift]在不依赖三方库的情况下如何异步下载和缓存图片?

在可滚动视图(如UITableView)中异步加载大量图片是一个很常见的任务。 然而,在图片正在下载的同时又要保持应用程序流畅滚动,可能有点挑战。

许多开发人员依靠像Alamofire和SDWebImage这样的库来避免背景图像加载最终的麻烦和缓存管理的麻烦。 但是,如果您想自己用纯代码而不是依赖第三方库, 那该怎么写呢?

嗯,一下就是我所要讲的:

在这篇文章中,您将使用您的技能构建一个流畅的应用程序,从iTunes API中提取游戏标题和图标的列表。 一路上,您将了解GCD(Grand Central Dispatch)和NSCache是如何协同工作以管理网络图片并生成一个整洁的缓存管理系统。

好,让我们开始吧:

打开Xcode,从菜单中选择“File \ New \ Project”,选择Single View应用程序模板并将项目命名为“ImagesDownloadAndCache”。


[Swift]在不依赖三方库的情况下如何异步下载和缓存图片?_第1张图片

创建UI

您即将构建的应用程序包含单个界面,带有刷新控件的表视图,以加载内容。

Xcode storyboard文件带有一个默认的UIViewController,让我们删除它并替换为一个UITableViewController,默认情况下UITableViewController有一个刷新控件属性。

从项目导航器视图中选择Main.storyboard,将UITableViewController对象从对象库拖动到画布。 接下来,选择初始的View Controller,然后从键盘中点击delete。

注意:选择一个iPhone模型,这里我选择了iPhone 6s模型。


[Swift]在不依赖三方库的情况下如何异步下载和缓存图片?_第2张图片

从项目导航器视图中选择ViewController.swift并更改类声明,使其成为一个UITableViewController子类。

class ViewController: UITableViewController {

切换回storyboard,选中TableViewController,然后在Identity inspector 中将类设置为ViewController。


[Swift]在不依赖三方库的情况下如何异步下载和缓存图片?_第3张图片

然后我们设置好 identifier


[Swift]在不依赖三方库的情况下如何异步下载和缓存图片?_第4张图片

界面搭建好了,我们可以开始编码了

首先,我们做好属性声明

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

你可能感兴趣的:([Swift]在不依赖三方库的情况下如何异步下载和缓存图片?)