開発環境

> xcodebuild -version
Xcode 13.1
Build version 13A1030d

はじめに

何かお題を作って、それに対しどのようにアプローチしていくかを記事として残す。

今回は前回の課題になっていた、一回目の表示をスムーズにするために先読みを実装する。

先読み

import UIKit

final class LoadImagesViewController: ComponentBaseViewController {

    @IBOutlet weak var collectionView: UICollectionView! {
        didSet {
            collectionView.dataSource = self
            collectionView.register(R.nib.loadImagesCollectionViewCell)
            configureFlowLayout()
        }
    }

    struct ViewControllerModel {
        let thumbnailImages: [UIImage]
    }

    private var viewControllerModel: ViewControllerModel?
    var presenter: LoadImagesPresenter!
    private let cellCount = 300

    override func viewDidLoad() {
        super.viewDidLoad()
        configureNavigationItem(navigationTitle: "018 LoadImages")
        collectionView.isHidden = true
        presenter.getImages()
    }

}

extension LoadImagesViewController: UICollectionViewDataSource {

    func collectionView(_ collectionView: UICollectionView, numberOfItemsInSection section: Int) -> Int {
        return cellCount
    }

    func collectionView(_ collectionView: UICollectionView, cellForItemAt indexPath: IndexPath) -> UICollectionViewCell {
        guard let viewControllerModel = viewControllerModel else {
            return UICollectionViewCell()
        }

        let cell = collectionView.dequeueReusableCell(withReuseIdentifier: R.reuseIdentifier.loadImagesCollectionViewCell,
                                                      for: indexPath)!
        let index = indexPath.row % viewControllerModel.thumbnailImages.count
        let image = viewControllerModel.thumbnailImages[index]
        cell.setup(image: image)
        return cell
    }

    private func configureFlowLayout() {
        let layout = UICollectionViewFlowLayout()
        layout.itemSize = .init(width: collectionView.frame.width,
                                height: collectionView.frame.height)
        layout.scrollDirection = .horizontal
        collectionView.collectionViewLayout = layout
    }

}

extension LoadImagesViewController: LoadImagesPresenterOutput {

    func updateViewControllerModel(_ viewControllerModel: ViewControllerModel) {
        self.viewControllerModel = viewControllerModel
        collectionView.isHidden = false
        collectionView.reloadData()
    }

}

 

import UIKit

protocol LoadImagesPresenter {
    init(output: LoadImagesPresenterOutput)
    func getImages()
}

final class LoadImagesPresenterImplement: LoadImagesPresenter {

    private weak var output: LoadImagesPresenterOutput?
    private var imagesCacher: [URL: UIImage] = [:]
    private let urls: [URL] = [
        URL(string: "https://placehold.jp/7276c4/ffffff/1000x2000.png?text=1000%20%C3%97%202000")!,
        URL(string: "https://placehold.jp/a4b562/ffffff/1000x2000.png?text=1000%20%C3%97%202000")!,
        URL(string: "https://placehold.jp/b56262/ffffff/1000x2000.png?text=1000%20%C3%97%202000")!,
        URL(string: "https://placehold.jp/b262b5/ffffff/1000x2000.png?text=1000%20%C3%97%202000")!,
        URL(string: "https://placehold.jp/6297b5/ffffff/1000x2000.png?text=1000%20%C3%97%202000")!,
        URL(string: "https://raw.githubusercontent.com/tokizuoh/Pendula/feature/%23104/Pendula/View/Component/018_LoadImages/Image/sky.jpeg")!
    ]

    init(output: LoadImagesPresenterOutput) {
        self.output = output
    }

    func getImages() {
        let images: [UIImage] = urls.compactMap {
            return getImage(url: $0)
        }

        output?.updateViewControllerModel(.init(thumbnailImages: images))
    }

    private func getImage(url: URL) -> UIImage? {
        if let image = imagesCacher[url] {
            return image

        } else {
            let image = fetchImage(url: url)
            imagesCacher[url] = image
            return image
        }
    }

    private func fetchImage(url: URL) -> UIImage? {
        guard let data = try? Data(contentsOf: url) else {
            return nil
        }

        return .init(data: data)
    }

}

protocol LoadImagesPresenterOutput: AnyObject {
    func updateViewControllerModel(_ viewControllerModel: LoadImagesViewController.ViewControllerModel)
}

今回からは処理の責務分割のためにMVPを導入。
画像を読み込み終えてから UICollectionView を表示するようにした。

gif

左: before, 右: after

 

人力キャプチャのため、正確な比較はできないが、

  • 左は空の画像のfetchに時間がかかっているため、空の画像読み込み前のスクロールがスムーズにできていない
  • 右は画像を全て取得してから UICollectionView を表示しているため、スクロールがスムーズにできている

おわりに

3記事でステップを踏んで UICollectionViewCell の読み込みの高速化を考えた。

サードパーティの画像読み込みライブラリ(Nukeなど)を使う必要性を感じないのでこれから調べる。

本記事のシリーズ