目次
はじめに
この記事はiOS 13以降にもSwift Concurrency(つまりasync/awaitやActorなど)が使えるようになると思っていなかったときに書いたものです。
はなしの準備
雑談として「最近はどんなアーキテクチャでiOSアプリ作るの?」という話題があったので整理の文章を書いてみます。
Appleの性質上、2021年7月でもまだ決め手のようなものはないし、私だったらTCAやVIPERを候補にモジュール分割してなるべくDB使わずに作って必要になったらCore Dataを採用すると思います。
それはそれとして、Android BlueprintのREADMEかなにかでGoogleのソフトウェアエンジニアが「チームが生産性を最大化させるアーキテクチャを選べばいい」なんてことを書いてあったのを読んだ記憶があるんですが、それは最もですねと思いつつも、しかしそもそも選択肢がわからないと選ぶことはできないので、今回は雑に情報を羅列してみます。
前提
- iOS 14以上をターゲットとする
- iOS 15以上からのSwift 5.5のasync/awaitは使えない
- なぜiOS 13からじゃないの?
- この記事を書いてる2ヶ月後か遅くても3ヶ月後の2021.10にはiOS 15が出てるから
- SwiftUIを使うときは使う
用語の整理
なるべく『アーキテクチャ』という言葉を単体で使いたくないので、目的別に構造設計の要素を分類して用語を固めときます。
- 構造設計のパターン
- GUIアーキテクチャ
- 主な目的
- パターン
- MVVM
- MVP
- VIPER
- 状態管理アーキテクチャ
- 主な目的
- データフローを管理したい(状態管理したい)
- パターン
- 主な目的
- レイヤードアーキテクチャ
- 主な目的
- アプリケーションを層に分けたい
- パターン
- クリーンアーキテクチャ
- 主な目的
- GUIアーキテクチャ
分類はしたけども、たとえばもちろんMVVMでクリーンアーキテクチャを採用してもよいわけで、排他的関係ではないです。
どの構造設計を選択するかの視点を整理する
- 構造設計の要素選択はこれまでさんざん出てきた既知の問題/課題を構成で解決できているかどうか
- 主流を気にしてみる
- 理由
- 主流になっているやり方はこれまでの課題に対する解決方法を提示していることがあるため
- 更新されなくなった古いフレームワークやライブラリを使わないため
- 理由
- 主流を気にしてみる
- 開発メンバーについて考慮すること
- 要素
- メンバー新規追加時の学習コスト
- サンプルコードを共有し、実現したい機能に対してパターンを提示できるかどうか
- メンバーの納得感
- なぜそのような作り方をしなければいけないかを共有できるかどうか
- メンバー新規追加時の学習コスト
- 要素
- 機能を組み合わせたりすることを前提にできるか
- 要素
- 画面ごとに機能があるが、画面と機能をセットに別の画面でも使えられるかどうか
- 要素
- 画面観のデータの共有ができるか
- 要素
- 複数画面で共有されるべきデータは共有され、変化が追随されるか
- 要素
その他よくある疑問
- モジュールをEmbedded Framework的に分割するかどうか
- モジュールを分割することで実装コストがかかる?
- なこたーない
- ライブラリ関係の分割されたモジュール導入は若干知らないといけないこともある
- 脱線: そもそもみんなライブラリの導入をたいてい雰囲気でやってる
- RELEASEビルドにQuick/Nimble入れないとか
- これまで雰囲気でやってきたんだろうから最悪気にしなくていい
- 脱線: そもそもみんなライブラリの導入をたいてい雰囲気でやってる
- ライブラリ関係の分割されたモジュール導入は若干知らないといけないこともある
- なこたーない
- 機能が増えて別モジュールと共有するものが増えたりすると成り行きでCommonモジュール行きになって肥大化する?
- 人間はすぐに肥大化という言葉を使いたがるが、機能が増えたらファイルが増えるのと一緒で気にしなくてもいいのでは?
- iOS SDKにはできることたくさんあるけど何かが肥大化して困ってんのかね?
- 人間はすぐに肥大化という言葉を使いたがるが、機能が増えたらファイルが増えるのと一緒で気にしなくてもいいのでは?
- 縦に分割するか横に分割するか
- どっちがいいかを考えなきゃいけないのは、何が目的で分割したいかに依存する
- モジュールを分割することで実装コストがかかる?
- DB
- 何を使ったらいい?
- 今現在は一長一短なのでメンバーが慣れてるのを使ったらいい(テックリードが決めたらいい)
- おすすめは?
- Core Data
- まったく何もわからないなら?
- もしかしたらSQLiteを使うほうが強烈なデメリットを感じることはないかもしれない
- Core DataもRealmもモバイアプリケーションに最適化されているため
- もしかしたらSQLiteを使うほうが強烈なデメリットを感じることはないかもしれない
- おすすめは?
- 今現在は一長一短なのでメンバーが慣れてるのを使ったらいい(テックリードが決めたらいい)
- 何を使ったらいい?
構造設計のパターン
TCA
関数型プログラミングとSwiftについての動画コンテンツ提供しているPoint-Freeのお二人が提供するRedux風なアプリ開発フレームワーク。
長所
Stateを変更できるのがReducerのみに限定している
- Stateを変更することをReducer関数のみに限定している
- ユーザ入力を受け付けるSwiftUI.TextFieldでさえ直接Stateは変更しない
- TextFieldは変更があればそれをReducerにアクションとして送りつけ
- BindingによってReducerから変化したStateをTextFieldに反映する
- ちなみにそのことで単一方向ではなく双方向バインディングを行っている
- (双方向バインディングが良いということではなく)
- TextFieldは変更があればそれをReducerにアクションとして送りつけ
- ユーザ入力を受け付けるSwiftUI.TextFieldでさえ直接Stateは変更しない
サンプルコードが充実している
https://github.com/pointfreeco/swift-composable-architecture/tree/main/Examples
- SwiftUI/UIKit版として用意されてたりする
- TCAのリポジトリ内にサンプルがあるため古いバージョンでないとサンプルが動作しないということはない
SwiftUIを採用することを前提にしているしUIKitでも問題なく使える
- SwiftUIを前提にしていてかつUIKitでも使いやすい
課題
従来の考え方とは違う考えを必要とする
複数Reducerをつなげるという前提が必要。Reducerの記述が増えすぎないように適切に分割していくが、Reducerをどのように分割すればよいかというのはこれまでにない考え方だろう。
サンプルにCore DataやRealmでの使い方がない
サンプルにCore DataやRealmでの使い方がないため自分で考えることは必要。SQLiteの使い方ならisowordsで検索すればいい。
過剰にDIしたくなる
Environmentという型を利用する前提だが、これがあればなんでもできる。
なんでもDIできるのでUseCaseやRepositoryなんかもDIしたくなるけど、数が増えて大変なのでやらないほうがいい。
副作用を実行する種類ごとのClient内部でUseCaseやRepositoryなんかを利用すればいい。テストコードではテスト用のClientの初期化をすればいい。
SwiftUIをどのように使うか
- 基本SwiftUIベースでRootを作る
- AppDelegateも持つことができる
- TCAはSwiftUIを採用することを前提にしているしUIKitでも問題なく使える
その他
iOSDC Japan 2021に出されてるプロポーザル
TCAに関係しそうなプロポーザルは5件くらい。
- The Composable Architectureを導入し、レガシーなアーキテクチャを刷新する
- TCA + GraphQL + SwiftUIでスケーラブルなアプリを作ろう
- The Composable Architecture がすごく良いので紹介したい 2021年秋
- Combine を使ったコードのテストを Scheduler で操る方法とその仕組み
- 【TCA】書きやすくて分かりやすい!Reducerのテストの基本
MVVM
MVVMはソフトウェアをModel・View・ViewModelの3要素に分割する。
課題
ViewModelをclassで作ってしまうとプロパティで画面の状態管理してしまいがち
- ViewModelで状態管理しようとしてしまうとViewModelの参照透過性を壊しがち
- リアクティブプログラミングでかつ参照透過性が壊れてたら修正するのがかなり難しい
- 従来のオブジェクト指向的にViewModelを扱ってしまうことから脱するのはかなり学習が必要
- 学習が進むとViewModelに状態を持たせず関数にできる
- RxSwift研究読本3 ViewModel設計パターン入門編を読んでみよう
- しかしそれは結局TCAのReducerに近づくのでTCA使えばいい
- 学習が進むとViewModelに状態を持たせず関数にできる
MVVMのModelをどうするかはチームで決める必要がある
- Modelをどうするかは自分でさらに細かく設計をする必要はある
- 例えば、画面間で同じデータソースを持つ
- 片方が変更されたら片方のViewにも反映されるなど
- 例えば、画面間で同じデータソースを持つ
- Swift/iOSでどうするかの答えはない
- Appleはわりと雑にモデリングしてるため
- 参考の一つとしてAndroid Blueprintのリポジトリやgoogle/ioschedがある
- LightweightなUseCaseは参考にはなる
- RepositoryやDataSourceも参考にはなるが...
- Swiftでどうするかやあなたのプロジェクトでどうするかは考えよう!
- LightweightなUseCaseは参考にはなる
SwiftUIをどのように使うか
- MVVMならプロトコルのObservableObjectに準拠したclassをViewModelと見立てて利用するはず
- 便利すぎる
@Published
は入力も出力もできてしまうのでインタフェースがわかりづらくなる- 頑張ってSwift言語仕様を知ってインタフェースを切り分けたりできるが学習と試行錯誤がいる
- しかしそれは結局TCAのReducerに近づくのでTCA使えばいい
- 頑張ってSwift言語仕様を知ってインタフェースを切り分けたりできるが学習と試行錯誤がいる
- 便利すぎる
その他
iOSDC Japan 2021に出されてるプロポーザル
MVVMに関係しそうなプロポーザルは1件。
MVP
MVVMはソフトウェアをModel・View・Presenterの3要素に分割する。
長所
リアクティブプログラミングの複雑さはないのでリアクティブプログラミング不慣れの人の書いたコードを修正したりする機会が減って良い
課題
Presenterのプロパティで画面の状態管理してしまいがち
- 非同期処理ライブラリについて
- コルーチンベースのSwift5.5のasync/awaitはiOS15以上からしか使えない
- google/promises はスレッドを止めたりできるのでasync/awaitできるPromiseライブラリなのでおすすめ
MVPのModelをどうするかはチームで決める必要がある
- ModelをどうするかはMVVMと同じ
非同期処理実行のための処理をどうするか
- MVPでリアクティブプログラミングのフレームワークを利用するパターンを選ぶのはやらないはず
- やるんだったらそもそもMVVMにしてるから
- だったら非同期処理どうするの?
- OSS
- google/promisesを利用してスレッドベースのasync/awaitおよびPromiseを利用する
- OSS
- だったら非同期処理どうするの?
- やるんだったらそもそもMVVMにしてるから
SwiftUIをどのように使うか
- UIKit部分はMVPとして、SwiftUI部分Viewにbindする
@Published
があるのでMVVM的にしたくなるかもしれない - もしくは後述するVIPERでのSwiftUI利用を行うかもしれない
iOSDC Japan 2021に出されてるプロポーザル
MVPに関係しそうなプロポーザルは1件。
VIPER
VIPERはView, Interactor, Presenter, Routerのメインパーツに分けましょうということ。
MVPよりさらに細かく分け、Routerは画面遷移を決定する役割を持つ。画面遷移を決定を行うことは画面を組み立てることでもあり、結果的にパーツの依存関係を注入するのにも使われる。2つの役割があるのではなく、結果としてそうなるだけ。
長所
InteractorやRouterのように細かくやることが決まっているのは迷う点が少ない
- とくにInteractorが何をやるかというのは決めてしまえば楽
- Routerとして分離することでRouterのテストコードもパターンをつかみやすい
課題
いろいろなVIPERがある問題
- 最初のVIPERはobjc.ioに投稿された記事でSwiftが誕生する前
- つまりルーツを知ってもObjective-Cで書かれたものが発見されるだけでそのルーツにほぼ意味はない
- V.I.P.E.Rの連携や詳細はプロジェクトごとに違いが出やすいのでWeb上の記事を見ても混乱するはず
- VIPER研究読本を見よう
V.I.P.E.Rのメインパーツを都度作る必要がある
メインパーツはそれぞれ依存しあわないようにプロトコルがある。いわゆるボイラープレートなコードは必要になるが、余裕があったらテンプレートから作成すればいい。
非同期処理実行のための処理をどうするか
- MVPと同様。おそらくgoogle/promisesを利用するのが良さそう
SwiftUIをどのように使うか
- VIPERは遷移のRouterがあるので、あくまでViewとしてSwiftUIを使うようなやり方に留めるのが良さそう
- UIKitベースで作ってUIViewとして変換したSwiftUI.Viewを使う
- クックパッド社のVIPERの記事でSwiftUIを使ってる部分の記載があり参考になる
- クロージャをSwiftUIに渡してユーザアクションからクロージャを呼び出してイベントを伝達する
- クックパッド社のVIPERの記事でSwiftUIを使ってる部分の記載があり参考になる
- UIKitベースで作ってUIViewとして変換したSwiftUI.Viewを使う
iOSDC Japan 2021に出されてるプロポーザル
VIPERに関係しそうなプロポーザルは1件。
モジュール分割
モジュール分割はやるべきだと思う。0からアプリを作るときにもまず考えるべき。もちろん、分割したものを1つに戻すことも時間があれば可能なので、やってみればいい。
モジュール分割の長所
機能ごとにわけておけば機能を外注されても不安感は減る
1つのアプリをもし自分たち以外のよその組織に外注するということになっても不安感は減る。なぜならそのモジュールまるごとあとで作り直せるから。
さらにモジュール分割されているメリットは標準的な部品、例えばStringやIntやDateもしくはUIViewControllerなどにアプリ固有のロジックを処理するextensionを生やされてもそれが自分たちまで汚染されることを気にしなくていい。具体例でいうと機能ごとにDateの文字列フォーマットのが違うことがあるかもしれない。そのルールが違うことは利用時に並べて判断しなけりゃいけないのは面倒で、そもそも機能が違うんだから、片方を使うときにもう片方が考慮にも入らない状況が望ましい。だけどDateのextension生やされたらそうもできない。
そういう分離した機能モジュール内でしか影響範囲が抑えられるなら、まずはそれでいい。いちばんやっかいなのは、自分も昨日を実装してめちゃくちゃ忙しいのに、コードレビューでそんなところに時間を費やすところだと思う(しかもそれが日本語とは限らない)。もし同僚とかならディスカッションすることに対してそれがコストだとは思えないけど、そうではないパターンもある。事前に完璧なコーディング規約を共有しろよと思うだろうけどみんな忙しいわけで、そこに書いてないことのほうが多いわけで...。完璧なコーディング規約をつくる時間を捻出しないといけないね、みたいな話になるとほんとうに厄介。本末転倒っぽくなります。
レイヤーごとに分割する長所
前述extensionについて機能ごとに分けてたら汚染を気にしなくていい、みたいなことを書いたがこれがレイヤーごとでも同じことが言えそう。たとえばWeb APIのリクエストに使う文字列フォーマットと表示用フォーマットは違う。だからレイヤーごとにそれを用意する、依存されているレイヤーは依存してきているレイヤーのメソッドなんて呼び出せないので悩みは減る。
OSS
RxSwift/ReactiveSwiftを使うかどうか
AppleのCombineフレームワークがある今、そしてiOS 15からのasync/awaitが待ってる現状ではRxSwift/ReactiveSwiftを自分は使わないとは思います。
google/promises
MVPやVIPERを使って非同期処理どうするかという件については、
iOS 15未満でSwift 5.5のasync/awaitが使えないので、まずはgoogle/promisesをとりあえず使ってスレッドベースのasync/awaitを使うというのはありかもしれません。
まとめ
2021年でもまだ決め手のようなものはないし、私だったらTCAやVIPERを候補にモジュール分割してなるべくDB使わずに作って必要になったらCore Dataを採用すると思います。
みなさんもそれぞれ思うところはあるでしょうからアウトプットしてみてください。