iOS
キャッシュ
Swift
Realm
RealmSwift

ニュースアプリでAPIの記事をRealmにキャッシュして有効期限内だったらそれを表示する

会社で作っているニュースアプリ、NewsDigestで、記事のキャッシュについての見直しをしていました。

良さげなキャッシュ方法が分かったので、そのやり方のTipsです。

キャッシュする目的

  • 無駄にAPIを叩かない
  • 高速に記事を表示する

キャッシュの仕組み

キャッシュがあればキャッシュの記事を使って表示、キャッシュがないorキャッシュの有効期限が切れていたらAPIを叩いて記事をリクエストします。

つまり、

WHEN DO
キャッシュがない時 APIを叩く
有効期限内のキャッシュがある キャッシュを使う

というルールです。

実装

環境

  • Swift3
  • RealmSwift

オブジェクト定義

ここで、ArticleEntityとFeedというオブジェクトが以下のような感じで定義されているとします。

ArticleEntity

import RealmSwift
import Unbox

final class ArticleEntity: Object, Unboxable {

    dynamic var title: String = ""
    dynamic var url: String = ""

    required convenience init(unboxer: Unboxer) throws {
        self.init()

        self.title = try unboxer.unbox(key: "title")
        self.url = try unboxer.unbox(key: "url")
    }
}

Feed

import RealmSwift
import Unbox

final class Feed: Object, Unboxable {

    /// ID
    dynamic var tabId: Int = 0
    /// 直近の記事
    var recentArticles: List<ArticleEntity> = List()
    // データの有効期限
    dynamic var expiredDate: String = ""

    override class func primaryKey() -> String? {
        return "tabId"
    }
}

APIで受け取ったJSON形式の記事をUnboxでいい感じにArticleEntityに入れた後、その記事の塊のArrayをFeedに入れてtabId(エンタメとか政治とかのタブの判別id)をキーとしてRealmに保存していきます。

column 内容
key タブ番号 103
object 記事のセット Feed

Feedは有効期限も持っていて、記事をViewに表示するときは、それを見てキャッシュを使うか判断します。

RealmにFeedオブジェクトを書き込む

import RealmSwift

    /// Realmに記事をキャッシュする
    func wirteToRealm(_ tabId: TabId, articles: [ArticleEntity]){

        let feed = Feed()
        feed.tabId = tabId.rawValue
        feed.recentArticles = List(articles)
        feed.expiredDate = Date(timeInterval: 60 * 30, since: Date()).toString() // 30分が期限

        do {
            let realm = try Realm()

            try realm.write {
                 // 追記: コメントでご指摘いただきました。deleteしなくても上書きされます                
                 // if let cache = realm.object(ofType: Feed.self, forPrimaryKey: tabId.rawValue) {
                 //   realm.delete(cache)
                 // }
                realm.add(feed, update: true)
            }
        } catch let e {

        }
    }

ここで保存したやつを、Realm Browserで展開した様子が以下です。

スクリーンショット 2017-08-29 18.47.06.png

いい感じですね。

PrimaryKeyであるtabIdとarticleEntity20件、有効期限がStringで入っています。

有効期限内のキャッシュがあったらキャッシュを使う、なければAPIを叩く

はい。最終的にやりたかったことがこれです。

/// 有効期限が切れていないキャッシュがあればそちらを使い、なければAPIを叩く
    func fetch(_ tabId: TabId) -> Single<[ArticleEntity]> {

        do {
            let realm = try Realm()

            // キャッシュがあり、有効期限が現在時刻より未来だったらキャッシュを使う
            if let cache = realm.object(ofType: Feed.self, forPrimaryKey: tabId.rawValue),
                cache.expiredDate > now { // <- 日付の比較はよしなに

                let entities = Array(cache.recentArticles)
                return Single<[ArticleEntity]>.just(entities)
            }
        } catch {
            logger?.error(error.localizedDescription)
        }

        // キャッシュがなければAPIを叩いたものを使う
        return fetchLatest(tabId)
    }

これで、30分以内はRealmのキャッシュの記事を表示するので通信が発生せず、高速で記事を表示することができます。(実際のNewsDigestでの有効期限はチューニング中)

引っ張って更新の時には直接APIを叩けば良いですね。

まとめ

ということで、

キャッシュがあればキャッシュの記事を使って表示、キャッシュがないorキャッシュの有効期限が切れていたらAPIを叩いて記事をリクエストする」が実現できました。

誰かの役に立てば幸いです。

告知

急成長!No.1報道ベンチャーでニュースの未来を創るアプリエンジニア募集中
https://www.wantedly.com/projects/78838