ネットワーク上から画像データをダウンロードするようなアプリを作成する場合、そのデータは再利用する可能性が高いデータのことが多いと思います。
再利用されるデータの場合、毎回同じデータをダウンロードしていると、通信時間がかかるだけでなく、パケット料も嵩んでしまい、ユーザに優しいアプリとは言えなくなってしまいます。
したがって、画像データについては「キャッシュ」を行い、一度本体に保存されたデータであればそれを再利用するのが適切な方法の一つと言えますが、キャッシュ機能を備えたフレームワークの多くは http 経由でダウンロードしたファイルを自動的にキャッシュする機能が大半です。
今回は、画像データはクラウド向けフレームワーク(具体的にはニフクラ mobile backend)のAPIでダウンロードし、キャッシュ機能だけ使ってみたかったので、その機能を実現できるようなフレームワークとして Kingfisher を使ってみることにしました。
標準フレームワークを使ったキャッシュ
実は、キャッシュ機能そのものであれば、外部フレームワークをつかうまでもなく、標準フレームワークを使って実現することが可能です。
- NSCache
A mutable collection you use to temporarily store transient key-value pairs that are subject to eviction when resources are low.
キャッシュ用メモリが他のアプリの駆動に影響を与えるような場合には、自動的に不必要と思われるキャッシュをメモリから削除する上、delegate を使えば削除されるアイテムを事前に知ることも可能です。
しかし、 NSCache はあくまでもオンラインのキャッシュであり、永続化させることはできません。つまり、キャッシュ結果をディスクに書き込むことによって、アプリを次回以降再起動させたときにも使い回すようなことはそのままでは実現することができません。
Kingfisher を使ったキャッシュ
一方、Kingfisher を使えば、メモリキャッシュを行うと同時にディスクへの書き込みも行ってくれるため、アプリ再起動後も以前のキャッシュを再利用することが可能となります。
Kingfisher そのものは、GitHub にもある通り「a powerful, pure-Swift library for downloading and caching images from the web」と、Webからのコンテンツダウンロードとキャッシングのための、強力なSwift純正ライブラリなのですが、任意のデータをキャッシュするだけの目的でも使うことが可能です。
使い方も非常に簡単で、基本的にはチートシート(Cheat sheet)にしたがって使うだけなのですが、せっかくなので使い方をメモしておきたいと思います。
フレームワークの導入
フレームワークの導入については、みなさんの環境によって違ってきますので、ここでは詳細は省きたいと思います。
導入方法についても公式ページに詳細が記載されています。
私の場合には Cocapods を使って管理していますので、 Podfile に必要な項目を追加し、
1 |
pod install |
するだけです。
また、キャッシュ機能を使いたい .swift ファイルの中に
1 |
import Kingfisher |
の一行を記入しておきます。
キャッシュのためのシングルトンの作成
Kingfisher のキャッシュはシングルトンパターンで構成されているため、異なるクラス間でもキャッシュを共有することが可能です。
キャッシュにアクセスできるよう、下記のような感じでクラスのプロパティを宣言しておきます。
1 2 |
// Kingfisher のキャッシュ let cache = ImageCache.default |
もちろん、上記のような宣言を行わなくとも使うことは可能ですが、 Userdefaults.stanstad の例でも同じように再宣言することが多いので、この辺りはお好みやプロジェクトのガイドラインにしたがっていただければと思います。
キャッシュの確認と、キャッシュからの画像の読み込み
キャッシュへの読み書きは、キーベースとなります。
つまり、ある画像に対し、一意なキーを自分で割り当て、そのキーを使ってキャッシュへ読み書きすることになります。
下記のコードのうち、
key にはあらかじめ
String 型で一意なキーを設定してあると仮定してあります。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 |
let iv = UIImaveView() .... if cache.isCached(forKey: key) { // キャッシュ上に画像がある場合 cache.retrieveImage(forKey: key) { result in switch result { case .success(let value): DispatchQueue.main.async { iv.image = value.image } case .failure(let error): print(error) } } } |
isCached(forKey:) を使うことにより、キーで指定した画像がキャッシュ上にあるか調べることができます。
キャッシュに画像がある場合には、
retrieveImage(forKey:) を使ってキャッシュから画像を取り出します。
取り出し後の処理はクロージャによって行わせますので、この部分にキャッシュから取り込んだ画像に対する処理を記述します。
クロージャに渡される値は Result 型( Result<ImageCacheResult, KingfisherError>)となっていますので、 switch 文を使うことによって成功・失敗の事例判定を行います。
Result 型とは、成功・失敗それぞれの状態に伴う値を一つの値として受け渡しできる型です。- Result
A value that represents either a success or a failure, including an associated value in each case.
取得に成功した場合には、
.success(let value) の
value 部分にキャッシュから取り出した値が入ってきます。
value には
cacheType のように、取り出した値がどこにキャッシュされているか確認するためのプロパティもありますが、今回は特に必要としないため使いません。
キャッシュへ画像を保存する
キャッシュに対して画像を保存する方法は極めて簡単です。
1 2 3 |
// ここで、data は Data 型で取得した画像データとする let image = UIImage(data: data)! cache.store(image, forKey: key) |
store(_:forKey:) の第一引数には、
UIImage 型をそのまま代入することが可能です。
ただし、引数はオプショナル型ではないことに注意してください。
キャッシュから画像を削除する
キャッシュから画像を削除する方法も簡単です。
削除したい画像と関連づけたキーを指定することで削除することが可能となります。
1 2 3 4 |
// キャッシュから画像を消す if cache.isCached(forKey: key) { cache.removeImage(forKey: key) } |
removeImage(forKey:) にはデフォルト引数がいくつか存在し、ディスクキャッシュ・メモリキャッシュからの削除、削除完了後に処理させたいクロージャの指定、またそのクロージャを実行するためのスレッド(Queue)の指定ができるのですが、通常の利用であれば上記の通りで大丈夫だと思います。
まとめ
キャッシュ付きの画像ダウンロートフレームワークは、AlamofireImage や Nuke など、ほかにも様々なフレームワークが存在するのですが、「キャッシュの部分だけ使いたい」ということであれば Kingfisher を使ってみるのが一番手軽そうな印象をうけました。