Swift-изображения изменяются на неправильные изображения при прокрутке после загрузки асинхронного изображения в UITableViewCell

Я пытаюсь асинхронно загружать изображения внутри моей ячейки FriendsTableView (UITableView). Изображения загружаются нормально, но когда я прокручиваю таблицу, изображения будут меняться несколько раз, и неправильные изображения назначаются неправильным ячейкам.

Я пробовал все методы, которые мог найти в StackOverflow, включая добавление тега в raw, а затем его проверку, но это не сработало. Я также проверяю ячейку, которая должна обновляться с помощью indexPath и проверять, существует ли ячейка. Так что я понятия не имею, почему это происходит.

вот мой код:

    func tableView(tableView: UITableView, cellForRowAtIndexPath indexPath: NSIndexPath) -> UITableViewCell {
        let cell = tableView.dequeueReusableCellWithIdentifier("friendCell", forIndexPath: indexPath) as! FriendTableViewCell
        var avatar_url: NSURL
        let friend = sortedFriends[indexPath.row]

        //Style the cell image to be round
        cell.friendAvatar.layer.cornerRadius = 36
        cell.friendAvatar.layer.masksToBounds = true

        //Load friend photo asyncronisly
        avatar_url = NSURL(string: String(friend["friend_photo_url"]))!
        if avatar_url != "" {
                getDataFromUrl(avatar_url) { (data, response, error)  in
                    dispatch_async(dispatch_get_main_queue()) { () -> Void in
                        guard let data = data where error == nil else { return }
                        let thisCell = tableView.cellForRowAtIndexPath(indexPath)
                        if (thisCell) != nil {
                            let updateCell =  thisCell as! FriendTableViewCell
                            updateCell.friendAvatar.image = UIImage(data: data)
                        }
                    }
                }
        }
        cell.friendNameLabel.text = friend["friend_name"].string
        cell.friendHealthPoints.text = String(friend["friend_health_points"])
        return cell
    }

6 ответов


это потому, что UITableView повторно использует ячейки. Загрузка их таким образом заставляет асинхронные запросы возвращаться в разное время и испортить порядок.

Я предлагаю вам использовать некоторую библиотеку, которая облегчит вашу жизнь, как Kingfisher. Он будет загружать и кэшировать изображения для вас. Также вам не придется беспокоиться о асинхронных вызовах.

https://github.com/onevcat/Kingfisher

ваш код будет выглядеть как-то так это:

func tableView(tableView: UITableView, cellForRowAtIndexPath indexPath: NSIndexPath) -> UITableViewCell {
        let cell = tableView.dequeueReusableCellWithIdentifier("friendCell", forIndexPath: indexPath) as! FriendTableViewCell
        var avatar_url: NSURL
        let friend = sortedFriends[indexPath.row]

        //Style the cell image to be round
        cell.friendAvatar.layer.cornerRadius = 36
        cell.friendAvatar.layer.masksToBounds = true

        //Load friend photo asyncronisly
        avatar_url = NSURL(string: String(friend["friend_photo_url"]))!
        if avatar_url != "" {
            cell.friendAvatar.kf_setImageWithURL(avatar_url)
        }
        cell.friendNameLabel.text = friend["friend_name"].string
        cell.friendHealthPoints.text = String(friend["friend_health_points"])
        return cell
    }

на cellForRowAtIndexPath:

1) присвоить значение индекса пользовательской ячейке. Например,

cell.tag = indexPath.row

2) в главном потоке перед назначением изображения проверьте, принадлежит ли изображение соответствующей ячейке, сопоставив ее с тегом.

dispatch_async(dispatch_get_main_queue(), ^{
   if(cell.tag == indexPath.row) {
     UIImage *tmpImage = [[UIImage alloc] initWithData:imgData];
     thumbnailImageView.image = tmpImage;
   }});
});

обновление

приведенный ниже код работал, но позже я переключился на использование Зимородок что удивительно просто.

ОБНОВЛЕНИЯ

Я не хотел использовать стороннюю библиотеку, такую как Alamofire и cell.тэг не работал на меня. Установка начального изображения на ноль сделала трюк для меня. Я также кэшировал изображения. Вот расширение, которое я написал для UIImageView:

let imageCache = NSCache<NSString, UIImage>()

extension UIImageView {

    func downloadImage(from imgURL: String!) {
        let url = URLRequest(url: URL(string: imgURL)!)

        // set initial image to nil so it doesn't use the image from a reused cell
        image = nil

        // check if the image is already in the cache
        if let imageToCache = imageCache.object(forKey: imgURL! as NSString) {
            self.image = imageToCache
            return
        }

        // download the image asynchronously
        let task = URLSession.shared.dataTask(with: url) { (data, response, error) in
            if error != nil {
                print(error!)
                return
            }

            DispatchQueue.main.async {
                // create UIImage
                let imageToCache = UIImage(data: data!)
                // add image to cache
                imageCache.setObject(imageToCache!, forKey: imgURL! as NSString)
                self.image = imageToCache
            }
        }
        task.resume()
    }
}

Вы можете вызвать этот метод как так в cellForRowAt

cell.postImage.downloadImage(from: self.posts[indexPath.row].pathToImage)

файлы source1 файл source2


лучшее решение для этой проблемы у меня есть для Swift 3 или Swift 4 Просто напишите эти две строки

  cell.videoImage.image = nil

 cell.thumbnailimage.setImageWith(imageurl!)

В зависимости от реализации может быть много вещей, которые заставят все ответы здесь не работать (включая мой). Проверка тега не сработала для меня, проверка кэша тоже, у меня есть пользовательский класс фотографий, который несет полное изображение, миниатюру и больше данных, поэтому я должен позаботиться об этом тоже, а не просто предотвратить повторное использование изображения неправильно. Поскольку вы, вероятно, будете назначать изображения в ячейку imageView после их загрузки, вам понадобится к отменить загрузку и сбросить все, что вам нужно на prepareForReuse ()

пример, если вы используете что-то вроде SDWebImage

  override func prepareForReuse() {
   super.prepareForReuse() 

   self.imageView.sd_cancelCurrentImageLoad()
   self.imageView = nil 
   //Stop or reset anything else that is needed here 

}

Если вы подклассы imageview и обрабатывать загрузку самостоятельно убедитесь, что вы настроили способ отменить загрузку до завершения вызывается и вызовите отмена на prepareForReuse()

например

imageView.cancelDownload()

вы можете отменить это из UIViewController тоже. Это на себе или в сочетании с некоторые из ответов, скорее всего, решат эту проблему.


Swift 3

  DispatchQueue.main.async(execute: {() -> Void in

    if cell.tag == indexPath.row {
        var tmpImage = UIImage(data: imgData)
        thumbnailImageView.image = tmpImage
    }
})