🌊

SwiftUIでVIPERのiOSアプリをつくってみた

2021/09/29に公開

前回の続きです。
3日で4アーキテクチャー実装して記事書いたのでさすがに疲れてきました……

前回の記事

https://zenn.dev/st43/articles/a8678e270b9f02

参考書

アーキテクチャーに関する知識は、こちら↓を参照しています。
以下の文中で「参考書」と出てきたらこの本のことを指します。

https://peaks.cc/books/iOS_architecture

環境

  • Xcode: 13.0
  • iOS: 15.0
  • Swift: 5.5

Clean Architectureとは

VIPERについて触れる前に、Clean Architectureについて触れましょう。
VIPERはClean Architectureの派生と言えるからです。

  • 2012年にUncle Bobが提唱したアーキテクチャー
  • 2003年にEric Evansの提唱したドメイン駆動設計の具体的なアーキテクチャーとして、Hexagonal ArchitectureやOnion Architectureが提唱された。そのコンセプトを統合するために生まれたのがClean Architecture

  • ソフトウェアの中で、変更の多い場所のUI、DB、デバイスとの接続などから、本来変更の少ないはずであるビジネスロジックをEntityとして切り出すことで、技術的な目まぐるしい変更からビジネスロジックを守ろう、というのが目的

Entity?

  • Clean Architectureの指すEntityはドメインとほぼ同義
  • 一方、ドメイン駆動設計におけるEntityは「同一性によって定義されるオブジェクト」

Entity in DDD ≠ Entity in Clean Architecture - Qiita

  • Entityの本来の意味が「(哲学的な意味で)実在」なので、意味としてはどちらも正しいが、指しているものが全然違うので混乱する
  • (Entityは単なる存在、と訳すと意味がわからないと思う。例えば太郎くんと花子さんがいるとしたら、二人は人間というEntityで分類するならば、等しいと言える。男性というEntityで分類するならば、違うものになる。ドメイン駆動設計のEntityは、DB理論のEntityに近い)

Clean Architectureのアーキテクチャー

  • Entity: 純粋なビジネスロジックを閉じこめるレイヤー。たとえば「銀行がローンにN%の利子をつけている」みたいな例
  • Use Case: アプリケーションの機能を実現するレイヤー
  • インターフェイスアダプター: データやイベントを変換するためのレイヤー。MVxアーキテクチャーにおけるPresenterやControllerが該当する
  • フレームワークとドライバ: UI、DB、デバイスドライバ、Web APIクライアント。Flutterならばネイティブコードがこのレイヤーになる
  • 基本的にEntityはあまり書けることがなくて、小さくなるはず。オニオン図の面積がそのままコード量になるイメージ

Clean Architectureのデータフロー

  • Clean Architectureで重要なのは、外→内の参照はあっても、内→外の参照がないこと
  • たとえば一番外側にいるViewから内にいるPresenterは参照を持つが、PresenterはViewのインスタンスを参照しない
    • そうは言っても、内→外のデータを渡す場面は絶対にある
    • その場合、非同期処理として実装する。選択肢はデリゲート、コールバック(手段としてはメソッドの戻り値にする方法もあるが、イマイチ)などを内側のレイヤーに用意させる。今ならCombine使う手もあるのかな?
    • また同じレイヤー内の通信は、依存関係逆転の原則を使う。要はprotocol定義して、それに依存させる
      • ただprotocolを定義するのは、インターフェイスアダプターの内外だけにすべき、らしい
      • 特にEntityは単純なコマンドとクエリだけを持つように設計せよ、とのこと

VIPERとは

  • 2016年にこちらのブログで発表されたアーキテクチャー
  • View, Interactor, Presenter, Entity, Routerで構成される
  • 雑に説明すると、Clean Architecture + Router
  • Routerは画面遷移を担当する
  • Interactore = Use Case

https://qiita.com/hicka04/items/09534b5daffec33b2bec

  • MVPのModelをInteractorとEntityに分解して、Routerを足すと、VIPERになる
    • VIPER - R = VIPE
    • I + E = M
    • (I + E) + VP = MVP

実装してみた

実装はこちらです。
今日はかなり疲労しておりまして、コメントの更新まではさすがにしないことにしました……

https://github.com/0si43/iOS-architecture-training/tree/VIPER

ブランチ的にはMVVMのときの実装からVIPER化しました。
構成図としてはこんな感じです。

Clean Architectureのオニオン図に合わせると、View -> Presenter -> Interactor -> Entityという流れができているので、OKです。
……書いてて思ったんですが、InteractorでAPI通信してますけど、これホントは「フレームワークとドライバ」でやることなので、Use Caseとはわけないと、Clean Architectureの思想としてはダメですね。
VIPERは思想というよりは現場で生まれたノウハウに近いので、実装しやすければなんでもいいんだよ! というところでしょうか。

