Обработка изображения коллаж для UITableViewCells - метод, который будет масштабироваться для сотен ячеек?

Проблема: я использую художественные ресурсы из музыки, доступ к которым осуществляется через API Apple (все локальные), чтобы создать коллаж из 4 изображений в качестве изображения в ячейке для списка воспроизведения в табличном представлении. Я испробовал несколько методов, которые я расскажу, чтобы довести производительность до приемлемого уровня, но всегда есть какой-то сбой.

Способ 1) Выполнить код обработки, чтобы вернуть изображение с коллажем для каждой ячейки. Это всегда было хорошо для тех немногих плейлистов, которые были у меня на устройстве, но я получал сообщения от активных пользователей, что они не могли прокрутить.

Способ 2) В viewdidload () итерировал по всем спискам воспроизведения и сохранил коллажное изображение и один из UUID в изменяемом массиве, затем извлек изображение для ячеек, используя UUID. Кажется, это работает нормально с задержкой при загрузке вида, но по какой-то причине последующие загрузки занимают больше времени.

Поэтому, прежде чем я попытаюсь устранить неполадки, связанные со способом 2, я хотел бы узнать, не ошибаюсь ли я в том, что делаю это совершенно неправильно? Я посмотрел на GCD и NSCache, и что касается GCD, я не знаю достаточно, чтобы создать правильный шаблон проектирования, чтобы использовать его, если это вообще возможно, поскольку такие вещи, как обновления пользовательского интерфейса и доступ к хранилищу, могут вызывать путь.

import UIKit
import MediaPlayer


class playlists: UITableViewController, UISearchBarDelegate, UISearchControllerDelegate {
...
    var compositedCellImages:[(UIImage, UInt64)] = []
...
    override func viewDidLoad() {
        super.viewDidLoad()

        let cloudFilter:MPMediaPropertyPredicate = MPMediaPropertyPredicate(value: false, forProperty: MPMediaItemPropertyIsCloudItem, comparisonType: MPMediaPredicateComparison.equalTo)
        playlistsQuery.addFilterPredicate(cloudFilter)

        playlistQueryCollections = playlistsQuery.collections?.filter{$0.value(forProperty: MPMediaPlaylistPropertyName) as? String != "Purchased"} as NSArray?

        var tmpArray:[MPMediaPlaylist] = []
        playlists = playlistQueryCollections as! [MPMediaPlaylist]

        for playlist in playlists {
            if playlist.value(forProperty: "parentPersistentID") as! NSNumber! == playlistFolderID {
                tmpArray.append(playlist)
                compositedCellImages.append(playlistListImage(inputPlaylistID: playlist.persistentID))
            }
        }
        playlists = tmpArray
...
}
    // MARK: - Table view data source
...
    override func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell {
        let cell = self.tableView.dequeueReusableCell(withIdentifier: "playlistCell", for: indexPath) as! playlistCell
...
            let currentItem = playlists[indexPath.row]
...
             cell.playlistCellImage.image = compositedCellImages[indexPath.row].0
            }
        }
        return cell
    }
...
func playlistListImage(inputPlaylistID:MPMediaEntityPersistentID) -> (UIImage,UInt64) {
    var playlistData:[MPMediaItem] = []
    var pickedArtwork:[UIImage] = []
    var shuffledIndexes:[Int] = []
    let playlistDetailImage:collageImageView = collageImageView()
    playlistDetailImage.frame = CGRect(x: 0, y: 0, width: 128, height: 128)

    let playlistDataPredicate = MPMediaPropertyPredicate(value: NSNumber(value: inputPlaylistID as UInt64), forProperty: MPMediaPlaylistPropertyPersistentID, comparisonType:MPMediaPredicateComparison.equalTo)
    let playlistDataQuery = MPMediaQuery.playlists()

    let cloudFilter:MPMediaPropertyPredicate = MPMediaPropertyPredicate(value: false, forProperty: MPMediaItemPropertyIsCloudItem, comparisonType: MPMediaPredicateComparison.equalTo)
    playlistDataQuery.addFilterPredicate(cloudFilter)

    playlistDataQuery.addFilterPredicate(playlistDataPredicate)
    playlistData = playlistDataQuery.items!
    playlistData = playlistData.filter{$0.mediaType == MPMediaType.music}

    for (index,_) in playlistData.enumerated() {
        shuffledIndexes.append(index)
    }

   shuffledIndexes.shuffleInPlace()

    for (_,element) in shuffledIndexes.enumerated() {
        if playlistData[element].artwork != nil {
            pickedArtwork.append(playlistData[element].artwork!.image(at: CGSize(width: 64, height: 64))!)
        }
        if pickedArtwork.count == 4 { break }
    }

    while pickedArtwork.count < 4 {
        if pickedArtwork.count == 0 {
            pickedArtwork.append(UIImage(named: "missing")!)
        } else {
            pickedArtwork.shuffleInPlace()
        pickedArtwork.append(pickedArtwork[0])
        }
    }

pickedArtwork.shuffleInPlace()

playlistDetailImage.drawInContext(pickedArtwork, matrixSize: 2)

return ((playlistDetailImage.image)!,inputPlaylistID)
}
...
}
...
class collageImageView: UIImageView {
    var inputImages:[UIImage] = []
    var rows:Int = 1
    var cols:Int = 1

