Eureka Engineering
Published in

Eureka Engineering

iOSアプリにおけるFluxの難しさと開発を加速させる”store-pattern” — SwiftUI/UIKit

この記事は「Eureka Advent Calendar 2020」1日目の記事です。

Head of iOS & Pairs iOSアプリ開発責任者のMuukiiです。
本記事ではPairs iOSアプリとPairs Engage iOSアプリが活用している状態管理の方法についてお話します。

この記事はFlux等のUnidirectional-data-flowについてある程度の知識と経験がある方向けとなっています。

概要

  • Fluxのコアコンセプトとしてストアパターン(store-pattern)の説明
  • ストアパターンの目線で設計を考えることでより効果的なソフトウェア改善に取り組むことができる。
  • Fluxやストアパターンのようなデータを中心とした考え方は簡単だが扱いは難しい
  • その難しさはパフォーマンスを維持することにある。
  • エウレカのiOSエンジニアで開発されているVergeGroup/Vergeはストアパターンを大規模ソフトウェアで実用化することを目的としたOSS

はじめに

Fluxというアーキテクチャはデータを中心として動作することを前提とする実装コンセプトであり、理論的には増大していく状態をできるだけ単純に管理して扱うことができるようになる。というものです。

このような特徴から、多くの機能を持ち、多くの状態(State)を表現しなければならないアプリケーションにおいてはFluxの採用が多く検討されるわけです。

ですが、現実でのアプリ開発ではこのコンセプトが綺麗に導入できないことは当然ありますし、導入を進めていく中で様々な問題にぶつかることもあります。こうしていくうちに「Fluxは本当に必要だったのか…?」という疑問を持ち始めることも考えられます。

私の経験からすると、その疑問は”Fluxが真に実現したいこと”とは別の部分に集中してしまっていることが原因かもしれません。

このような場合、Fluxのコアコンセプトとも呼べるストアパターン(store-patten)を理解することで考え方が整理できるようになります。

本記事では、store-patternを全面的に採用しているPairs iOSアプリという大規模なアプリケーションの開発の中で得た課題と経験をもとに、store-patternが持つ価値と難しさを説明し、そのコンセプトを元に効率的に開発してくためのOSSについても紹介します。

Fluxとは

store-patternの説明に入る前にFluxの簡単な流れをおさらいします。

https://facebook.github.io/flux/docs/in-depth-overview
  1. StoreがStateを保持し、Viewはそれを参照します。(a source of truth)
  2. StateはDispatcherが受け取るAction(データ)によって変更されていきます。(unidirectional data flow)

Actionというのはこのようなものです。Fluxを眺めたことがある方にとっては見慣れたものかもしれません。

Reduxに関してはFluxをコンセプトとした具体的な実装パターンとして存在してます。ここでは詳しい説明は割愛します。

store-patternとは

それではstore-patternとはなにか。それは先ほどのFluxの説明の1番の部分のみを指します。

StoreがStateを保持し、Viewはそれを参照します。

状態を共有する必要なる複数のViewは同じStoreを参照します。

状態を共有する必要なる複数のViewは同じStoreを参照します。

すなわち、

View間で共有するState(状態)は同一のStoreで管理しましょう。

これが最低条件で、Storeが持つStateについてはどのように更新されるべきかには触れていません。

StoreとViewの接続方法については任意です。Fluxライブラリが行う方法で問題ありません。

store-patternという言葉はどのぐらい一般的なものかは定かではないですが、Vue.jsのドキュメントにより、このようなデザインパターンのことをストアパターン(store-pattern)と呼びながら説明を行っています。

もし、あなたが大規模な SPA を構築することなく、Vuex を導入した場合、冗長で恐ろしいと感じるかもしれません。そう感じることは全く普通です。あなたのアプリがシンプルであれば、Vuex なしで問題ないでしょう。単純な ストアパターン が必要なだけかもしれません。しかし、中規模から大規模の SPA を構築する場合は、Vue コンポーネントの外の状態をどうやってうまく扱うか考える絶好の機会です。

Vuexのドキュメントより

このことから、Fluxはstore-patternという原始的なデザインパターンを元に展開されたアーキテクチャだと考えることができます。

記事の中ではstore-patternを活用することから直面する可能性のある課題にも触れています。その場合はFluxも対象である。と捉えてください。

