SwiftUIに適したアプリケーション設計を思考する

Muukii (Hiroshi Kimura)
Eureka Engineering
Published in
20 min readDec 16, 2019

(最終更新日:2020/12/01)

この記事は「eureka Advent Calendar 2019」16日目の記事です。

15日目はPairs JP Web Teamの新(@ooDEMi)による「Building Design System for Pairs」でした。

エウレカのiOSエンジニアのMuukiiです🤠
Pairsの日本版と台湾・韓国版 iOSアプリの開発を担当しております。

今回はSwiftUIとそのためのアプリケーション設計についてお話をします。

Pairs全体的に利用しているVergeというフレームワークの運用と開発による知見からの考察となります。

2019年はWWDCでSwiftUIが発表されて大きな話題となっています。
もちろんエウレカの中でもSwiftUIについて研究したり話し合ったりしています。

SwiftUIの登場により、いままでのiOSアプリ開発が変わることは確かだと思います。

その中でも特に私が関心を持っている部分として、

UIKitによるUIドリブンなプログラミングからSwiftUIによるデータドリブンなプログラミングになる

というところです。
この変化がアプリケーションの状態管理をより強くできる可能性があると考えています。

そこで、SwiftUIを使ったときの設計について考えてみました。
結論までは到達していませんが、これまで考えてきたことを考察としてまとめます。

複雑化する状態の組み合わせを上手く管理するためにアーキテクチャを選択する

フロントエンド(UI)を持つソフトウェアで最も気合をいれて取り組むべきと感じるところは、やはり「状態管理」です。

デバイスの状態とアプリが提供するサービスにおける状態を組み合わせると想定すべきパターンは膨大な数になります。

考慮すべきパターンにひとつフラグが入るだけでパターンは単純計算で倍に、指数関数的に増えていきます。

WWDC 2019のSessionでもここについて触れていて、
不具合はパターンの複雑度が人間の能力を超えたときに発生する可能性が表れてくる。と説明されています。

不具合はシステムの複雑度が人間の能力を超えたときに発生する

この問題に対応していくために、様々なツールや設計手法が多くの開発者によって作られています。

  • Linter
  • Compiler
  • Application Architecture
  • Design Pattern
  • Code Generator
  • Unit Testing
  • UI Testing

SwiftUIもこの問題に対応するためのひとつのツールといえます。

では具体的にSwiftUIのメリットをよりよく活用していくために、アプリケーションの設計はどのようなものが良さそうか?

というのが今回のテーマです。

MVC, Clean Architecture, MVP, MVVM, Flux
など多くのアーキテクチャがある中でどのように考えられるでしょうか?

注意しておきたいところとして、今挙げたアーキテクチャはそれぞれカバーしている領域がどれも微妙に異なることです。
ものによっては画面に見える部分を中心に問題を解決することが考えられ、それ以外の部分は使用するプロジェクトに委ねられていることがあります。

ひとまず、SwiftUI + MVVMはどう?

ここで触れるMVVMとは、画面(View)やCellごとにViewModelを定義を行い、それぞれが担当するViewに表示するデータをバインディングするような設計を指します。

バインディングの方法は特に指定はありません。
Bi-directionalにすることもあればUni-directionalにすることもあると思います。

ViewModelを定義することで、ViewModelの担当範囲(コンポーネントごとなど)においてViewを安定的に更新していくことに注力できます。

Viewのための状態を細かく表現できることも強みとも言えるかもしれません。

次に、気をつけたいところはViewModelのライフサイクルです。
生成するタイミングと破棄するタイミングはどのように管理したら良いでしょうか。

UIKitアプリの場合、表示するUIViewControllerやUIViewに保持してもらうことで表示の役目を終えたときに一緒にViewModelも破棄してもらうことが可能です。

一方でViewModelは破棄せずに、また次の表示タイミングで使い回すようなキャッシュのような役割も可能です。

このようなことはUIKitがUIドリブンであるため可能ですが、一方でSwiftUIはデータ(状態)を元に画面の構築を行うことが大きな違いとなります。

UIKitアプリにおけるViewModelとViewの関係性

ViewModelの役割はSwiftUI.Viewがすでに担っているのでは?と考える

ViewModelの存在の必要性について悩んでいたのですが、よくよく考えてみるとSwiftUI.ViewはすでにViewModelの役割を持っている考えられないか?と思いました。

SwiftUI.Viewにおいて、

  • @ State
  • @ ObservedObject
  • @ EnviromentObject

これらで定義しているプロパティは自身の変更により画面の更新(var body: some View)が呼び出されます。
また、テキスト入力をプロパティにそのまま書き込みを行いたい場合は`@Binding`を使用することが出来ます。

UIKit + MVVMでやっていたことがSwiftUI.Viewで出来る、と捉えることができるかもしれません。

SwiftUI.Viewは画面に表示されるコンポーネントの実体ではなく、あくまでViewを構築するためのモデル。つまりViewModelという表現は当てはめられそうです。

ということで、SwiftUIはすでにMVVMパターンの上にあると捉えることにして、次はViewModelより下のレイヤーを考えていきます。

