【iOS 13対応】UITableViewでセルの画像を非同期でネットワークから取得する（OSS不使用）

UITableViewのセルにネットワークから画像を取得して表示する場合、
Three20など、外部ライブラリで対処する方が大半なんじゃないかと思っているのですが、
WWDC2019のビデオをみて「なるほどー」と思ったので、
改めて、OSSなしでUITableViewにネットワークから取得した画像を表示するコード例を作成しました。

ちなみにそのビデオは[こちら](https://developer.apple.com/videos/play/wwdc2019/712/)

# 基本

基本的には、`UITableViewDataSource`の`tableView(_:cellForRowAt:)` で非同期での画像取得を行うのですが、この時にUITableViewCellに、`非同期処理をキャンセル可能なオブジェクト`を渡しておいて、
UITableViewCell側は、`prepareForReuse`をオーバーライドしておいて、呼ばれるタイミングでキャンセルを行います。

こうすることによって、
- 意図しないセルに誤った画像が設定される
- ネットワークの無駄な通信

といった問題を抑えることができます。

キャッシュに関しては、URLSessionに任せることにします。

# iOS 13

iOS 13では、Combineに対応したURLSessionがあるので、そちらを使うことにします。
また、iOS 13より加わった、`Low Data Mode`にも対応させます。
前出のビデオのコードを大いに参考しています。

セルに対して`AnyCancellable`を渡すことによってキャンセルに対応しています。

## UITableViewDataSource#tableView(_:cellForRowAt:)

```swift
    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

```swift
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:)

```swift
    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

```swift
class MovieTableViewCell: UITableViewCell {
    
    @IBOutlet weak var thumbnailImageView: UIImageView!
    
    var sessionDataTask:URLSessionDataTask?

    override func prepareForReuse() {
        super.prepareForReuse()
        sessionDataTask?.cancel()
    }
}

```

ギガが減るのを阻止しましょう。