Swift 向け Firebase RealtimeDB+Storage の画像読み込みライブラリを作った


最近Firebaseをバックエンドにしたアプリをよく開発しているのですが RealtimeDB, Storage, TableView で問題にぶち当たったので解決のためにライブラリを作りました。
Firebase RealtimeDB, Storage に関しては Salada を利用している前提です。

Firebase RealtimeDB+Storage よる画像読み込みの問題

TableViewCell + ImageView で以下のような問題が発生

  • cellForRowAtIndexPath で Model を取得
  • ImageView に画像を load
  • 読み込み終了前にスクロール
  • cell が reuse される
  • 次の indexPath で別の画像を load
  • 最初の画像が一瞬表示され、その後に適切な画像が表示される
class MyTableViewCell {
    @IBOutlet weak var imageView: UIImageView!

    func configure(id: String) {
        Firebase.User.observeSingle(id: id, type: .value) { user in
            guard let user: Firebase.User = user else { return }
            if let profileImageRef = user.profileImage.ref {
                imageView.sd_setImage(ref)
            }
        }
    }

    func prepareForReuse() {
        imageView.image = nil
    }
}

この問題はセル内での2重の非同期通信によって発生し、以下のような図で表せます。

解決策

ここで発生している問題は以下の2点です。

  • cell の reuse 時に画像ダウンロードをキャンセルしていない
  • imageViewの image を set する時に、正しい画像かどうかを判定していない

まず、cell の reuse 時に画像ダウンロードをキャンセルしていないから解決していきます。
これは UIImageView の extention に cancelLoading() suspendLoading() を実装しました。
これらのメソッドのどちらかを呼ぶことで画像の読み込みを停止、キャンセルできます。
次に imageView の image を set する時に、正しい画像かどうかを判定していない に関しては load 時に Bool を返り値に取るブロックを渡すことで、その条件を満たした場合のみ画像を set するようにするメソッド load(_ storageReference: StorageReference, shouldSetImageConditionBlock: @escaping (() -> Bool) = { return true } ) を UIImageView の extension に実装しました。

これらによって最終的なセルの実装はこんな感じになります。

class MyTableViewCell {
    @IBOutlet weak var imageView: UIImageView!
    private var userID: String?

    func configure(id: String) {
        Firebase.User.observeSingle(id: id, type: .value) { [weak self] user in
            guard let `self` = self else { return }
            guard let user: Firebase.User = user else { return }
            if let profileImageRef = user.profileImage.ref {
                imageView.load(profileImageRef) { self.userID == user.ID }
            }
        }
    }

    override func prepareForReuse() {
        super.prepareForReuse()
        userID = nil
        imageView.suspendLoading()
        imageView.image = nil
    }
}

ここまでやれば非同期の重なる TableViewCell での画像読み込みで古い画像でチラついたりすることはなくなります。ヤッター。

今回実装したライブラリ

ここにあります

pod 'ImageStore'

して使ってください。
FirebaseStorage は Pod の dependency に入れられないので Example/ImageStore/ 以下にある UIImageView+FirebaseStorage.swift ImageStore+FirebaseStorage.swift をプロジェクトに入れると幸せになれます。
あとは README.md とこの記事を読めばなんとなく使い方が分かると思います。
またバグ等ありましたらぜひ issue や PR 投げていただけると幸いです。