Pairs iOSアプリではstore-patternを選択している

「Pairs iOSアプリが使用する状態管理アーキテクチャは?」

という質問には

「store-patternを基本としています。」

これが回答になります。

どういうことかというと、

Flux/ReduxのようなActionをデータとして表現するもの存在しません。

これに関しては本記事後半のVergeというライブラリの説明にて行います。

なぜstore-patternを選択し、なにが欲しかったのか

Pairsがstore-patternを選択した理由は次のようなものです。

  • 複数の画面にまたがる共有される情報を正しく効率的に表示するため
  • 特定のViewに古い情報が残ったりすることを効率的に防ぐため

これらを開発効率・実行効率ともに高く実現するための第一歩として、データをあちこちにコピーしてしまうのではなく、信頼できる唯一のデータとして一箇所で管理を行うことなのです。

先ほども触れていますが、Actionをデータとして表現することはルールとしていません。
これは必須とは考えておらず、むしろ導入する時の大きなハードルとなります。

データとして表現されたAction

FluxやReduxのコンセプトで導入を考え始めるとき、こんな悩み事が発生します。

  • 💭「reducerをどうしようか、どう分割する?」「 switch-case大変になってきた。」
  • 💭「Actionの表現方法はどうしようか? 」「うまく表現できないケースが見つかってしまった」
  • 💭「書くのが疲れてきた…」

など、導入をしているうちに疲れてしまうことが起こりえます。

しかし、Fluxの本来の強みである、

信頼できる情報源を一箇所にまとめ、画面に表示する情報に一貫性を持たせる

この目的を実現するだけならActionのデータ化は行わなくても可能です。データを特定の場所で管理し、共有すれば良いのです。

むしろ、ここでハマっていると後ほど説明する「store-patternが生み出す難しい課題」と向き合う体力がなくなってしまいます。

つまり、store-patternから始めることが、Fluxの一番の強みを獲得するための近道なのです。

データを中心として動作するアプリの実装の難しさ

Fluxに比べて、導入のハードルはstore-patternのほうが低いはずです。なぜなら、状態を複数のViewで共有する構成にすることだけに集中できるからです。

そして両者ともに導入を進める中で、課題が発生しやすいポイントはStateの設計でしょう。
どのような課題を抱えていくのでしょうか?

もし開発しているアプリが多くのデータを扱い、Stateが大きくなっている場合には注意が必要です。

例えば、メールやSNSにおける投稿のリストなどの100~1000件程度のレコードのようなデータをStateで管理する場合です。

状況次第ではStateからデータを取り出す際にパフォーマンスが大きく劣化してしまうこともあり得ます。

これはアプリ内でデータベースを利用する時に発生する課題と似ています。

store-patternを利用することはCoreDataやRealmのようなDatabaseをアプリの中央記憶領域とすることと同じと考えられる

store-patternやFluxは状態を(できるだけ)一箇所に集めてアプリケーションが動作するようにするというものですが、これは特に新しいコンセプトというわけではなく、典型的なデータを永続化することを目的としたアプリ内でDatabase(CoreData/Realm)を利用するアプリケーションと構成が非常に似ています。


(できるだけ)一箇所、というのは、store-patternやFluxを導入したとしても、Stateに該当するような変数をStore以外の場所で管理してはいけないということではありません。

  • 🎯 StoreはViewによって何らかの方法で購読され、データが表示される
  • 🎯 RealmはResultsやObjectによって購読され、データが表示される
  • 🎯 CoreDataはNSFetchedResultsControllerやNSManagedObjectによって購読され、データが表示される

このように、データの記憶領域が異なるものに入れ替わっているだけ。と捉えることができそうです。

Firebase Realtime Databaseなども同様に当てはめることができます。

Databaseでのプラクティスがstore-patternでも有効であり、必要になる

StoreとViewの関係において、StoreがDatabaseで置き換えられるということは、StoreはDatabaseのように振る舞うことができる必要があるということになります。

