もしも、アプリでデータを永続化しなかったら?

この記事は eureka Native Advent Calendar 2017 — Qiita の10日目の記事です。

前回は丹さんのRxSwiftにおけるマルチスレッドの理解を深める — Schedulerについてでした。

Pairs Global事業部のiOSエンジニアのmuukiiです 🤠

eureka Native Advent Calendar 2017 1日目の記事にて、PairsのGlobal版iOSアプリでは、ほとんどDBを使用していないという話をしました。

「いまのところ」は「ほとんど」使っていないというのが本当のところなのですが、アプリはどのようなデータを永続化すべきなのでしょうか🤔

サーバーのデータをアプリで永続化することで、データを同期する管理が必要になってきます。
データの更新頻度が低く、かつ長く使われる場合はクライアントでDBなどを用いて永続化することはメリットにつながります。しかし、データの更新頻度が高く、使われる期間が短い場合は管理コストのほうが上回ります。
SNSのようなデータの流動性が高く、そもそもオンラインでこそ利用価値が出るプロダクトの場合において、どのデータを永続化すべきなのかは慎重に判断する必要があります。

Pairs Global版では現状はチャット機能にのみRealmを使用してメッセージの永続化を行っています。
チャットにおけるメッセージは送信後に変更されることはほとんどなく、何度も読み込まれる確率が高いので、チャット画面の表示の高速化が期待できます。

一方で、ユーザーの検索結果はAPIから取得したJSONをstructやclassにマッピングして表示するようにしています。

「永続化しない、DBを使わない」は逆に大変なこともある

データの管理をメモリ上だけとすると、「永続化処理も省けるし、JSONをstructに移してArrayにして溜めていけばいい、しかもスレッドセーフになるから並行処理も気軽に!」みたいに簡潔になりそうでが、逆にCoreData, Realmのようなデータストアを利用しないデメリットも存在します。

それは、同じ内容を示すオブジェクトでも参照は異なるオブジェクトが存在してしまったら、参照を取り間違えれば古いデータだと気づかずに扱ってしまう可能性があることです。

例えば、同じデータを扱う画面が複数同時に存在しているとします。
各画面で参照が異なるオブジェクトを持っていたら、ひとつの画面でオブジェクトに対して加えた変更が他の画面では知ることが出来ずに表示が古いままになってしまいます。

structであれば値型なので変更は同期されませんし、classであっても参照の違う同じModelオブジェクトである可能性があります。
CoreData, Realmによってユニークに管理されていればデータが変更されたタイミングでリロードを行えば最新の情報がすべての画面で表示されるでしょう。

Modelをclassにして簡単なデータストアを作ってIDごとに共通の参照にあできれば良いですが、
structが使えなくなるのと、信頼できるデータストアの実装は大変です。
そうなってくるとRealmやCoreDataで管理したほうが信頼性とパフォーマンスも担保出来るでしょう。
また、RealmとCoreDataはメモリ上だけで管理するモードも用意されているのでそれを検討する価値もあります。

DBを使用しない場合、「データが必ず最新であるとは限らない」というトレードオフが発生することを理解した上で、使用する必要があります。

Pinterestの実装を参考に

このような実装を進めるにあたってPinterestの記事がとても参考になりました。
Immutable models and data consistency in our iOS App

記事の中ではModelをImmutableに変更した時に発生した課題、つまりModelの一貫性をどのように実現するかのアプローチが書かれています。
NSNotificationCenterを使用して、Modelの名前とModelのサーバーで発行されたユニークIDを組み合わせたNotificationNameで更新を送信・受信を行うというものです。

結論として、Pinterestと同じようなアプローチを取ることにしました。
インターフェースは次のとおりです。ModelObserverの実装はGistに載せておきました。

https://gist.github.com/muukii/175c35879fbb1c541a9d19ff1717ad0f

public protocol ModelObservableType {
associatedtype Identifier : Hashable
var identifier: Identifier { get }
func addRecursiveModelObserve(observer: ModelObserver)
}
public final class ModelObserver {
init()
func update<T: ModelObservableType>(_ model: T)
func notificationKey<T: ModelObservableType>(_ model: T) -> String
@discardableResult
public func add<T: ModelObservableType>(model: T, didUpdate: @escaping (ModelObserver, T) -> Void) -> NSObjectProtocol
public func remove(token: NSObjectProtocol)
}
使い方はNotificationCenterに近いと思います。ModelObserverインスタンスを介して、ModelObservableTypeを実装したModelオブジェクトの更新通知の送信・購読を行います。
実装方法
Modelの定義
更新が可能となるModelのclassまたはstructは ModelObservableTypeを実装します。
Modelはサーバーで発行されたユニークIDを持つ必要があります。
struct MyModel : ModelObservableType {
let identifier: String // サーバー上でユニークなID
var name: String
// ...
}
ModelObserverの作成
ViewレイヤーとModelレイヤーではModelObserverインスタンスを共有します。
もし、アプリの規模が小さい場合はModelObserverのシングルトンインスタンスを作るのも良いでしょう。
// Model更新通知の流れ
// View <- ModelObserver <- Model
let modelObserver = ModelObserver()
Model更新の購読
Modelが更新されたことを知るために、ModelObserverに更新通知を送信してもらうように登録します。
let model: MyModel
// modelについての更新の購読
modelObserver.add(model: model) { [weak self] (modelObserver: ModelObserver, newModel: MyModel) in
self.nameLabel.text = newModel.name
// 購読直後と更新時に呼び出されます
}
Model更新の通知
Modelを更新したらModelObserverに更新通知を飛ばしてもらいます。
var newModel: MyModel = // APIレスポンスから作ったもの == 最新のデータ
newModel.name = "foo" // Modelの変更
// Model更新の通知を行います。
modelObserver.update(model: newModel)
modelObserver.addにて登録したクロージャが呼び出され、newModelが届きます。
まとめ
Modelレイヤーは取得したデータを持ち続けることはせずに、データに更新があったことをModelObserverを通じてViewレイヤーに教えてあげるのみとなっています。
そのためViewレイヤーには「自分が持っているデータが最新ではない可能性があるので、更新情報を知る必要がある」という責務を持たせる考え方で実装を行っています。

今回紹介した方法は実際にPairsのGlobal版iOSアプリでそのまま使われています。
ユースケースによってはこの方法に改善を加える必要が出てくるかもしれません。

関連として、DBを使用しない場合に、どのように更新されたデータをUICollectionViewやUITableViewに反映していくのかをこちらの記事に書いていますので是非読んでみてください。

こちら → UICollectionViewやUITableViewのreloadDataを呼ぶ必要はほとんどない
Like what you read? Give eureka_developers a round of applause.

From a quick cheer to a standing ovation, clap to show how much you enjoyed this story.