[追記 2020/10/10]

iOS14よりSwiftUI.StateObjectが導入されています。
これによりViewの状態管理を部分的に担う、SwiftUI.Viewの外に置く必要があるViewModelのような役割が参加しやすくなっています。
ただし、StateObjectのライフサイクルには要注意です。注意深くinit/deinitを観察しながらオブジェクトを配置することをお勧めします。

次に考えるところは、もっと大きな単位での状態を保持する役割

ViewModelは自身が持つ状態(State/データ)をViewへ反映することを担当しますが、その状態はViewModel自身が作り出すことはあまり無く、ViewModelよりさらに下のレイヤーに位置するデータレイヤーなどから状態のもととなるデータの取得を行い、それをViewに反映させる。という仲介人のような立ち位置です。

ViewModelの立ち位置

そのため、任意の情報をアプリケーション全体で同期を行い表示するという役割をViewModelだけで担当することは困難です。

もっと広範囲で全体的な状態を管理する部分について考えてみます。

Reduxを考える前に、ストアパターンを考える

ストアパターンのイメージ

ここまでくると、やはりReduxか!?となってしまいそうですが、その前にFluxアーキテクチャのコンセプトともいえるストアパターンを考えます。

Vue.jsのために開発されたFluxの実装であるVuexのドキュメントには次のように示しています。

もし、あなたが大規模な SPA を構築することなく、Vuex を導入した場合、冗長で恐ろしいと感じるかもしれません。そう感じることは全く普通です。あなたのアプリがシンプルであれば、Vuex なしで問題ないでしょう。単純な ストアパターン が必要なだけかもしれません。

引用文の中にある「ストアパターンが必要なだけかもしれません。」が大切なポイントだと考えており、Fluxという概念やRedux, Vuexという実装はストアパターンがベースとなっていることです。

このポイントをおさえておくことで、SwiftUIにReduxやVuexの実装を取り込み、何かしらの不都合が生じた場合はストアパターンの概念を忘れなければ適切にカスタマイズしていくことが可能だと思います。

🌲 Single state tree

ストアパターンは、状態を管理するストアを作成し、ストアを各コンポーネントで共有することで表示するデータの同期を行うことができる。というものでした。

Flux実装であるReduxやVuexはストアの数をひとつに限定し、アプリケーションのすべての状態をひとつのオブジェクト(Single state tree)として定義することを推奨しています。

以下、Vuexのドキュメントからの引用です。

Vuex は 単一ステートツリー (single state tree) を使います。つまり、この単一なオブジェクトはアプリケーションレベルの状態が全て含まれており、”信頼できる唯一の情報源 (single source of truth)” として機能します。これは、通常、アプリケーションごとに1つしかストアは持たないことを意味します。単一ステートツリーは状態の特定の部分を見つけること、デバッグのために現在のアプリケーションの状態のスナップショットを撮ることを容易にします。

Single state treeを定義し、SwiftUI.Viewによって表示に適した形に変換していくことで、アプリケーション全体のどの部分でも最新で正しいデータを表示することが簡単になります。

どの実装を参考にしたらいい? Redux? Vuex?

私自身は、ReduxとVuexのどちらのドキュメントも参考にしています。

特に、Vuexにおける、mutationとactionの切り分けそれぞれの処理の内容を名前の定義とともに記述出来るところが気に入っています。

引用元 : アクション | Vuex

依存物はだれが所有する?

ReduxやFluxなどを用いたWebアプリケーションのサンプルなどを読んでいるときに、いつも懸念に思う部分として、「Storeの状態を変化させるために必要な依存物の管理方法は?」というところです。

依存物とは主にデータレイヤーで必要となる、次のようなものです。

  • HTTPリクエストを行うAPIクライアントのインスタンス (URLSessionなど)
  • データベースなどのインスタンス
  • その他、なんらかのフラグなど

管理方法が気になる理由は、VuexでいうところのActionを発行する際に、APIクライアントへのアクセス方法がないとリクエストを投げることが出来ないからです。

依存物をシングルトンにしてしまうことで、staticな領域を通してActionからアクセス可能になりますが、シングルトンによる別の弊害も考えられます。
(見つかるサンプルは大体シングルトンにしていました。)

例えばTwitterクライアントのようなマルチアカウント機能を提供する場合にはどうなるんでしょう?
できればAPIクライアントをアカウントごとに生成し、トークン管理も任せたいところです。

Storeが上手いこと持つのか、依存物をなにかしらのdictionaryに詰めて状態に合わせて適切な依存物を取り出すのか。など方法は色々ありそうですが、ポピュラーと言えそうな事例はうまいこと見つけられませんでした。

Single state treeを採用するときに忘れてはいけないこと

Single state treeはメリットが大きいですが、デメリットというかトレードオフがあることを忘れてはいけません。

それについてもRedux・Vuexのドキュメントでしっかりと触れられています。

Stateシェイプのノーマライズ

Stateの中身は値型で持つことを推奨されていますが、値の持ち方に気をつけないと更新するときに計算量が膨大になってしまう可能性があります。

