[swift][iOS]RxTableViewに紐付く画像をlazy loadingする

## TableViewに紐付く形でUIImageViewを設置
<img width="408" alt="スクリーンショット 2019-03-18 16.34.50.png" src="https://qiita-image-store.s3.amazonaws.com/0/214829/97729f51-50b6-902f-f2b1-db71cc168967.png">

こんな感じでUITableViewとcell、UIImageViewを設置。

## lazy loadingを実装
https://stackoverflow.com/questions/28694645/how-to-implement-lazy-loading-of-images-in-table-view-using-swift
↑を参考に、swiftのバージョンを合わせて実装。

```.swift
extension UIImageView {
    func downloadImageFrom(link: URL, contentMode: UIViewContentMode) {
        URLSession.shared.dataTask( with: link, completionHandler: { (data, _, _) -> Void in
            DispatchQueue.main.async {
                    self.contentMode = contentMode
                    if let data = data { self.image = UIImage(data: data) }
                }
            }
        ).resume()
    }
}
```

現時点で参考元と異なる点は、linkをstringではなくURLで貰っている点ぐらい。

dataSource周りは以下のように実装。

```.swift
var photoURLs: [URL]! = [] // cellに表示したい画像URLの配列

private func configureData() {
    let dataSource = RxTableViewSectionedReloadDataSource<SectionData>(
        configureCell: { (_, tableView, indexPath, item) in
            let cell: HogeCell = (tableView.dequeueReusableCell(
                withIdentifier: "HogeCell",
                for: indexPath
                ) as? HogeCell)!

            cell.imageView.image = UIImage(named: "NoImage") // NoImageというLoading中を示す画像
            // ここでcellごとにlazy loadingを行う
            cell.imageView.downloadImageFrom(
                link: self.photoURLs[indexPath.row],
                contentMode: UIViewContentMode.scaleAspectFit,
            )
            return cell
    }, canEditRowAtIndexPath: { (_, _) in
        return true
    }, canMoveRowAtIndexPath: { (_, _) in
        return true
    }
    )

    self.imageItemsRelay.map {
        return [SectionData(header: "section1", items: $0)]
        }.bind(to: tableView.rx.items(dataSource: dataSource))
        .disposed(by: rx.disposeBag)

    self.tableView.rx.itemDeleted
        .subscribe(onNext: { [weak self] indexPath in
            guard let `self` = self else { return }
            // 何か処理
        }).disposed(by: rx.disposeBag)

    self.tableView.rx.itemMoved
        .subscribe(onNext: { [weak self] sourceIndexPath, destinationIndexPath  in
            // 何か処理
        }).disposed(by: rx.disposeBag)
}
```

## 問題点
これだとcellが画面外に出るとcellが再レンダリングされてしまい、その度にlazy loadingが走って画像を取得しに行ってしまう。(dequeueReusableCell使ってるのに。。。)
一度表示した画像はそのままに表示しておきたい。

## 原因
- https://github.com/RxSwiftCommunity/RxDataSources より
- dataSourceとして利用している`RxTableViewSectionedReloadDataSource`がSection単位でreloadData()をして表示内容を更新するようになっているため画像の再取得が行われてしまっている模様
 
## 対策
- `RxTableViewSectionedAnimatedDataSource`を使う
- 初回にlazy loadingでDLしてきた画像を持っておいて、2回目以降はDLしてきた画像を使う

## `RxTableViewSectionedAnimatedDataSource`を使ってみる
- `RxTableViewSectionedAnimatedDataSource`は変化のあったcellだけ変更がかかるみたい
- 置き換えてみたがダメだった
- `RxTableViewSectionedAnimatedDataSource`内で使われている、古い配列と新しい配列を比較するアルゴリズム`differencesForSectionedView`で比較ありと言われているっぽい？
- アルゴリズムの中までは見れていないが、一旦こっちは諦めた

## 初回にlazy loadingでDLしてきた画像を持っておいて、2回目以降はDLしてきた画像を使う
タイトルのまま。
以下にような辞書を用意しておく。

```.swift
var downloadedUIimages: [Int64: UIImage] = [:]
```

Int64は画像を一意で識別できるIDとか。
無いならURLでもいいかも。

```.swift
var photoURLs: [URL]! = []
var downloadedUIimages: [Int64: UIImage] = [:]

private func configureData() {
    let dataSource = RxTableViewSectionedReloadDataSource<SectionData>(
        configureCell: { (_, tableView, indexPath, item) in
            let cell: HogeCell = (tableView.dequeueReusableCell(
                withIdentifier: "HogeCell",
                for: indexPath
                ) as? HogeCell)!            
           // DLしようとする画像が既にdownloadedUIimagesに存在していれば再利用する
            if let _downloadedImage = self.downloadedUIimages[item.imageId] {
                cell.imageView.image = _downloadedImage
            }
            else {
                cell.imageView.image = UIImage(named: "NoImage")
                cell.imageView.downloadImageFrom(
                    link: self.photoURLs[indexPath.row],
                    contentMode: UIViewContentMode.scaleAspectFit,
                    // 完了時にdownloadedUIimagesにimageIDとUIImageを紐づけて持っておく
                    completion: {
                        self.downloadedUIimages[item.imageId] = cell.imageView.image
                })
            }
            return cell
    }, canEditRowAtIndexPath: { (_, _) in
        return true
    }, canMoveRowAtIndexPath: { (_, _) in
        return true
    }
    )

    self.imageItemsRelay.map {
        return [SectionData(header: "section1", items: $0)]
        }.bind(to: tableView.rx.items(dataSource: dataSource))
        .disposed(by: rx.disposeBag)

    self.tableView.rx.itemDeleted
        .subscribe(onNext: { [weak self] indexPath in
            // 何か処理
        }).disposed(by: rx.disposeBag)

    self.tableView.rx.itemMoved
        .subscribe(onNext: { [weak self] sourceIndexPath, destinationIndexPath  in
            // 何か処理
            // 削除したならdownloadedUIimagesからも削除してもいいかもしれない
        }).disposed(by: rx.disposeBag)
}

extension UIImageView {
    func downloadImageFrom(link: URL, contentMode: UIViewContentMode, completion: (() -> Void)? = nil) {
        URLSession.shared.dataTask( with: link, completionHandler: { (data, _, _) -> Void in
            DispatchQueue.main.async {
                    self.contentMode = contentMode
                    if let data = data { self.image = UIImage(data: data) }
                    completion?()
                }
            }
        ).resume()
    }
}
```

上記のようにすることで、スクロールしてcellが画面外に行っても画像の再DLが行われることはなくなった。