🌱

Realmでは無くCacheを使ってオフライン動作対応のキャッシュ機能をiOSアプリで実装する

2021/09/05に公開

hyperoslo/Cache
iOSやAndroidなどのネイティブアプリケーションではオフラインでの動作対応が必要だと思います。iOSアプリでオフラインでの動作対応を行う為のキャッシュ機能はRealmを利用するのが一般的だと思うのですが、今回はhyperoslo/Cacheを使用してキャッシュ機能を実装します。

🙅‍♂️なぜRealmではなくてCacheを選定したのか

今回キャッシュ機能を実装しオフライン動作対応で行たかったことは、下記の2つです。

  • マスターデータに関するWeb APIのレスポンスをキャッシュとして保持する
  • キャッシュ済みのリソースに対するAPIのリクエスト時は、レスポンスを待たずにユーザー側へ表示を行う

要するにローカルストレージでデータをキャッシュとして保持したいだけで、凝った機能を実装する予定がありません。

しかしその為だけにRealmを利用するには、Realm系のクラスの継承・プロパティの宣言、スキーマ変更に伴うクラッシュを避ける為のマイグレーション作業など多くの点を考慮する必要があります。
CacheはCodableに準拠していれば数行記述するだけでローカルのストレージでデータを保存することが可能です。

Cacheでのキャッシュ機能の実装

ライブラリのインストール

以下をCartfileに追記し、ライブラリのインストールを行います。

github "hyperoslo/Cache"

ストレージの作成

ここでは例として飲食店アプリを想定します。自身のお気に入り店舗リストをリクエストする処理をイメージしていただきたいです。基本的にはここに書かれている通りに実装を行なっていきます。

またStaticResultという型はCacheというライブラリがResultという列挙型を宣言しているので、回避するために作成したResultのエイリアスです。

Storageクラスのインスタンスを作成するためにディスクとメモリのインスタンスを作成します。ここではFavoriteShopというCodableに準拠した型を指定して、特定の型を操作可能にしています。詳しいオプションの説明はこの記事では省きます。

import Foundation
import Cache
import Alamofire

protocol FavoriteShopModelInput {
    func fetchFavoriteShopList(completion: @escaping (StaticResult<[FavoriteShop], ErrorResponse>) -> ())
}

final class FavoriteShopModel: FavoriteShopModelInput {
    let diskConfig = DiskConfig(name: "FoodApp")
    let memoryConfig = MemoryConfig(expiry: .never, countLimit: 10, totalCostLimit: 10)
    var storage: Cache.Storage<String, [FavoriteShop]>? = {
        return try? Cache.Storage(diskConfig: diskConfig, memoryConfig: memoryConfig, transformer: TransformerFactory.forCodable(ofType: [FavoriteShop].self))
    }()
}

データの保存とデータの存在チェック

実際にお気に入り店舗リストをリクエストする処理のイメージです。
try? storage!.existsObject(forKey: "favoriteShops")の構文で対象のKeyに紐づいたオブジェクトが存在するかどうかを論理値で取得しています。
ここでは存在していたらキャッシュ済みのオブジェクトをtry? storage!.object(forKey: "favoriteShops")で取得し、コールバックに渡しています。

その上で対象のエンドポイントに対するリクエストを行い、取得したデータをtry? self.storage!.setObject(favoriteShops, forKey: "favoriteShops")で保存し、最新の値をコールバックに渡しています。

func fetchFavoriteShopList(completion: @escaping (StaticResult<[FavoriteShop], ErrorResponse>) -> ()) {
    Logger.debug()
    let isExistsCache = try? storage!.existsObject(forKey: "favoriteShops")
    if isExistsCache! {
        let cacheFavoriteShops = try? storage!.object(forKey: "favoriteShops")
        completion(.success(cacheFavoriteShops!))
    }
    
    AF.request("https://foodapp/hoge", method: .get, encoding: JSONEncoding.default)
        .validate()
        .responseJSON() { (response) in
            switch response.result {
            case .success:
                guard let data = response.data else { return }
                let favoriteShops = try! JSONDecoder().decode([FavoriteShop].self, from: data)
                try? self.storage!.setObject(favoriteShops, forKey: "favoriteShops")
                completion(.success(favoriteShops))
            case .failure:
                Logger.debug()
            }
        }
}

こうする事でキャッシュとして保持していた場合は、レスポンスを待たずに対象のデータをユーザー側で表示をさせることが可能になります。

最後に

このようにCacheを利用すれば簡単にローカルストレージにデータをキャッシュとして保持することが出来ます。Realmで用意されているような凝った機能が必要なければ選定する選択肢として考えてみても良さそうです。

GitHubで編集を提案

Discussion