Database(RDB/NoSQL)をアプリで用いる場合、次のような特徴があります。

  • 正規化の必要性 — 木構造で表現されるデータ構造の場合、データの正規化は必須と言って良いでしょう。
    正規化を行わない場合、書き込みの際の処理が指数関数的に増大する恐れがあります。
    もっとも、RDBの場合は、自然と正規化された構造を考えていることもあると思います。
  • インデックスの構築 — インデックスは期待するデータを探すための近道として必要になるケースは多いです。
  • リレーションシップが必ず存在するかどうかが型安全にならないこともある — CoreDataやRealmにおいて、正規化によるトレードオフからリレーションシップとなるオブジェクトを取り出す際にはそれがnilでないことを確認するオペレーションが必要になることがあります。

このように、Databaseを利用する際に発生する課題とは、主にパフォーマンス面に関連するものです。
カスタムで用意するStoreやStateは必ずしもそのように振る舞えなければならないというほどではないですが、必要になることは大いにあり得ます。

特に、DBのレコードのようなデータを扱うアプリでは注意が必要です。
例えばInstagramの投稿のレコードがこのケースに相当します。

このようなデータをStateの中で表現する場合、ArrayやDictionaryといったコレクションを用いることになるはずです。

では、このInstagramのレコードの例ではどのようなState構造にするのが良いでしょうか。

一般的にはデータを効率よく扱えるようにするために正規化(Normalization)を行い、構造を設計します。

Stateの正規化とは

ReduxによるNormalizing State Shapeでの例を引用しながら説明します。

とあるアプリはStateを持ち、そのStateは次の3つのエンティティを保持します。

  • Post
  • Author
  • Comment

これらのエンティティは次のような関係です。

  • ユーザーが投稿するPost(記事)を表示
  • PostはひとつのAuthorを持ち、Commentを複数持つ

ここからStateを設計してみます。

これでアプリはPostを表示することができますが、このStateの構造には課題があります。

それはAuthorとPost/Commentのリレーションシップ部分にあり、AuthorがPostとCommentの両方に実体として存在していることです。

Authorは一人だとしても複数のPostとCommentを持つことが可能であり、その全てのPostとCommentにAuthorの実体がコピーされて存在することになります。
これではAuthorのnameを更新する時にはPostとCommentが持つ全てのAuthorを更新して周る必要があることから計算量の爆発を招くことになります。

これを防ぐためにはStateの正規化を行います。

これで、各エンティティごとにIDに対して実体はひとつというDictionaryに保管され、Post/CommentからのAuthorに対するリレーションシップはIDを持つことで表現しています。
Authorのnameを更新する時には存在する実体一つに対してのみ変更を与えられれば良いので、計算量はO(1)で済みます。

引き換えに、Post/CommentからAuthorを情報を直接は取り出せなくなります。

let comment: Comment
let author: Author? = authorsByID[comment.authorID]

残念ながらこのようにAuthorの取り出しにおいて、Dictionary内の探索と結果が必ず見つかるかはコンパイル時に保証できなくなるトレードオフが発生します。

探索のコストはほとんど無視できるものですが、Optionalが発生することは大きな課題です。
これは開発者が正しくハンドリングするしかありません。必ず存在すると保証できる場面ではforce-unwrapを活用しても良いです。

また、Dictonaryでエンティティをユニークに管理することから、順番の情報が失われてしまいました。
これはpostIDsによってエンティティのIDをArrayで管理することで順番を表現しています。
例えば、サーバーから取得した時点でのデータの順番を保持したい場合などに使用します。
もしくは、保持しているエンティティをフィルタ&ソートしたい場合にもこのようなデータ構造を活用できます。

例えば、PostをAuthorごとにグルーピングしたい場合、次のような各エンティティのIDのみで表現されたデータ構造を用意することができます。

var postsByAuthor: [String : Set<String>] // [Author.ID : Set<Post.ID>]

実体を保持せず、IDのみで関係性を表現することで、エンティティ自体の更新の影響を受けません。

まさにデータベースにおけるインデックスです。
アプリケーションのユースケースに合わせてインデックスを構築すると良いでしょう。

データを取り出す際にフィルタ&ソートを行うよりも遥かに高速にすることができます。

重複した処理を防ぐためStateに変更が存在するかを確認する必要がある

Stateというデータを中心としてアプリが動作するということは、Stateの更新に合わせて表示の更新などの処理を呼び出すことになります。

Storeから更新通知を受け取り、Viewがsubviewなどに対し表示するデータを更新していく

