Overview
iOS13から SwiftUIとCombineが導入され、Swift5.5から async awaitが導入され、既存のUIKitの実装から大きく変化が求められてきています。
SwiftUIの紹介から数年が経ったことで、大分関連記事や事例紹介が増えてきました。iOS13未満をサポート対象にしていたアプリも徐々に減り、これらのリファクタリングタスクが必要となってきています。
技術選定の観点
この記事の大きな焦点は2点です。
- SwiftUIの提唱する “Single Source of Truth” と Combine実装の共存
- Combineの非同期処理と Swiftのasync/awaitによる非同期処理の使い分け
私はこれらの問題をUI アーキテクチャで綺麗に実装することで、それぞれの強みを最大限に生かすことができると考えます。以下の図のようにMVVM アーキテクチャを設計します。
- UIのみのデータ更新
- Appleの提唱する “Source of Truth”に倣います
- View Hierarchy の最高層でPropertyを保持し、データの更新に伴ってHierarchical Top-Downでレンダリングが更新される
- FlaxやReduxのような単一方向データフローになるようにする
- DomainとUIの繋ぎのデータ管理
- Combineを用いて、ObservableObjectでデータ変化をSubscribeします
- Viewと双方向バインディングするが、それぞれのデータフローを単一フローになるように設計する
- Main Threadから切り離された非同期的な動作を可能にする
- Domainのデータハンドリング
- Domainロジックは機能別にモジュール化されるように設計し、UIロジックにDependency Injectionすることで依存関係を明確にする
- 関数型プログラミングのような実装にすることで、UIロジックから関数が呼ばれることで処理される仕様とする
- Observableパターンは用いずに、Swiftの async / await機能を用いることで、シンプルにかつ非同期的に処理が実行される
View
SwiftUIでは、全てのView内のデータは唯一のデータ源から来ていることが定義されています。
“Every piece of data that you read in your view hierarchy has a source of truth, and it should always have a single source of truth”
一方、UIKitでは、ViewはClass型でしたが、SwiftUIではStruct型で定義されます。Viewは状態を保持する代わりに、データが更新されるたびにBlueprintをレンダリングする仕組みになっています。これは、SwiftUIにおいて非常に大きな変更点です。
SwiftUIには、多くの新しいProperty WrapperがView用に提供されており、これらを適切に使用することで、単一方向データフローを実現することができます。また、“single source of truth”ということは、すべてのスレッドからの変更に対しても安全に行うことができる点が非常にメリットです。
“You can safely mutate state properties from any thread.”
Stateは、必要となる階層の最上層のViewに定義されます。下層のViewはこの最上層のViewのStateをBindingまたはData Copyして利用します。Bindingを使用することでChildViewに対して読み書きが可能ですが、letを使用することでChildViewに対して読み込みのみが可能になります。
ViewModel
ViewModelはMVVMアーキテクチャにおいて、UIロジックとドメインロジックを双方向バインディングする方法です。Observerパターンにより、ドメインロジックの処理結果を購読したり、メインスレッドから切り離して処理を行います。このためCombineというiOS13から導入された技術を採用することが望ましいです。RxSwiftは今でも非常に有用なOpen SourceのReactive Programmingライブラリですが、iOSがCombineに標準対応しているため、できるだけCombineを利用することを推奨します。
(NotificationCenter.default.publisher 、 Timer.publish() や NSObject.KeyValueObservingPublisher などのようにiOSが標準でサポートしています。)
MVVMの双方向バイディングはデメリットとして、データ変更が双方向であることが挙げられます。Reduxなどのように一方向性でのデータ変更する仕組みを紹介します。
MVVMの一方向性を強化するために、ViewModelはInput、Output、データフローの定義の3つから構成されます。Inputはデータ変更のリクエストを定義し、Outputは読み取り専用のデータを提供することで、単方向のデータフローを実現します。CombineのObservableObjectとPublishedはこの一方向性を実現する鍵となります。ObservableObjectを準拠することで、データ変更時にViewに再読み込みされます。Published Property WrapperはViewの読み込みをSwiftUIに伝えます。
Model
DomainロジックはUIロジックとは別に、関心分離を行います。関心分離 (Separation of Concerns、SoC) は、プログラムを関心(責任、目的)ごとに分離して構成要素を作ることを意味します。
機能ごとにモジュール化することで、必要な機能を必要なUIに紐付けることができます(これはDependency Injectionとも呼ばれます)。
例えば、図のように、決済情報や決済処理が必要なUIに対して、Purchase Serviceという決済関連の機能を持つモジュールを適用させ、その他のUIから参照できないようにすることができます。同様に、認証情報や認証処理が必要なUIに対して、Auth Serviceを紐付けることで、どの画面で認証情報が必要なのかが簡単に理解できるようになります。
この関心分離された、機能ごとのモデルを、モジュールやSDKのような関数呼び出しで利用できるように実装することで、Combineとは異なる async / awaitを用いた記述が可能になります。
CombineとSwift async / awaitは、非同期処理に関する技術として重複することがありますが、GCD(Grand Central Dispatch)の記述に代わって使用される方が多いとされます。
まとめ
SwiftUI, MVVM, Combine, async/await など様々な新技術に対して、Design Architecture の観点で技術選定方法をまとめてみました。
今後のリファクタリングで参考にしていただけましたら幸いです。