UITableViewのセルにネットワークから画像を取得して表示する場合、
Three20など、外部ライブラリで対処する方が大半なんじゃないかと思っているのですが、
WWDC2019のビデオをみて「なるほどー」と思ったので、
改めて、OSSなしでUITableViewにネットワークから取得した画像を表示するコード例を作成しました。
ちなみにそのビデオはこちら
基本
基本的には、UITableViewDataSource
のtableView(_:cellForRowAt:)
で非同期での画像取得を行うのですが、この時にUITableViewCellに、非同期処理をキャンセル可能なオブジェクト
を渡しておいて、
UITableViewCell側は、prepareForReuse
をオーバーライドしておいて、呼ばれるタイミングでキャンセルを行います。
こうすることによって、
- 意図しないセルに誤った画像が設定される
- ネットワークの無駄な通信
といった問題を抑えることができます。
キャッシュに関しては、URLSessionに任せることにします。
iOS 13
iOS 13では、Combineに対応したURLSessionがあるので、そちらを使うことにします。
また、iOS 13より加わった、Low Data Mode
にも対応させます。
前出のビデオのコードを大いに参考しています。
セルに対してAnyCancellable
を渡すことによってキャンセルに対応しています。
UITableViewDataSource#tableView(_:cellForRowAt:)
func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell {
let object = fetchedResultsController.object(at: indexPath)
let cell = tableView.dequeueReusableCell(withIdentifier: "identifier", for: indexPath) as! TableViewCell
var request = URLRequest(url: object.imageUrl!)
request.allowsConstrainedNetworkAccess = false
cell.cancelable = URLSession.shared.dataTaskPublisher(for: request)
.tryCatch({ error -> URLSession.DataTaskPublisher in
guard error.networkUnavailableReason == .constrained else {
throw error
}
return URLSession.shared.dataTaskPublisher(for: object.smallImageUrl!)
})
.tryMap({ data,response -> UIImage in
guard let image = UIImage(data: data) else {
throw URLError(.resourceUnavailable)
}
return image
})
.retry(1)
.replaceError(with: UIImage(named: "no_image"))
.receive(on: DispatchQueue.main)
.assign(to: \.thumbnailImageView.image, on: cell)
return cell
}
UITableViewCell
class TableViewCell: UITableViewCell {
@IBOutlet weak var thumbnailImageView: UIImageView!
var cancelable:AnyCancellable?
override func prepareForReuse() {
super.prepareForReuse()
cancelable?.cancel()
}
}
iOS 12以下
iOS 12以下では、直接URLSessionDataTask
を使います。
UITableViewDataSource#tableView(_:cellForRowAt:)
func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell {
let object = fetchedResultsController.object(at: indexPath)
let cell = tableView.dequeueReusableCell(withIdentifier: "identifier", for: indexPath) as! MovieTableViewCell
let sessionDataTask = URLSession.shared.dataTask(with: object.imageUrl!, completionHandler: { (data, urlResponse, error) in
if let data = data {
DispatchQueue.main.async {
cell.thumbnailImageView.image = UIImage(data: data) ?? UIImage(named: "no_image")
}
} else {
DispatchQueue.main.async {
cell.thumbnailImageView.image = UIImage(named: "no_image")
}
}
})
cell.sessionDataTask = sessionDataTask
sessionDataTask.resume()
return cell
}
UITableViewCell
class MovieTableViewCell: UITableViewCell {
@IBOutlet weak var thumbnailImageView: UIImageView!
var sessionDataTask:URLSessionDataTask?
override func prepareForReuse() {
super.prepareForReuse()
sessionDataTask?.cancel()
}
}
ギガが減るのを阻止しましょう。