特にサーバーから受け取ったデータはデータベースに格納するように正規化してStateに格納しておくとパフォーマンスが下がりづらい状態を作ることが出来ます。

このような理由からReduxやVuexにはORMが拡張として用意されています。

正規化についてはReduxのドキュメントから詳細を得ることが出来ます。

適切な粒度でのメモ化

Storeが持つStateを使ってその場で計算を行い、別のStateを作ることがよくあります。
いわゆるComputed Propertyなのですが、ときにこの計算のコストがボトルネックになることが起こりえます。
そこで、計算結果を適切にキャッシュすることで、計算コストを抑えるアプローチがあります。
これをメモ化と呼ぶようです。

こちらについてもReduxドキュメントにて詳細を得ることが出来ます。

CoreDataやRealmと連携が必要になったら?

Single state-treeを用いる場合、表示するデータなどは基本的にはstateを通ります。
では、一部のデータをCoreDataやRealmを用いて永続化を行う場合はどのように考えるべきでしょうか?

永続化はアプリによってはユーザー体験を高めるための重要なポイントです。
CoreDataを使用することを例に考えてみます。

アイデア1 : NSManagedObjectからstructに変換を行う

多くのFlux実装における考え方としてはStateが持つデータは値型であるべきとされていますが、NSManagedObjectはデータの実体へのアクセスを行う参照型のオブジェクトです。

そのまま考えると、NSManagedObjectを値型(struct)に変換してStateに載せていくことが良さそうです。
NSManagedObjectが更新されるたびに、この変換とStateの更新を行います。

この方針には懸念点があり、それはNSManagedObjectから値を取り出すことは一定のコストがかかることです。

NSManagedObjectが持つデータはプロパティとして公開されていますが、実際にはメモリに読み込まれているかどうかはケースによります。
読み込まれていない場合はプロパティアクセス時にディスクIOのコストが発生します。

場合によりますが、レコード数とレコードが持つプロパティ次第ではNSManagedObjectから値型への変換コストは無視できないものになることは覚えておきたいポイントです。

アイデア2 : Storeとは別にCoreDataへアクセスを行う

もうひとつの考え方としては、Storeに載せるということを諦めてしまうことです。
諦めるきっかけになりそうなのは、おそらくパフォーマンスが問題になったときでしょう。

例えば、アプリが持つ機能のなかで、チャット機能のみがCoreDataを用いているのであれば、チャット機能を表示する画面のみCoreDataとStoreを利用して表示を行うように設計を考えます。

これでパフォーマンスの問題からは逃れることは出来ますが、状態管理は分裂してしまうので、Single-source-treeのメリットのひとつである、「問題が発生した時のデバッグの行いやすさ」は薄れてしまいます。

このあたりは、Firebaseを使ったアプリ開発の事例が増えてきているので、そのあたりから色々経験談をもらうことができるかもしれません。

[更新 2020/10/10]

Realm v5より、FrozenObjectという機能が使えるようになっています。
これは通常mutableなオブジェクトであるRealmSwift.Objectをimmutableに変換してしまうというもので、効率よくstate-treeに組み込むことが可能となりそうです。
こちらのデモアプリの中で実際に試しています。

SwiftUIにうまく合いそうな設計を考えていくために頼りになりそうな参考は?

SwiftUIのような宣言的構文による実装はiOSにとっては公式では初めてではありますが、アプリケーション開発全般ではそうではありません。

特にWebアプリケーションではポピュラーになっているReactやVueはすでにこれを採用しています。

SwiftUIはReactに似ている部分が多く、Reactにおけるプラクティスは非常に参考になります。
Vueも近い部分はあると思いますが、Viewの定義の仕方は少し異なる部分があるかもしれません。

iOSにおいても、FacebookはComponentKitを活用していたり、FlutterやReactNativeにおいても事例はたくさんあります。

ここまで考えてみての感想

SwiftUIにおいて、私が考察を行ったのはデータの反映の部分が中心だったので、SwiftUIで既存のアプリが作り変えられるかという部分は不明です。
現時点の感触としては結構難しい部分があるんじゃないかとは思っていますが、突き詰めていないので正確なことは言えません。

一方で、データの反映に関しては、UIKitアプリとSwiftUIアプリで登場する役割を整理して紐付けることが出来たので、既存のUIKitアプリの設計の改善にも役立ちそうですし、将来来るかもしれないSwiftUIへの載せ替えに対応も出来るかもしれません。

エウレカが開発を行うPairs(ペアーズ)では将来に向けたアーキテクチャ改善を検討し、少しづつ実行に移しています。

[更新2020/12/01]

本記事の続きとも言える記事を書きました。

[更新 2020/10/10]

状態管理ライブラリの開発を行っています。

Flux, Redux, Vuexを参考に、扱いやすさとパフォーマンスの両立を実現するために必要と考えられるツールをいくつかのモジュールに分割して提供するようにしています。

UIKitとSwiftUIのどちらでも扱えるインターフェース設計となっているため、すでにPairs iOSアプリ全体で使用しています。

実際に運用される大規模なアプリケーションで使うことにより様々な問題を見つかり、実用性の高いライブラリに日々進化しています。

--

--