从URL异步加载图片

3

我正在使用Swift将图像加载到UICollectionView中。我正在异步获取图像。虽然UI在滚动时没有锁定,但还是存在一些问题。我遇到了一些重复的问题,并且它们加载到视图中相当缓慢。我创建了一个视频来准确展示这个问题:

https://www.youtube.com/watch?v=nQmAXpbrv1w&feature=youtu.be

我的代码主要参考了这个问题的被接受答案:在Swift上从URL加载/下载图像

以下是我的完整代码。

在我的UICollectionViewController类声明之下,我声明了一个变量:

let API = APIGet()

在UICollectionViewController的viewDidLoad中,我调用:
 override func viewDidLoad() {
    API.getRequest()
    API.delegate = self
}

在我的APIGet类中,我有一个函数叫做getRequest:
  func getRequest() {
let string = "https://api.imgur.com/3/gallery/t/cats"
let url = NSURL(string: string)
let request = NSMutableURLRequest(URL: url!)
request.setValue("Client-ID \(cliendID)", forHTTPHeaderField: "Authorization")
let session = NSURLSession.sharedSession()
let tache = session.dataTaskWithRequest(request) { (data, response, error) -> Void in
    if let antwort = response as? NSHTTPURLResponse {
        let code = antwort.statusCode
        print(code)
        do {

            let json = try NSJSONSerialization.JSONObjectWithData(data!, options: NSJSONReadingOptions.MutableContainers)

            dispatch_async(dispatch_get_main_queue(), { () -> Void in
                if let data = json["data"] as? NSDictionary {
                    if let items = data["items"] as? NSArray {
                        var x = 0
                        while x < items.count {
                            if let url2 = items[x]["link"] {
                                self.urls.append(url2! as! String)


                            }

                            x++
                        }
                        self.delegate?.retrievedUrls(self.urls)

                    }
                }

            })



        }
        catch {

        }
    }
   }
 tache.resume()

 }

上述函数正好完成了它应该做的事情:从端点获取JSON,并从JSON中获取图像的URL。一旦检索到所有URL,它就将它们传回UICollectionViewController。
现在,对于我的UICOllectionViewController中的代码。这是从APIGet类接收URL的函数。
  func retrievedUrls(x: [String]) {
    //
    self.URLs = x
    var y = 0
    while y < self.URLs.count {
        if let checkedURL = NSURL(string: self.URLs[y]) {
            z = y
            downloadImage(checkedURL)

        }
        y++
    }
}

然后,这两个函数负责下载图像:
   func getDataFromUrl(url:NSURL, completion: ((data: NSData?, response: NSURLResponse?, error: NSError? ) -> Void)) {
    NSURLSession.sharedSession().dataTaskWithURL(url) { (data, response, error) in
        completion(data: data, response: response, error: error)
        }.resume()

}


func downloadImage(url: NSURL){
    getDataFromUrl(url) { (data, response, error)  in
        dispatch_async(dispatch_get_main_queue()) { () -> Void in
            guard let data = data where error == nil else { return }

            if let image = UIImage(data: data) {
                self.images.append(image)
                self.collectionView?.reloadData()
            }
        }
    }
}

当图片加载完成后,如果我调用self.collectionView.reloadData(),则每次加载新图片时,集合视图会闪烁并且单元格会移动。但是,如果我根本不调用self.collectionView.reloadData(),那么在图片加载完成后,集合视图永远不会重新加载,单元格保持为空。我该怎么解决这个问题?我希望图片直接加载到它们各自的单元格中,而不会导致集合视图闪烁/闪光或单元格移动,也不会出现临时重复。

我的cellForRowAtIndexPath方法很简单:

  override func collectionView(collectionView: UICollectionView, cellForItemAtIndexPath indexPath: NSIndexPath) -> UICollectionViewCell {
    let cell = collectionView.dequeueReusableCellWithReuseIdentifier("cell1", forIndexPath: indexPath) as! cell1

    if indexPath.row > (images.count - 1) {
        cell.image.backgroundColor = UIColor.grayColor()
    }
    else {
    cell.image.image = images[indexPath.row]
    }


    return cell
}

这是我第一次尝试在iOS中进行网络编程,因此我不想使用库(如alamofire、AFNetworking、SDWebImage等),以便从基础开始学习。谢谢您阅读所有内容!
编辑:以下是我的最终代码,大部分取自下面的答案:
    func retrievedUrls(x: [String]) {
    //
    self.URLs = x
    self.collectionView?.reloadData()
    var y = 0
    while y < self.URLs.count {
        if let checkedURL = NSURL(string: self.URLs[y]) {

            downloadImage(checkedURL, completion: { (image) -> Void in

            })

        }
        y++
    }
}

func getDataFromUrl(url:NSURL, completion: ((data: NSData?, response: NSURLResponse?, error: NSError? ) -> Void)) {
    NSURLSession.sharedSession().dataTaskWithURL(url) { (data, response, error) in
        completion(data: data, response: response, error: error)
        }.resume()

}