ここで直面する課題は、Stateの中でViewの更新に必要なデータが確かに変わっているかどうかを知る方法が必要になることです。
なぜなら、同じデータであればViewは更新する必要はなく、Viewを更新することには一般的には一定のコストを持っているからです。

UILabel.textに何度も同じテキストを設定することは大きな問題になるかはわかりませんが、カスタムUIコンポーネントなどであればその限りではありません。

store-patternの特徴である、Stateを一箇所にまとめることにより、Stateの一部のpropertyが変更されればState自体の変更となります。

store-patternで使用するStateのサンプル
Storeが持つStateを購読する擬似コード

このサンプルコードにおけるsubscribeStateではStateの変更を購読しています。しかし、Stateが変更されれば届くことから、nameやageが前回の値と変わっているかを知る必要があります。

これを防ぐためにはいくつかのアプローチがあります。

今のところ、私は2番の手段が一番妥当なものだと考えています。
後半で紹介するVergeというライブラリでもこのアプローチをベースに扱いやすいものを提供しています。

UIKitでもSwiftUIでもViewが必要とするデータが確かに変わったことを確認しながらViewの更新を行う必要性は変わりません。

一般的にView関連の処理が一番コストが高く、その次に考えるべきことが、データの変更を検知すること自体のコストです。

state.myState != oldState.myState

見た目は1行ですが、この裏にある演算コストにも注意を払いましょう。

現実世界ではStoreのStateを利用するViewは数十個から数百個になることはあります。

Storeに対する一度の変更により、Stateに対してのアクセスと演算が大量に行われてViewの更新が行われるのです。

あらゆる部分でコストを下げておくことは念入りにしておくべきです。

時にはInstrumentsからTimeProfilerを起動し、詳細にパフォーマンスを計測することをお勧めします。
Swiftの言語的な特徴などから、暗黙的なdynamic-castingやValue-typeのcopyやdestructionが発生し、実行速度にオーバーヘッドが生じていることはあり得ます。

特にAnyHashableには暗黙的なdyanamic-castingがあります。

store-pattern まとめ

  • store-patternは理論的に考え方を単純化することで状態管理を楽にする。というアプローチ
  • すなわち、「State(状態)というデータ」をもとに動作するアプリケーションとなる。
  • あらゆるものを保持可能なデータで表現するため、適切なデータ構造を構築する必要がある。
  • データの扱いには慎重になる必要があり、必然的に計算量などを考慮することが求められる。
  • store-patternのようなデータドリブンと典型的なイベントドリブンは計算量と複雑さとイベントハンドリングの複雑さのトレードオフのようなものがある。
Verge

🌚 store-patternの導入・利用を効率的にするライブラリ

VergeGroup/Verge

Vergeはstore-patternをiOSアプリで効率的に利用することができるように設計されたライブラリです。
UIKit / SwiftUIでの利用をサポートしています。

Vergeの開発はPairsでの実践をもとにフィードバックが取り込まれていきます。
VergeはSwiftUIもサポートしていますが、まだまだ実践的と言えません。

まずは簡単にVergeの使い方を説明するために、Vergeを利用したコードを紹介します。

StoreまたはViewModelを定義する

MyViewModel with Verge

これがStoreの定義になります。

Storeの定義はひとつだけに止める必要はありません。
データを共有する範囲に応じて複数個作成していくこともあります。

例えば、アプリ全体のデータを共有するStoreと、ViewController単位で状態を管理するViewModelです。

Pairsでは中心となるStoreをひとつ配置して、それと連携を行いながら動作するViewModelという名前のStoreが多く存在しています。

Cellに対してもCellModelという名前のStoreを生成しています。
これによりCollectionViewを更新することなくCellに対して直接更新を呼びかけることが実現しています。

PairsのStore/ViewModel構成

Stateの更新にはcommitを実行する

Stateを変更する手段として、 StoreComponentTypecommit メソッドを提供します。

この commit はStoreが実行するクリティカルセッションであり、lockを取得しながらStateの変更を行います。

commit の内容に応じてメソッドを定義します。これはFluxにおけるActionに相当します。

そのメソッドは同期処理のみに制限する必要はなく、非同期処理を行っても構いません。
同期処理で完了させなければならないのはcommitの内側のみです。

ViewModelをViewで利用する

次にこのMyStoreをUIKit.UIViewControllerで利用するコードです。

