Почему прокрутка настолько изменчива в этом табличном представлении? - PullRequest
0 голосов
/ 03 февраля 2020

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

import UIKit

class BMContentCell: UITableViewCell
{
    private var imageCache: NSCache<NSString, UIImage>? // key: string url pointing to the image on the internet, value: UIImage object
    private let kCellContentFormat = "VOD: %@ | DRM: %@ | Auth: %@"
    private let kPosterContent = "poster"

    private var contentImageView: UIImageView =
    {
        let imageView = UIImageView()
        imageView.translatesAutoresizingMaskIntoConstraints = false
        imageView.contentMode = .scaleAspectFit
        imageView.layer.cornerRadius = 15
        imageView.clipsToBounds = true
        imageView.layer.borderColor = UIColor.clear.cgColor
        imageView.layer.borderWidth = 1
        return imageView
    }()
    private let contentTitle: UILabel =
    {
        let label = UILabel()
        label.font = UIFont.systemFont(ofSize: 32)
        label.textColor = UIColor.black
        label.translatesAutoresizingMaskIntoConstraints = false
        return label
    }()
    private let contentSubtitle: UILabel =
    {
        let label = UILabel()
        label.font = UIFont.systemFont(ofSize: 26)
        label.textColor = UIColor.black
        label.translatesAutoresizingMaskIntoConstraints = false
        return label
    }()
    private let contentAttributes: UILabel =
    {
        let label = UILabel()
        label.backgroundColor = .white
        label.font = UIFont.systemFont(ofSize: 20)
        label.textColor = UIColor.black
        label.translatesAutoresizingMaskIntoConstraints = false
        return label
    }()

    override func awakeFromNib()
    {
        super.awakeFromNib()
    }
    override func setSelected(_ selected: Bool, animated: Bool)
    {
        super.setSelected(selected, animated: animated)
    }

    override init(style: UITableViewCell.CellStyle, reuseIdentifier: String?)
    {
        super.init(style: style, reuseIdentifier: reuseIdentifier)
        backgroundColor = .white
        addSubview(contentImageView)
        addSubview(contentTitle)
        addSubview(contentSubtitle)
        addSubview(contentAttributes)
        contentImageView.anchor(
            top: self.topAnchor,
            leading: self.leadingAnchor,
            bottom: self.bottomAnchor,
            trailing: contentTitle.leadingAnchor,
            padding: .init(top: 0, left: 0, bottom: 0, right: 0),
            size: .init(width: 180, height: 180)
        )
        contentTitle.anchor(
            top: self.topAnchor,
            leading: contentImageView.trailingAnchor,
            bottom: contentSubtitle.topAnchor,
            trailing: self.trailingAnchor,
            size: CGSize.init(width: 0, height: 60)
        )
        contentSubtitle.anchor(
            top: contentTitle.bottomAnchor,
            leading: contentImageView.trailingAnchor,
            bottom: contentAttributes.topAnchor,
            trailing: self.trailingAnchor,
            padding: .zero,
            size: CGSize.init(width: 0, height: 60)
        )
        contentAttributes.anchor(
            top: contentSubtitle.bottomAnchor,
            leading: contentImageView.trailingAnchor,
            bottom: self.bottomAnchor,
            trailing: self.trailingAnchor,
            padding: .zero,
            size: CGSize.init(width: 0, height: 60)
        )
    }

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

    internal func configureCell(content: BMContent)
    {
        self.contentTitle.text = content.media.name
        self.contentSubtitle.text = content.name
        self.contentAttributes.text = self.contentCellAttributes(
            String(!content.isLiveStream()),
            String(content.isDrm),
            String(content.authentication.required)
        )

        var posterImageContent = content.media.images.filter { $0.type == kPosterContent }
        DispatchQueue.global(qos: .userInitiated).async
        {
            if posterImageContent.count > 1
            {
                posterImageContent.sort { $0.url < $1.url }
                self.loadImageURLFromCache(imageURLStr: posterImageContent[0].url)
            }
            else if posterImageContent.count == 1
            {
                self.loadImageURLFromCache(imageURLStr: posterImageContent[0].url)
            }
        }
    }

    private func contentCellAttributes(_ isLiveStream: String, _ isDRM: String, _ isAuthRequired: String) -> String
    {
        return String(format: kCellContentFormat, isLiveStream, isDRM, isAuthRequired)
    }

