Kyome流SwiftUI×AppKitでの個人開発アーキテクチャ
個人開発しているmacOSアプリのメンテナンス性を向上するためにアーキテクチャの導入を進めてきました[1]が、ようやくある程度納得のいく形になったので文書化します。
アーキテクチャを導入するモチベーション
- 機能追加の手順をテンプレート化し、思考のリソースを抑えつつ品質を担保したい。
- SwiftUIのプレビューやテストのモックなどで依存性の注入ができるようにしたい。
- OSSで公開しているものもあるので、第三者がコントリビューションしやすいコードにしたい。
要件
- あくまで基本は個人開発向けのため、複雑でないこと
- 特別なフレームワークやライブラリを必要としないこと
アーキテクチャの概要
依存の方向を一方向にするように心がけたレイヤードなアーキテクチャです。
大きくData・Domain・Presentationの3つの層で構成します。
- Data層:データの読み書きを担当
- Entity
- Repository
- Domain層:ビジネスロジックを担当
- AppModel
- Model
- ViewModel
- Presentation層:UIの提供を担当
- App
- View
各項目の詳細
Data層:Entity
structやenumなど全体で利用するデータの型を定義します。
ここでは複雑なビジネスロジックは書きません。
私はアプリ固有のエラーの種類もここで定義することにしました。
Data層:Repository
UserDefaultsやファイル(JSONやCSV)、データベースの読み書きを処理します。
また、データが更新されたことを通知する場合もあります。
RepositoryはEntity層で定義したデータの型以外には依存しません。
命名規則として、〇〇Repositoryのようにします。依存性の注入を実現するために、AnyObjectに準拠したprotocolでインタフェースを定義し、それに準拠した〇〇RepositoryImplをclassで定義しデータアクセスの処理を書きます。また、プレビュー用にダミーの処理を定義した〇〇RepositoryMockもセットで用意します。
Domain層:AppModel
1つのアプリにつき1つだけ存在する特殊なModelです。
アプリ起動直後・終了直前のイベントハンドリングをすることと、一部のRepositoryとModel、Viewのインスタンスをシングルトンで保持することが責務です。それぞれのインスタンスの生成時に依存性の注入も行います。この責務以外のことはなるべく何もさせません。
命名規則として、アプリ名+AppModelのようにします。ViewModelを初期化する際にRepositoryやModelを渡すために、ObservableObjectに準拠したprotocolでインタフェースを定義します。それに準拠した〇〇AppModelImplをclassで定義します。
Domain層:Model
Presentation層と直接関係のないビジネスロジックをここに書きます。
ModelはRepositoryや別のModelに依存することができます。ただし、AppModelへの依存と三つ巴のように循環するような依存は禁止します。

命名規則として、〇〇Modelのようにします。依存性の注入を実現するために、AnyObjectに準拠したprotocolでインタフェースを定義し、それに準拠した〇〇ModelImplをclassで定義しビジネスロジックを書きます。また、プレビュー用にダミーの処理を定義した〇〇ModelMockもセットで用意します。
Domain層:ViewModel
Viewに対応するModelとして、Viewで表示すべきデータの手配とユーザーのアクションをModelに伝達する役割を担当します。
ViewModelはRepositoryやModelに依存することができます。ただし、AppModelと別のViewModelへの依存は禁止します。(AppModelのもつRepositoryやModelを受け取ることは許します。)

命名規則として、View名+Modelのようにします。依存性の注入を実現するために、AnyObjectまたはObservableObjectに準拠したprotocolでインタフェースを定義し、それに準拠したView名+ModelImplをclassで定義しビジネスロジックを書きます。また、プレビュー用にダミーの処理を定義したView名+ModelMockもセットで用意します。
また、macOS向けの特殊なViewModelとして、WindowModelとMenuBarModelも用意します。それぞれウインドウの管理とメニューバーの管理を担当します。
Presentation層:App
SwiftUIベースのアプリなら1つ存在しているアプリ名+Appの@mainなstructです。
AppModelを保持し、WindowGroupやWindow、Settingsなどウインドウの定義をします。
RepositoryやModelの受け渡しのために必要であれば、.environmentObject()でViewにAppModelを流します。
Presentation層:View
基本的にはSwiftUIのViewです。ウインドウ内のビューヒエラルキーの中で最も低いViewには対応するViewModelを持たせます。(ViewはViewModelに依存することができます。)

RepositoryやModelをViewModelに渡すために必要であれば、@EnvironmentObjectでAppModelを持ちます。プレビューにはRepositoryMockやModelMockを使います。
常駐型アプリの場合は、macOS向けの特殊なViewとしてMenuBarを用意します。
データやイベントの伝達手段

基本的に依存の方向性に対して決まる。
- 依存先へは
funcで伝達する。 - 依存先からの伝達は
Combineで購読して受け取る。
例外的にAppModelからModelへの伝達は依存の方向性を無視してfuncを用いる。
実装例
-
ほとんどのアプリがアーキテクチャのことをほとんど知らない学生時代に作ったもので、まともな設計ではありませんでした。 ↩︎
Discussion