ViewControllerはStoreが持つ sinkState メソッドを使い、Stateの変更を監視することができます。
実際にはStateそのものではなく、 Changes<State> というオブジェクトに包まれた状態で届きます。

Changes<State> は現在のStateと変更前のStateを保持することで、Changesさえ手に入れば、Stateの購読から外れた場所でも変更の検出を可能にしています。

例えばこれは、RxSwiftのdistinctUntilChangedなどを使用する必要がなくなることを意味します。

Vergeを用いたstore-patternの実装についてはこちらの記事もどうぞ。

Vergeを開発するモチベーション

さて、Vergeはstore-patternにフォーカスをしていますが、FluxやReduxの実装となるiOS用のライブラリはすでにいくつか存在しています。

ポピュラーなものではReSwiftComposableArchitectureなどでしょうか。

その中でVergeという別のライブラリを開発するモチベーションは次のようなものです。

  • コアコンセプトとなる実装は提供しているが、パフォーマンスを考慮した周辺ツールが少なめ
  • Actionなどの定義を必要とすることが前提となっている

私の意見として、Actionをデータとして表現することをあまり好んではいません。具体的に次のようなイメージです。

enumによるAction表現
Actionごとにstructを用いた表現

私が避けたいこととして

  • Actionというデータをdynamic-castingしてswitch-caseを避けたい
    設計次第でdynamic-castingは避けられますが、switch-caseは継続します。私はここで発生する線形探索による処理の分岐が気になっています。
  • Actionをstructで表現は美しいが、デメリットもある。
    これはSwiftにおいてはバイナリサイズの増大を引き起こす可能性があります。(Swiftのバージョンによって変わり得ます)
    structやclassなどのシンボルを増やすことがバイナリサイズに影響しています。これはすこし細かい話ですが、巨大なアプリを提供するケースにおいては深刻な問題になり得ます。
    https://github.com/muukii/Swift-binary-size

私の考えではActionをデータとして表現することはデバッギングの効率を高めることが主な目的だと捉えています。
ですが、それが実際に役立つことは経験上では少なく、Actionの追加時の手間のほうが大きいことが懸念です。

“Actionを定義して、Actionをハンドリングするためにswitch-caseを追加”

これらを省略するために、VergeではSwiftにおけるメソッドをActionと捉え、そのメソッドの中からMutation(変更)がCommitされるという考え方に落とし込んでいます。
これにより、開発者は慣れたプログラミングスタイルでstore-patternを利用したアプリ開発が可能になります。

デバッギング情報は別の方法で提供しています。 Changes<State> はその変更がどこから発生したものか、という情報を持っています。
加えて、Stateのpropertyごとの変化も取得可能にしています。

Storeは複数存在しても良い

先ほど少しだけ触れていますが、Storeは複数存在することはあり得ます。

FluxでもStoreの数に関しては特に指定はされていません。
Storeをひとつにし、単一のStateツリーで表現することは理論上はとても美しいですが、現実問題として、大きなStateを扱うことはパフォーマンス低下を招く可能性は様々な観点から起こり得ます。

したがって、Stateを複数に分離することが可能であればそれは適用して良いと考えています。(分離することを考えること自体が難しいので一緒にすべき。という見方も同時にありますが)

当然、分離したStore間での連携は発生します。
必要な値をコピーするなど

Pairs iOSアプリではUIKitベースであり、繊細なUIコンポーネントの更新が求められるため、ViewControllerごとにViewModelを作成し、Cellを使う場合はCellModelまで定義を行います。
ViewModel, CellModelという名前で実体としてはStoreを内包している形になります。
すなわち、Storeの繋がりで形成されています。

ViewControllerの範囲を超えて状態を共有したい場合に備えて、ViewModelより広い範囲を担当するStoreを用意しています。

store-patternにおける開発のボトルネックを解消するいくつかの機能

Vergeはstore-patternを利用する時に発生する課題をできるだけ簡潔に解決するための機能を多く提供しています。
それらの一部を本記事で紹介します。

Changes オブジェクト

さきほど少しだけ触れていますが、Vergeの大きな特徴のひとつがChangesです。

Changes<State>の構造

Changes<State> はclassかつImmutableであり、Stateの変更に伴って都度新しいインスタンスが生成されていきます。

