SwiftUIコースのモダン化
前回のDogGramの近代改修、8割方おわったので記事にします。
正直別物になりました。まだまだバグバグですけど、ちょっと大きめの規模のSwiftUIを組みたい人には参考になると思い記事にします。
まず大前提として、Udemyの DogGram は受講済みとします。受けてない人で、ちょい大きめのアプリを組んでみたい人はぜひ受けてみてください(そして自分の手で改修してください)
やったこと
- Udemyで受講したコース DogGram のコードを改修した
- 改修内容
- クリーンアーキテクチャ
- FirebaseFirestoreSwift対応
- Swift Concurrency(async/await)対応
- SwiftPackageManagerを用いた依存管理
Viewのレイアウトは流用してますが それ以外はフルスクラッチに近い です。個別に。
クリーンアーキテクチャ
元のDogGramのコードは基本的に状態を View
にもたせるというシンプルな構造でした。@State var
を使った単純なアプリ作成でよく使う方法ですね。
ただこれが この規模のアプリには適してません。 正直破綻一歩手間で、ここから何か画面を追加しようとするとおそらくそれなりの改修、とくに複数のView階層をまたいだPropertyの受け渡しをしたくなると地獄を見ると思います。
さらにデータ管理レイヤーに 直接Viewを渡して、完了後に特定メソッドをコール という信じれないことをしています...最低でもInterface(protocol)かまそうや...
このため、
- Viewとデータ管理層を明確に分離する
- データ管理層からは絶対にViewを参照しない
- View自体にStateをもたせるのをやめる
- Stateを管理するクラス、
ViewModel
を作る
- Stateを管理するクラス、
というのを目標にします。目的はクラスごとの役割の分離と明確化、そしてレイヤー化です。つまりクリーンアーキテクチャです。
今回はGoogle(Android)の アーキテクチャ解説 にある分け方にしたがいました
- UILayer
- View ViewModel
- DomainLayer
- UseCase
- DataLayer
- Repository
- 元コードの
XxService
は全てここにくる
個別に何をするかは上記Googleの解説を参考にしてください。
FirebaseFirestoreSwift対応
元のコードでは地道にFirestoreからのデータをパースしているのですが、今なら FirebaseFirestoreSwift
を使うことでSwiftの標準的な Codable(Encodable/Decodable)
を使ってデータを処理出来ます。
そんなわけで対応させました。だいぶコードがスッキリしたはずです。
同時に、このへんのクラス(Struct)を XxModel
から Entity
に改名。
個人的にですが、Model
はビジネスロジックを含む印象があるんですよね。Entity
にはその意味はない(と思う)。そのため改名しました。Modelは意味が広すぎる。
このへんは突っ込むと終わりのない議論になるので、個人の好みと思ってください。
さらにちょっと特殊なこととして、Firebaseをxcframeworkで組み込みました。これはビルド高速化のためです。
SwiftPackageManager
を使うとクリーンのたびにリポジトリからフェッチしなおすようで、Firebase規模のプロジェクトだとめちゃくちゃ時間かかります。数分単位(M1 MacbookAir)。まってられません。
Cocoapodsは後々SwiftPMで依存管理するつもりだったので対象外です。
そのためビルド済みFrameworkをプロジェクトに組み込みました。ひと手間かかりますが、ひと手間でその後の数分を何度も短縮できるのでメリット大きいです。
話それますが、自分はこの辺の都合でCarthageが好きです。
ひと手間かかりますが、ひと手間かけた後は最速のビルド時間を享受できます。
セットアップは楽だけど以降ずっと時間がかかる、よりセットアップは時間かかるけど以降は早い ほうが好き。だって総合的に時短になるじゃないですか。 もっと注目されていいと思うんですけどね。さみしい。
Swift Concurrency対応
令和4年のiOSアプリらしく、async/awaitに対応 させます。
これは当該コース公開時になかった要素なのでコースに含まれていないのはしょうがないです。が、モダンなiOSアプリなら対応必須 でしょう。
今回はめんどくさかったのでさらに iOS15専用 にしました。開発中のXcode13.1時点では async/awaitがiOS15専用だったというだけですけどね。
コードの見通しが数段よくなりますし、ぜひコース内容もアプデしてほしいですね。
基本的には DataService
などのクラスにあるコールバックハンドラを駆逐、asyncにするだけです。
さらに前述の FirebaseFirestoreSwift
はasync/awaitの拡張があります。もちろん採用。コールバックを削除し、asyncにして見通しを良くしていきます。
対応してない部分は withCheckedThrowingContinuation
で自作します。
もっとモダンにするため、リポジトリ層を actor
にしスレッドセーフを担保します。もちろん ViewModel は @MainActor
アノテーションをつけます。
もうやりたい放題ですね(笑)。「今自分が一から作るならこうする」を詰め込みました。
SwiftPackageManagerを用いた依存管理
最後、〆です。上記で作った3つのレイヤー、DataLayer/DomainLayer/UILayer
をそれぞれ独立したライブラリにし、SwiftPackageManagerで依存を管理 します。
こうするメリットが以下
- 逆方向の参照ができない。ビルドエラーになる
- Ex: DataLayerはDomainLayerのクラスを参照できない
public
が明示的な意味を持つようになる- 分離によるビルド高速化
もちろん、古典的なXcodeProjectでも依存管理できます。が、モダンに行きたいじゃないですか(笑)。そんなわけでSwiftPMで依存を記述しました。
メインのProjectは当該SwiftPMを参照し、エントリーポイントのファイルが一個あるだけです。
が、SwiftPMはまだちょっとが不安定ですね。現時点でならXcodeProjの古典的依存管理のほうが安定性が高いです。
まだ改修したいところ
一通りやったのですが、まだ足りてないのが以下です
- エラーハンドリング
- 単なる手抜きです
- やってないだけ
- UnitTest記述
- Data/Domain層は全てProtocolを挟むようにしたので実装差し替えはできるはず
- なのでテストの記述がしやすいはず
- しかし未検証。まだ書いていない
- 冗長なView階層構造
- ViewModelを作成、保持するためのView階層を挟んでいるのでちょっとが冗長
- 「意図せずViewModelが再生成される」のを防ぐためのものではあるが...
- もっといい仕組みがほしい
まぁ気が向いたら...
まとめ
そんなわけでやりたいことのほとんどは詰め込み終わりました。満足です。
テストまで書こうと思ったのですが、キリいいところまで来てしまったせいでモチベが急降下しました(笑)
めちゃくちゃ改修加えてますが、DogGramのコース自体はいいと思います。特に次に進みたい初心者には。
ただしアプリ規模とアーキテクチャの不一致が気になった、というところでしょうか。
そういう対象の人はぜひコースを受けてみてください。きっと発見があると思います。