Swiftで Image View の読み込み中表示を作成する


# はじめに
ImageViewを使っているとき、画像データは非同期で取得して後からセットしたいという場面があると思います。
画像を取得しにいっている間は読み込み中であることがわかりやすいようローディング表示がつけたい、ということでUIImageViewのExtensionとしてActivity Indicatorを内蔵したカスタムクラスを作成しました。


## デモ動画
![output.gif](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3410763/0057026a-eba2-bb07-e93d-29a7dc08a096.gif)



# 仕様
- メソッドを呼ぶとローディング表示が出てくるようにする。
- 画像の更新を監視し、画像がセットされたらローディング表示を非表示にする。
- 画像がセットされない状態で時間が経過したら別の画像(タイムアウト画像)を表示する機能も用意する。

## コード
以下のコードをコピペすればそのまま使えます。
使用例は[こちら(GitHub)](https://github.com/Ito-Yosuke/ImageLoadingViewDemo)

```Swift
import UIKit

class ImageLoadingView: UIImageView {
    // ローディング表示のビュー
    var activityIndicatorView = UIActivityIndicatorView(style: .large)
    
    // 画像なしの場合表示するタイムアウト画像
    var noImage: UIImage?
    
    // タイムアウト後に実行するブロック
    private var workItem: DispatchWorkItem?
    
    // UIImage
    override var image: UIImage? {
        didSet {
            guard image != nil else { return }
            
            // imageがセットされたらActivity Indicatorを消す
            activityIndicatorView.stopAnimating()
        }
    }
    
    override init(frame: CGRect) {
        super.init(frame: frame)
        configure()
    }
    
    required init?(coder: NSCoder) {
        super.init(coder: coder)
    }
    
    override func awakeFromNib() {
        super.awakeFromNib()
        configure()
    }
    
    /// 初期化処理
    private func configure() {
        guard image == nil else {
            return
        }
        
        // activityIndicatorViewを追加
        activityIndicatorView.frame.size = self.frame.size
        activityIndicatorView.hidesWhenStopped = true
        self.addSubview(activityIndicatorView)
    }
    
    /// ActivityIndicatorを表示する場合呼ぶメソッド
    /// - Parameters:
    ///   - timeout: ActivityIndicatorを消してnoImageを表示するまでのduration。0を指定した場合はtimeoutしない。デフォルトは10.0
    ///   - noImage: タイムアウト画像として表示する画像。デフォルトはnil
    public func showIndicator(timeout: Double = 10.0, noImage: UIImage? = nil) {
        guard image == nil else { return }
        
        self.noImage = noImage
        
        // ローディング表示の開始
        activityIndicatorView.startAnimating()
        
        // タイムアウト時の処理の初期化
        workItem?.cancel()
        
        // タイムアウト時の処理
        workItem = DispatchWorkItem { [weak self] in
            // すでに画像が差し込まれているなら上書きしない
            guard self?.image == nil else { return }
            
            // タイムアウト画像をセット
            self?.image = self?.noImage ?? UIImage()
        }
        
        // タイムアウト処理の予約
        if timeout != 0 {
            DispatchQueue.main.asyncAfter(deadline: .now() + timeout, execute: workItem!)
        }
    }
}
```

## 説明
### 初期化処理
```Swift
    private func configure() {
        guard image == nil else {
            return
        }
        
        // activityIndicatorViewを追加
        activityIndicatorView.frame.size = self.frame.size
        activityIndicatorView.hidesWhenStopped = true
        self.addSubview(activityIndicatorView)
    }
```
共通処理の初期化時に走るコードをまとめた部分。
コードから呼び出したときだけでなくstoryboardから設定したときにも動作するように、init(frame: CGRect) だけでなく awakeFromNib()にも差し込んでいる。

### ActivityIndicator表示とタイムアウト設定
```Swift
    public func showIndicator(timeout: Double = 10.0, noImage: UIImage? = nil) {
        guard image == nil else { return }
        
        self.noImage = noImage
        
        // ローディング表示の開始
        activityIndicatorView.startAnimating()
        
        // タイムアウト時の処理の初期化
        workItem?.cancel()
        
        // タイムアウト時の処理
        workItem = DispatchWorkItem { [weak self] in
            // すでに画像が差し込まれているなら上書きしない
            guard self?.image == nil else { return }
            
            // タイムアウト画像をセット
            self?.image = self?.noImage ?? UIImage()
        }
        
        // タイムアウト処理の予約
        if timeout != 0 {
            DispatchQueue.main.asyncAfter(deadline: .now() + timeout, execute: workItem!)
        }
    }
```
activityIndicatorのアニメーション開始とタイムアウト処理の開始をしている。
時間の管理方法はTimerを利用するなど他の方法もあると思うが、今回はTableView内で複数表示されることも想定してDispatchQueueのasyncAfter(deadline:)を利用している。
タイムアウト時の処理についても同様に、TableViewCellで使用することを念頭に置き初期化をこの部分に記述している。

### UIImage更新の監視
```Swift
    override var image: UIImage? {
        didSet {
            guard image != nil else { return }
            
            // imageがセットされたらActivity Indicatorを消す
            activityIndicatorView.stopAnimating()
        }
    }
```
UIImageView.imageをoverrideし、didSetで更新を監視している。
imageがセットされたらActivityIndicatorのアニメーションを停止する。


## 参考文献
https://developer.apple.com/documentation/uikit/uiactivityindicatorview