TableViewに紐付く形でUIImageViewを設置

こんな感じで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が行われることはなくなった。