classである理由としては主に処理中におけるValue-typeのコピーコストを避ける狙いがあります。

Linked-listのようなイメージが近いかもしれません。

このように現在と前回のStateを持つ構造であることから、Stateの購読から外れた場所でも変更の検出が可能となります。

RootViewがStateを購読し、自身がもつsubviewに対し、Stateを渡す

複雑なView構造のなかでも最小限のStateの購読で済ませることができます。
最小限のStateの購読はブレークポイントによるデバッギングを多少簡単にします。
購読の実装処理はどうしてもReactiveProgrammingスタイルの実装になるためstack-traceが大変なことになりがちです。メソッドが呼び出されたが、どのように発生したものなのかを慎重に読み解く必要が出てきます。

Changes<State>はデータであり、変更を検出可能

ChangesはStateをラッピングする構造ですが、dynamicMemberLookupにより、あたかもStateに直接アクセスしているかのようなシンタックスを実現しています。
その自然なアクセスに加えて、”変化があれば処理を実行する。”という機能を様々なパターンに応じて展開しています。

Stateのサンプル

Stateの組み合わせや非同期処理による待ち合わせなども、Changes.ifChangedメソッドで大半が実現可能です。

Reactiveフレームワークによる複雑なストリームの合成を用いることなく課題はクリアできます。

もっともこれはstore-patternの設計上、得意な分野とも言えます。

Changesが提供するもう一つの機能に、Memoization付きのcomputed-propertyがありますが、今回は割愛します。

Derived オブジェクト

Derivedはreduxでいうところのreselect、RxSwiftで例えるとBehaviorRelayに近いものです。

Stateから一部分を取り出し、任意の変化を与えることができます。

let derived: Derived<Int> = store.derived(.map(\.count))

値の取り出し (on-demand)

値の更新の購読

Derivedを用いることで、Stateの全体を見せることを避けながらViewに特定のStateを購読可能な状態で渡すことができるようになります。

DerivedはMemoizeをサポート

DerivedはStateから値を取り出し、任意の変化を与えることに加えて、メモ化(Memoization)をサポートしています。

Memoizationを簡単に説明すると、 (T) -> U という関数において、Tの入力が変わらない限りは結果Uは同じものを返却するという前提のもと、Tに応じたUをキャッシュして、それを返却することで、重複した処理を削り、パフォーマンスを稼ぐというものです。

MemoizationはDerivedがStateから値を取り出す時と、取り出した値に変化を加えた後に行われます。
この積極的なMemoizationによってDerivedが発行する更新イベントを最小化することができます。最後に渡されたChangesにより、処理を実行する必要があるかを決定することができます。
これはView側でのパフォーマンスを維持することに貢献します。

DerivedにおけるMemoizationはほとんどの場合はEquatableへの準拠をコンパイル時に検出し、自動的に適用されます。そうでない場合は、ユースケースに応じでどのようにMemoizationを行うかを実装することが可能です。

Verge/ORM

Vergeは拡張モジュールとしてORMを提供しています。
アプリが利用するエンティティを正規化してStateに格納することをできるだけ簡潔に安全に実行することをサポートします。

Pairsでは永続化しないデータに関してはVergeORMを使い、画面に表示されるデータを同期しています。

More and more…

Vergeには他にもさまざまな最適化とその手法を提供しています。
ドキュメントは少しづつ強化をしていますので、導入を検討される際にはぜひ読んでみてください。

具体的な利用事例に応じた質問にも答えられますのでTwitterなどから気軽に連絡ください。👨🏻‍🦰

https://twitter.com/muukii_app

まとめ

本記事ではFluxのコアコンセプトに当たるstore-patternを中心に説明を行いました。

アーキテクチャには理想と現実のギャップは必ず存在しています。

Fluxアーキテクチャは新しいようで、見方を少し変えると、Databaseを中心として動作するアプリと非常に近いということから、Databaseを扱う上でのプラクティスはstore-patternで絶対的に有効です。

逆に改めて思い知らされたこととして、エンジニアとしてプログラミングを行う以上、データ構造・計算量・プログラミング言語の特徴に応じたチューニングを行うスキルの必要性は避けられないということ、です。