    func drawInContext(_ imageSet: [UIImage], matrixSize: Int) {
        let frameLeg:Int = Int(self.frame.width/CGFloat(matrixSize))
        var increment:Int = 0

        UIGraphicsBeginImageContextWithOptions(self.frame.size, false, UIScreen.main.scale)
        self.image?.draw(in: self.frame)
        for col in 1...matrixSize {
            for row in 1...matrixSize {
                imageSet[increment].draw(in: CGRect(x: (row - 1) * frameLeg, y: (col-1) * frameLeg, width: frameLeg, height: frameLeg))
                increment += 1
            }
        }
        self.image = UIGraphicsGetImageFromCurrentImageContext()
        UIGraphicsEndImageContext()
    }
}

1 ответ

Решение

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

Для примера, если как выполнить отложенную загрузку, см. https://developer.apple.com/library/content/samplecode/LazyTableImages/Introduction/Intro.html

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

Кроме того, вы можете кэшировать результаты вычислений для каждой строки, так что, когда пользователь прокручивает назад и вперед, вы можете сначала проверить кэшированные значения. Также очистите кеш в didReceiveMemoryWarning.

Итак, в tableView(_: UITableView, cellForRowAt: IndexPath)

if <cache contains image for row> {
  cell.playlistCellImage.image = <cached image>
} else if tableView.isDragging && !tableView.isDecelerating {
   let image = <calculate image for row>
   <add image to cache>
   cell.playlistCellImage.image = image
} else {
   cell.playlistCellImage.image = <placeholder image>
}

Затем переопределите методы делегата для представления прокрутки

override func scrollViewDidEndDecelerating(_ scrollView: UIScrollView) {
    loadImagesForOnScreenRows()
}

override func scrollViewDidEndDragging(_ scrollView: UIScrollView, willDecelerate decelerate: Bool) {
    loadImagesForOnScreenRows()
}

И реализовать loadImagesForOnScreenRows()

for path in tableView.indexPathsForVisibleRows {
    let image = <calculate image for row>
    <add image to cache>
    if let cell = tableView.cellForRow(at: path) {
        cell.playlistCellImage.image = image
    }
}

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

ОБНОВЛЕНИЕ - Для вычисления в фоновом потоке.

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

Что-то вроде следующего (непроверенного) кода

func loadImageInBackground(inputPlaylistID:MPMediaEntityPersistentID, completion:@escaping (UIImage, UInt64)) 
{
    let backgroundQueue = DispatchQueue.global(dos: DispatchQoS.QoSClass.background)
backgroundQueue.async {
       let (image,n) = playlistListImage(inputPlaylistID:inputPlaylistID)
       DispatchQueue.main.async {
           completion(image,n)
       }
    }

}

И в tableView(_: UITableView, cellForRowAt: IndexPath) вместо непосредственного вычисления изображения вызовите фоновый метод:

if <cache contains image for row> {
  cell.playlistCellImage.image = <cached image>
} else if tableView.isDragging && !tableView.isDecelerating {
   cell.playlistCellImage.image = <placeholder image>
   loadImageInBackground(...) {
      (image, n) in 
          if let cell = tableView.cellForRow(at:indexPath) {        
             cell.playlistCellImage.image = image
          }
       <add image to cache>
   }
} else {
   cell.playlistCellImage.image = <placeholder image>
}

и аналогичное обновление в loadImagesForOnScreenRows().

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

Другие вопросы по тегам