公式のVIPERはViewController(のView)を前提にしていたので、View - Presenter間は手続き的な処理を想定していたんですが、
今回はViewがSwiftUIなので、CombineのPub/Sub関係にしました。

EntityとInteractorに切るのは容易だった

iOS開発においては、Entityは単なるStruct(大抵Codableな)になる場合が多いので、切り分けは容易でした。
元々MVCでアプリつくったときからファイル的にもクラス的にもわけていたものを、名前変えるだけですみました。

ただ厳密に考えていくと、どこまでがEntityで、どこまでがInteractorなのかは個人差出るところだと思います。
Modelっていう粒度が大きいので、分割したい気持ちはわかるんですが、Entity/Interactorの区分は開発者によって個性が出ちゃいそうな予感がしました。

SwiftUIのRouter

というわけで、VIPERのVIPEまでつくった(というか既存コードの名前変えただけですみました)のですが、Routerですこし悩みました。
参考書の12章がRouter編で、わかりやすく書かれてるんですが、バリバリView Controller前提で、SwiftUIのRouterはどうしようか悩みました。
そんなときググったら、

https://blog.personal-factory.com/2020/05/31/viper-architecture-in-swiftui/

こちらが出てきました。
これを参考にして、こんな実装にしました。

import SwiftUI

/// ユーザー検索のRouter
struct UserSearchRouter {
    func navigationLink(user: User) -> some View {
        return NavigationLink(destination: RepositoryView(repositoryUrlString: user.reposUrl)) {
            UserRow(user: user)
        }
    }
}

struct UserSearchRouter_Previews: PreviewProvider {
    static var previews: some View {
        UserSearchRouter().navigationLink(user: User.mockUser)
    }
}

戻り値をsome Viewにするのがポイントです。
これを呼び出すViewは、

presenter.router.navigationLink(user: user)

という風に、Presenter経由でRouterを使います。
まあ別にもはやViewが直接持っても変わらないような気がしますが、

一応元のVIPERの構成がPresenterがRouterを持つ設計になっていたので、それにならいました。

CoordinatorとRouterの違い

参考書読んでいて、RouterってCoordinatorと何が違うんだろう? と素朴な疑問を抱きました。

https://stackoverflow.com/questions/55555285/routers-vs-coordinators

StackOverFlowでも同じ質問をしている人がいました。
Coordinatorは階層化してアプリ全体の画面遷移を管理しているけれど、Routerは特定のViewの画面遷移だけを行なっているだけ、という理解でいいみたいです。

(拙訳)
よく似てますが、Routerは単一のViewControllerのルーティングを管理しているだけで、Coordinatorは全体のフローを担っています。
なので、たとえ取得したのが子Coordinatorであっても、全体の画面遷移のフローを気にしないといけない訳です。

(原文)
They are both similar but router manages routing from a single view controller, while coordinator takes care of entire flow.
So in a given Coordinator you might get child coordinators which makes you take care of the entire flow.

まとめ

というわけで、VIPER(とClean Architecture)でした。

VIPER自体は現場生まれたアーキテクチャーということもあり、各要素の責務がちゃんと腹落ちすれば、意外とキレイに書けるなと思いました。
人気があるアーキテクチャーになったのも頷ける気がしました。

Clean Architectureは個人的にはかなり抵抗感あるものでした。
理論的にはクリーンになるはず、というのはわかるんですが、チーム開発でこれ導入して上手くいく未来が見えないんですよね……
みんなが聖書の内容ちゃんと理解したら世界から戦争なくなる、みたいな話と同じレベルじゃないかと思います。

アジャイルもそうなんですが、「試したけど現場で上手くいかなかったよ」への反論として、
「それは参加者がキチンとClean Architecture(ないしアジャイル)を理解してなかったからだよ」というのがあるんですが、
なんかやっぱ多くの人間が誤解する理論って、どれだけすばらしいものでも何かが不自然なんだと思うんですよね。

ただClean Architectureの根幹の、ドメインロジックを固めて、UIやDBやWebの速い変更から守るっていう思想は、
Clean Architectureを使わなかったとしても、システムをつくる上で大事な考えだと思いました。
特定のフレームワークに依存しない、かつキレイな設計を考え抜くと、同じところにたどりつきそうな気がします。

参考書的にはこれで終わりなんですが、SwiftUIと相性がいいというTCAが気になってるので、明日そっちで実装して最後にしようと思います。

Discussion