ただし、これらは技術的にある程度はカプセル化を行い、より多くの開発者が深い部分を気にしなくても開発を続けることができるAPIを提供することは可能でしょう。CoreDataやRealmがクエリを内部的に最適化するように。

もしかすると、結果的に「store-pattern難しい」と印象づけることになったかもしれませんが、
ビジネス成長と共にアプリが発展・複雑化することに対する手段として、store-patternを用いることなく、イベントドリブンで複雑なイベントが飛び交うアーキテクチャと比較した場合でも開発効率やメンテナンス性は高く保つことは可能だろうと考えます。

そしてこのstore-patternをベースに開発しているのがVergeであり、主にエウレカのiOSエンジニアによってフィードバック・開発が行われています。
このメリットは、1000万人が利用する規模のアプリで実用に耐えたプラクティスがVergeにフィードバックされていくことです。
実行時の安定性・パフォーマンスから開発時の記述のスムーズさは日々検討が行われています。

PairsとEngageのアプリはこれからも発展が続き、さらなるユーザー体験向上ため機能が強化されていきます。
それらを効率的に実現するために技術面のアップデートを継続的に行うことは必要不可欠です。

エウレカではこのような大規模のアプリ開発に加えて、ビジネス成長を技術面から支え、加速させることに興味関心があるiOSエンジニアを募集しています。

もっと具体的な話を知りたいと思った方へ

store-patternの利用事例をもっと具体的に知りたいなどの需要があれば、Pairs iOSアプリの実際の機能を例にした詳細の説明を別途記事にしようと思います。

気になる💭という方はこの記事をClapやTwitterなどでシェアすることで需要を示してください👨‍💻!

2日目はShimaによる「iOSにおけるツールチップの実装」です!

付録

facebook/Recoil

Facebookが以前公開したRecoilは巨大なStateツリーからくるパフォーマンスの問題を避けることを考えたアーキテクチャのようです。

ドキュメントを読む限りなので誤解している可能性もありますが、
大きな値型の塊を作るのではなく、小さな値型の器(Atom)たちからStateが形成され、全体を購読することと直接特定の器を購読することも可能なようです。

💡Tip: CoreDataとかを活用してstore-pattern/Fluxは可能なの?

実際に実験をしたわけではありませんが、可能だと考えています。
むしろ、連続するデータを格納することに最適化されたデータストアを使うことは非常にメリットが大きいでしょう。

前述の通り、Stateとその扱い方次第では計算量が最悪になり、専門的なチューニングを施す必要に迫られることがあります。
モバイル用に設計されたCoreDataやRealmはすでにチューニング済みなので、ルールに沿ってStateを構成することでパフォーマンス問題には遭遇しづらいはずです。

とはいえ、そのチューニングがあるからこそ、CoreData/Realmを利用する際の技術的な制約があることはトレードオフです。それがstore-patternに活用した際には、本来の恩恵が得られるまでいくらかの手間がかかる可能性もあります。

2020年3月ごろ、Facebookが「iOS版Messengerを作り直しました。」という内容の記事が公開されています。この記事を読む限りのことしか分かりませんが、彼らはSQLiteを活用し、Viewに表示されるデータの多くの処理をSQLiteに任せることができた。と語っています。
これはもしかすると、状態管理を行うデータストアとしてSQLiteを選択したのかもしれません。

どうやらSQLiteのViewを活用し、正規化されたデータを非正規化し、UIにとって扱いやすいデータを構築しているみたいです。

💡Tip: CoreData/RealmとカスタムなStore併用可能になりつつある

一般的にstore-patternで用いられるStoreはStateを保持し、それはスレッド安全であること、かつ、不変(immutable)であることが求められます。

NSManagedObjectやRealmSwift.ObjectはImmutableデータモデルではなく、かつスレッドセーフではないことから、それらをStateの中で直接管理することは危険になります。
ただし、Realmはv5からFrozen-objectsをサポートしています。RealmSwift.ObjectをMutableからImmutableな状態に変換するというものです。
CoreDataに関しては、CoreStoreというライブラリがNSManagedObjectからsnapshotを作り出す技術を提供しています。

--

--

Learn about Eureka’s engineering efforts, product developments and more.

Get the Medium app

A button that says 'Download on the App Store', and if clicked it will lead you to the iOS App store
A button that says 'Get it on, Google Play', and if clicked it will lead you to the Google Play store