Быстрая прокрутка UICollectionView отображает неправильные изображения в ячейке при асинхронном добавлении новых элементов

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

Я видел вопросы и ответы, связанные с этой проблемой так и раньше, но ни одно из решений не работает для меня. Словарь let items = [[String: Any]]() заполняется после вызова API. Мне нужна ячейка, чтобы удалить изображение из переработанной ячейки. Прямо сейчас возникает неприятный образ" танцующего " эффекта.

вот мой код:

var searchResults = [[String: Any]]()

let cellId = "cellId"

override func viewDidLoad() {
    super.viewDidLoad()

    collectionView = UICollectionView(frame: self.view.frame, collectionViewLayout: layout)
    collectionView.delegate = self
    collectionView.dataSource = self
    collectionView.register(MyCollectionViewCell.self, forCellWithReuseIdentifier: cellId)



func collectionView(_ collectionView: UICollectionView, cellForItemAt indexPath: IndexPath) -> UICollectionViewCell {

    let cell = collectionView.dequeueReusableCell(withReuseIdentifier: cellId, for: indexPath) as! MyCollectionViewCell

    let item = searchResults[indexPath.item]

    cell.backgroundColor = .white

    cell.itemLabel.text = item["title"] as? String

    let imageUrl = item["img_url"] as! String

    let url = URL(string: imageUrl)

    let request = Request(url: url!)

    cell.itemImageView.image = nil

    Nuke.loadImage(with: request, into: cell.itemImageView)

    return cell
}


class MyCollectionViewCell: UICollectionViewCell {

var itemImageView: UIImageView = {
    let imageView = UIImageView()
    imageView.contentMode = .scaleAspectFit
    imageView.layer.cornerRadius = 0
    imageView.clipsToBounds = true
    imageView.translatesAutoresizingMaskIntoConstraints = false
    imageView.backgroundColor = .white
    return imageView
}()


 override init(frame: CGRect) {
    super.init(frame: frame)

 ---------

 }

override func prepareForReuse() {
    super.prepareForReuse()
    itemImageView.image = nil
}

required init?(coder aDecoder: NSCoder) {
    fatalError("init(coder:) has not been implemented")
}    
}

Я изменил загрузку изображения ячейки на следующий код:

cell.itemImageView.image = nil

APIManager.sharedInstance.myImageQuery(url: imageUrl) { (image) in

guard let cell = collectionView.cellForItem(at: indexPath) as? MyCollectionViewCell
                else { return }

cell.itemImageView.image = image

cell.activityIndicator.stopAnimating()
}

вот мой менеджер API.

struct APIManager {

static let sharedInstance = APIManager()

func myImageQuery(url: String, completionHandler: @escaping (UIImage?) -> ()) {

    if let url = URL(string: url) {

        Manager.shared.loadImage(with: url, token: nil) { // internal to Nuke

            guard let image = .value as UIImage? else {
                return completionHandler(nil)
            }

            completionHandler(image)
        }
    }
}

если пользователь прокручивает ограничение содержимого, мой вид коллекции загрузит больше элементов. Это, по-видимому, корень проблемы, когда повторное использование ячеек повторно использует изображения. Другие поля в ячейке, такие как заголовок элемента, также меняются местами по мере появления новых элементов нагруженный.

func scrollViewDidScroll(_ scrollView: UIScrollView) {

    if (scrollView.contentOffset.y + view.frame.size.height) > (scrollView.contentSize.height * 0.8) {

        loadItems()
    }
}

вот моя функция загрузки данных

func loadItems() {

    if loadingMoreItems {
        return
    }

    if noMoreData {
        return
    }

    if(!Utility.isConnectedToNetwork()){

        return
    }

    loadingMoreItems = true

    currentPage = currentPage + 1

    LoadingOverlay.shared.showOverlay(view: self.view)

    APIManager.sharedInstance.getItems(itemURL, page: currentPage) { (result, error) -> () in

        if error != nil {

        }

        else {

            self.parseData(jsonData: result!)
        }

        self.loadingMoreItems = false

        LoadingOverlay.shared.hideOverlayView()
    }
}


func parseData(jsonData: [String: Any]) {

    guard let items = jsonData["items"] as? [[String: Any]] else {
        return
    }

    if items.count == 0 {

        noMoreData = true

        return
    }

    for item in items {

        searchResults.append(item)
    }

    for index in 0..<self.searchResults.count {

        let url = URL(string: self.searchResults[index]["img_url"] as! String)

        let request = Request(url: url!)

        self.preHeatedImages.append(request)

    }

    self.preheater.startPreheating(with: self.preHeatedImages)

    DispatchQueue.main.async {

//        appendCollectionView(numberOfItems: items.count)

        self.collectionView.reloadData()

        self.collectionView.collectionViewLayout.invalidateLayout()
    }
}

3 ответов


когда представление коллекции прокручивает ячейку за пределы, представление коллекции может повторно использовать ячейку для отображения другого элемента. Вот почему вы получаете ячейки из метода с именем dequeueReusableCell(withReuseIdentifier:for:).

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

я рекомендую вам изменить свой Nuke.loadImage(with:into:) метод для закрытия вместо представления изображения:

struct Nuke {

    static func loadImage(with request: URLRequest, body: @escaping (UIImage) -> ()) {
        // ...
    }

}

таким образом, в collectionView(_:cellForItemAt:), вы можете загрузить образ такой:

Nuke.loadImage(with: request) { [weak collectionView] (image) in
    guard let cell = collectionView?.cellForItem(at: indexPath) as? MyCollectionViewCell
    else { return }
    cell.itemImageView.image = image
}

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


попробуйте это:

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

 override func prepareForReuse()
    {
        super.prepareForReuse()
        self.imageView.image = UIImage(named: "DefaultImage"
    }

со страницы проекта Nuke:

Nuke.метод loadImage (with:into:) отменяет предыдущий невыполненный запрос, связанный с целью. Nuke содержит слабую ссылку на цель, когда цель освобождается, связанный запрос автоматически отменяется.

и, насколько я могу судить, Вы правильно используете их пример кода:

func collectionView(_ collectionView: UICollectionView, cellForItemAt indexPath: IndexPath) -> UICollectionViewCell {
    ...
    cell.imageView.image = nil
    Nuke.loadImage(with: url, into: cell.imageView)
    ...
}

вы явно устанавливаете изображение UIImageViews на ноль, поэтому на самом деле должно быть он не может отображать изображение, пока Nuke не загрузит новое изображение в цель.

также Nuke отменит предыдущий запрос. Я сам использую Nuke и не имею этих проблем. Вы уверены, что ваш "измененный код" не является неисправной частью здесь? Для меня все кажется правильным, и я не вижу проблемы, почему это не должно работать.

одной из возможных проблем может быть перезагрузка всего представления коллекции. Мы всегда должны стараться обеспечить лучший пользовательский опыт, поэтому, если вы измените источник данных представления коллекции, попробуйте использовать performBatchUpdates для анимации вставок/обновлений/перемещений / удалений.

хорошая библиотека, которая автоматически может позаботиться об этом, например, Dwifft.