始めに
SwiftUI発表を見たときは、Swift の発表より大きな衝撃を受けました。
発表翌日は Swift 界隈はザワザワしていました。
長らく Storyboard と格闘していた我々 iOS エンジニアとしては悲願だった技術なのではないでしょうか。
ただし、残念ながら SwiftUI を使ったアプリは、iOS 13 以降のみサポートするという、大きな制限があるため、本格的に使われ始めるのは 2 年以上時間を要するといわれています。
SwiftUI はレイアウト作成の技術のためフォーカスされていますが、一緒に発表された、Combine Framework (※動画) と合わせて活用することが前提の作りとなっています。
今回はこの Combine Framework に焦点を当てて記事を書きたいと思います。
今では iOS アプリは、MVC、MVP、MVVM、Flux、Redux、Clean Architecture(VIPPER) 、Micro View Controller …etc などのよう々なアーキテクチャを使い開発されてきていますが、多くの現場では MVC でのアプローチでの開発を行っています。
SwiftUI + Combine Framework の発表は、MVC から MVVM への Apple から開発者に向けて、「2 年後には、開発のメインストリームが MVVM に移るからしっかり勉強しておいてね」といったメッセージのように感じました。 MVC 一択脳の方は、MVVM 脳を構築する必要があります。
MVVM の記事を書こうとも考えましたが、すでに世の中に SwiftUI を使った MVVM の良質な記事をいくつか目にするのでそれは見送ります。
技術書やネット記事などで目にしたこともあるかもしれませんが、MVC 開発で起こっている大きな問題のひとつに、ViewController が肥大化する問題(Fat / Massive ViewController)というものがあります。MVVM を齧ったぐらいの知識で構築すると、Massive ViewModel 化するように思えてなりません。
近年 iPhone アプリの Android 化が止まらないと言われるように、昔の iPhone では想像できないほど、iPhone の高機能化(裏を返すと操作が難しくなってきた)してきています。
アプリを取り巻く OS やハードウェアも、iPhone 端末サイズのバリエーションの増加、iPad OS の発表、Split View …etc のように変化してきています。
データ更新のトリガもいろいろあり、画面遷移、ユーザーのボタンタップ、フォアグラウンド復帰、通信状態の変化、Push 通知による遷移、ポーリング、 …etc
MVVM でこれからの問題を解決しようとすると IN と OUT をしっかり設計したうえで実装しないと複雑化し、保守不能な負債コード(従来の MVC より技術難易度が高すぎる分より厳しい状況)が生まれる未来が想像できます。
Apple はツールとして Xcode やフレームワークとして UIKit、SwiftUI、Combine Framework、…etc は提供してくれるのだが、開発ガイドライン(HIG は UI や UX などのデザインに関するものとしてあるが)はないので、開発者のレベルや思想の違いによりアプリの構造がまったく別物になっています。
実際、iOS アプリ開発で関わらせていただいた現場によって、まったく別のアプローチで開発しておりました。
今回は Redux という開発ガイドラインを使ってアプリ開発をしてみたいと思います。Redux は考え方であり、特定のフレームワークに依存しないものとしてとらえているため、意図的に、開発ガイドライン と書かせていただきました。
Redux とは
Redux は、Flux アーキテクチャ(2014 年 5 月の F8 のセッションにて発表)のアイデアに影響を受けた、 Dan Abrmov さんによって、 2015 年 8 月にリリース(Web アプリ向け)されたものです。
Dan Abrmov さんは、複雑性の原因を、変化(mutation)と非同期性(asynchronicity)が組み合わさったものであると考えており、それを解決するための Redux は次の特徴を備えています。
- Flux アーキテクチャの情報の伝播を 1 方向に制限する特徴を踏襲し、いつどのように 更新が起きるかを明瞭にする
- Elm アーキテクチャの純粋関数による副作用の排除や、イミュータブルな状態表現の 制約を踏襲し、厳格で整合性のとれた状態管理を実現する
Redux を構成する要素
・Action:
Storeに送られる情報です。情報であり、処理は持ちません。
・State:
アプリケーションの状態を表現するデータ集合です。あくまでデータなので処理は持ちません。
・Reducer:
Action と Stateの入力を受けて、新たなStateを出力する関数です。純粋関数として記述されます。
・Store:
StateとReducerを保持するアプリケーションの単一インスタンスです。Action、State はただのデータで、Reducer は純粋関数、じゃあ API 通信や処理はどこにいくのだ?
と Redux を学んだ時に疑問に思いました。
Redux では、非同期な処理は、Redux-thunk という middle ware に処理を任せます。
この Redux-thunk は Action を Reducer の間に来る処理となります。
Redux の守るべき原則
- Single source of truth(信頼できる唯一の情報源)
- State in read-only(state は読み取り専用にする)
- Changes are made with pure functions(変更はすべて純粋関数で行われる)
Redux を構成する要素と守る原則を交えて解説していきます。
Single source of truth(信頼できる唯一の情報源)
Redux では、 Store の中に State を保持していますが、この State は、アプリケーションの中でユニークなものになります。
MVC などのアプリで、記事購読アプリを作ろうとした場合は、記事一覧、ユーザー情報、ユーザーのお気に入り情報などの情報を別々のデータとして管理していた思いますが、Redux を使うと、次のようなツリーをひとつ State で管理できます。
Root ━ 記事一覧 ━━━━━━━━━━━ 記事タイトル
┃ ┣ 記事サマリー
┃ ┗ 著者
┣ ユーザー情報
┗ ユーザーのお気に入り情報Flux では、画面毎や機能毎に State をもつのが Redux と大きく異る点です。
State in read-only(state は読み取り専用にする)
「State をイミュータブルで表現する」とは、作成された State が値を変えることのできない不変なインスタンスであることを意味しています。 Reducer により新たな State が生成されるまでの間、 View レイヤで参照している現在の State はまったく変更されないことが保証されます。
State は、View や ViewController から直接変更することはできません、Reducer によってのみ生み出されます。 Reducer は、State と Action を引数に新たな State を返します。 State はイミュータブルなため、Reducer に記述されたビジネスロジックの実行結果をコピーした State に適用し、新たな State を作成します。
少し話が脱線するが、Swift には”値渡し”と”参照渡し”がありますが、画面表示に表示するデータは”値渡し”のものを利用したほうがよいでしょう。参照渡しだと、参照していた奥深くのデータが知らぬ間に書き換わっていて期待した表示にならないという不具合に遭遇することにもなります。
State を生み出す Reducer について補足させてもらうと Reducer は、純粋関数で書くことが求められます。
純粋関数の特性を次に列挙します。
- 与えられた要素や関数外の要素を変化させず、戻り値以外の出力を行わない(副作用の排除)
- 取り扱うすべての要素が引数として宣言されている(引数以外の要素を参照しない)
- 入力に対して出力が常に一意である(同じ入力には常に同じ出力を返す)
Redux の全体像
View (UIKit の ViewController、SwiftUI の View) は画面を表示するための情報として State ツリー、アクション(ボタンタップなど)として Action のディスパッチを行いますが、その裏にある Action の実行+Reducer の存在は意識しません。 Action が実行され、Reducer により新たな State が作成されたら、その値が購読している※1 View に引き渡されます。 ※1 購読するには、RxSwift のデータバインドや、NotificationCenter が使われてますが、SwiftUI では @ObservableObject を使用します。
次の記事ではいよいよ実際のコードを交えて Redux による実装方法解説していきます。