    private func loadImageURLFromCache(imageURLStr: String)
    {
        // check if the url (key) exists in the cache
        if let imageFromCache = imageCache?.object(forKey: imageURLStr as NSString)
        {
            // the image exists in cache, load the image and return
            DispatchQueue.main.async {self.contentImageView.image = imageFromCache as UIImage}
            return
        }
        guard let url = URL(string: imageURLStr) else
        {
            // if the url is not valid, load a blank UIImage
            self.imageCache?.setObject(UIImage(), forKey: imageURLStr as NSString)
            DispatchQueue.main.async {self.contentImageView.image = UIImage()}
            return
        }
        let session = URLSession.shared     
        let task = session.dataTask(with: url)
        {
            data, response, error in
                if let data = data
                {
                    guard let imageFromURL = UIImage(data: data) else
                    {
                        // if we cannot create an image from the data for some reason, load a blank UIImage
                        let placeholderImage = UIImage()
                        self.imageCache?.setObject(placeholderImage, forKey: imageURLStr as NSString)
                        DispatchQueue.main.async {self.contentImageView.image = placeholderImage}
                        return
                    }
                    // load the image and set the key and value in the cache
                    DispatchQueue.main.async {self.contentImageView.image = imageFromURL}
                    self.imageCache?.setObject(imageFromURL, forKey: imageURLStr as NSString)
                }
        }
        task.resume()
    }
}

Если вы посмотрите на метод configureCell, он ищет URL-адрес изображения (который является ключом для объекта NSCache в этом классе) и проверяет кэш, если этот URL-адрес уже существует. Если это так, он просто загружает UIImage, в противном случае он идет и извлекает его из inte rnet.

Контроллер представления, в котором находится табличное представление, называется BMContentViewController. Метод cellForRowAt внутри этого класса выглядит следующим образом:

func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell
    {
        let cell = tableView.dequeueReusableCell(withIdentifier: kReuseIdentifier, for: indexPath) as! BMContentCell
        if self.viewModel.currentContent.count > 0
        {
                cell.configureCell(content: self.viewModel.currentContent[indexPath.row])
        }
        return cell
    }

Каждый раз, когда я прокручиваю вверх или вниз в своем табличном представлении в приложении tvOS, это очень изменчиво. И также кажется, что изображения изначально кажутся неправильными для любого данного телешоу / движения ie, и после нескольких миллисекунд обновляется правильное изображение.

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

Спасибо!

1 Ответ

1 голос
/ 03 февраля 2020

Вы определенно на правильном пути. Причина того, что ваши изображения отображаются неправильно в течение полсекунды, заключается в том, что ячейки используются повторно при прокрутке, и, таким образом, они сохраняют последнее состояние, в котором они «подготовлены». Обычно это не будет проблемой, но в вашем configureCell метод, вы помещаете loadImageURLFromCache в поток, которого нет в основной очереди, и затем каждое обновление пользовательского интерфейса (правильно) выполняется в основной очереди. Это поведение async и является причиной, по которой для обновления изображения требуется доля. На мой взгляд, вам не нужно DispatchQueue.global(qos: .userInitiated).async вообще. Так как ваш кеш-объект не использует асинхронный поиск, вы можете безопасно установить изображение для imageView в текущем потоке без включения в основной поток асинхронного вызова c. Кроме того, если вы прокручиваете много изображений, вам действительно следует рассмотреть возможность использования библиотеки кеша, которая записывает на диск и имеет методы очистки, так как использование вашей памяти будет только расти и расти. Я также рекомендовал бы не иметь "висячего возврата" в середине вашего loadImageURLFromCache метода. Я немного подправил ваш метод. Я не проверил, правильно ли он напечатан или компилируется, поэтому это может быть не решение для вставки и сборки.

private func loadImageURLFromCache(imageURLStr: String)
{
    // check if the url (key) exists in the cache
    if let imageFromCache = imageCache?.object(forKey: imageURLStr) as? UIImage
    {
        // the image exists in cache, load the image and return
        self.contentImageView.image = imageFromCache
        return
    } else if let url = URL(string: imageURLStr) {
        loadImageFromURL(url)
    } else {
        loadBlankImage()
    }
}

private func loadImageFromURL(_ url: URL) {
    let session = URLSession.shared     
        let task = session.dataTask(with: url)
        {
            [weak self] (data, response, error) in
                if let data = data
                {
                    guard let imageFromURL = UIImage(data: data) else
                    {
                        // if we cannot create an image from the data for some reason, load a blank UIImage
                        self?.loadBlankImage()
                        return
                    }
                    // load the image and set the key and value in the cache
                    DispatchQueue.main.async {self?.contentImageView.image = imageFromURL}
                    self?.imageCache?.setObject(imageFromURL, forKey: imageURLStr as NSString)
                }
        }
        task.resume()
}

private func loadBlankImage() {
    contentImageView.image = UIImage()
}
...