func downloadImage(url: NSURL, completion: (UIImage) -> Void) {
    getDataFromUrl(url) { (data, response, error) -> Void in
        dispatch_async(dispatch_get_main_queue()) { () -> Void in
            guard let data = data where error == nil else { return }

            if let image = UIImage(data: data) {
                self.images.append(image)
                let index = self.images.indexOf(image)
                let indexPath = NSIndexPath(forRow: index!, inSection: 0)
                self.collectionView?.reloadItemsAtIndexPaths([indexPath])


            }
        }
    }
}

我的 cellForItemAtIndexPath 没有改变。

1
需要注意的一点是,在回答问题之前,不要创建一个本地变量 y,然后编写 while y < self.URLs.count { // ... },而是尝试使用 for url in self.URLs。这样做的好处是你不再需要处理索引了,并且你可以立即获得所需的对象,而无需再次使用该索引。 - Alex Popov
好的,我在 APIGet 类中的 API 请求函数中也使用了相同的模式,我会在那里进行更改。 - jjjjjjjj
我知道你不想使用库,但当你决定理解概念足够不必重复造轮子时,我强烈推荐SDWebImage用于图像,BrightFutures用于所有其他异步需求 :) - Alex Popov
1个回答

3

我不确定您对闭包等内容的熟悉程度,但是由于您在使用NSURLSession时使用了它们,因此我将假定您对其有一定了解。

在您的函数downloadImage(_:)中,您可以重写它,使其签名为

func downloadImage(url: NSURL, completion: (UIImage) -> Void)

这允许您在调用时决定如何处理该图像。 downloadImage 将被修改为:

func downloadImage(url: NSURL, completion: (UIImage) -> Void) {
  getDataFromUrl(url) { (data, response, error) -> Void in
    dispatch_async(dispatch_get_main_queue()) { () -> Void in
      guard let data = data where error == nil else { return }

      if let image = UIImage(data: data) {
        self.images.append(image)
        completion(image)
      }
    }
  }
}

我们所改变的是将我们的图像传递到新定义的完成块中。
现在,这有什么用呢?`retreivedUrls(_:)`可以通过以下两种方式之一进行修改:直接将图像分配给单元格,或重新加载相应索引的单元格(哪种更容易;直接分配不会导致闪烁,但根据我的经验,重新加载单个单元格也不会)。
func retrievedUrls(x: [String]) {
  //
  self.URLs = x
  var y = 0
  while y < self.URLs.count {
    if let checkedURL = NSURL(string: self.URLs[y]) {
      z = y
      downloadImage(checkedURL, completion: { (image) -> Void in
        // either assign directly:
        let indexPath = NSIndexPath(forRow: y, inSection: 0)
        let cell = collectionView.cellForItemAtIndexPath(indexPath) as? YourCellType
        cell?.image.image = image
        // or reload the index
        let indexPath = NSIndexPath(forRow: y, inSection: 0)
        collectionView?.reloadItemsAtIndexPaths([indexPath])
      })

    }
    y++
  }
}

请注意,我们使用了完成块(completion block),只有在图片下载并添加到images数组后才会调用它(和之前调用collectionView?.reloadData()的时刻相同)。
然而!需要注意的是,在您的实现中,您正在将图像附加到数组中,但可能未保持与URL相同的顺序!如果某些图像没有按照传入URL的顺序下载(这很可能,因为它都是异步的),它们将混淆。我不知道您的应用程序是什么,但假设您希望将图像分配给与给定URL相同的单元格。我的建议是将您的images数组更改为Array<UIImage?>类型,并使用与URL数量相同的nil填充它,然后修改cellForItemAtIndexPath以检查图像是否为nil:如果为nil,则将其分配给UIColor.grayColor(),但如果它不为空,则将其分配给单元格的image。这样可以保持顺序。虽然这不是完美的解决方案,但需要的最少的修改即可实现。

self.collectionView?.reloadItemsAtIndexPaths([indexPath]) 这一行导致程序崩溃。调试器显示y的值为60,indexPath.row的值为60,我的图像数组中仅有一张图片。错误信息显示:“尝试从在更新之前只包含0个项目的第0部分中删除第60项”。考虑到这是第一次循环,而且y++直到reloadAtIndexPath完成后才被调用,因此我认为该值应该为1。 - jjjjjjjj
我认为问题在于reloadItemsAtIndexPaths接受该索引路径,删除其内容,然后重新加载它。如果一开始没有任何内容,就没有东西可删除,因此会抛出错误。 - jjjjjjjj
1
谢谢,我会看一下的。我曾经尝试过使用我在原始帖子中提到的被接受的答案,但它并没有很好地工作。我认为它每次重用单元格时都会执行图像下载,这导致了大量的内存使用,并且也导致了滞后,因为它每次显示单元格时都要执行计算。不过我会看一下你提供的Dropbox链接,谢谢。 - jjjjjjjj

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接