TUNAG iOSアプリのチャット機能をVIPERアーキテクチャで開発した話

はじめに

こんにちは、スタメンで iOS/Android アプリのエンジニアをしている @temoki です。

昨年の10月にスタメンにジョインしてからの私の最初のミッションは、TUNAG iOS アプリのチャット機能の開発プロジェクトでした。本記事ではこのチャット機能開発プロジェクトにおいて採用した VIPER というアーキテクチャについて紹介し、チャット機能の初版リリースからいくつかの機能改善を経た現在における所感をお伝えしたいと思います。

VIPERの概要

VIPERとは?

私が VIPER を知ったのは objc.ioArchitecting iOS Apps with VIPER という記事です。これを読めば VIPER について一通り理解することができますが、端的に説明するために次の文を引用します。

VIPER is an application of Clean Architecture to iOS apps. The word VIPER is a backronym for View, Interactor, Presenter, Entity, and Routing. Clean Architecture divides an app’s logical structure into distinct layers of responsibility. This makes it easier to isolate dependencies (e.g. your database) and to test the interactions at the boundaries between layers:

つまり次のとおりです(日本語に翻訳しただけですが)。

  • VIPER は iOS アプリケーションに Clean Architecture を適用したもの
  • VIPERという言葉は、 View、Interactor、Presenter、 Entitiy および Routing を表す バクロニム
  • アプリケーションの論理構造を責任の異なるレイヤーに分割することで(データベース等の)依存関係を分離し、レイヤー間の境界で相互作用をテストすることが簡単になる

VIPERのメインパーツ

VIPER は次の5つのパーツに分類され、それぞれが明確な責務を持ちます。

  • View
  • Presenter からの指示を表示する
  • ユーザー入力を Presenter に伝える
  • Interactor
  • アプリケーションの中の一つのユースケース(画面単位など)を表現する
  • データ操作とビジネスロジックを含む
  • Presenter
  • Interactor から受けとったデータを表示するための準備を行う
  • ユーザー入力に反応して Interactor に新しいデータを要求する
  • このようなビューロジックを含むが、iOS の UIKit には依存しない
  • Entity
  • Interactor のみに使用される単純なモデルオブジェクト
  • Router
  • 画面がどのように表示されるかの画面遷移ロジック
  • その表示する画面の生成(依存性注入)

これは Passive View 方式の MVP (Model View Presenter) アーキテクチャがベースになっており、大きくなりがちな Presenter から画面遷移と生成の責務を Router に、そして画面固有のユースケースを Interactor に分離しているのが特徴的です。

それぞれのパーツのつながりは下図のようになります( objc.io の記事からの引用)。

TUNAG アプリへの適用

ここからは TUNAG iOS アプリへの VIPER 適用についてお話していきます。

なぜ VIPER を採用したのか?

TUNAG のチャットは複数の画面で構成されますが、メインとなるチャットルーム画面のスクリーンショットがこちらです。

メンション、絵文字、スタンプ、写真投稿、ファイル添付などたくさんの機能があります。TUNAG のチャットは簡易的なものではなく、メンバー間の日常コミュニケーションやビジネス用途としても利用されるものですので、この1つの画面だけでもとても多くの表示パターンと入力パターンがあります。そして TUNAG という事業は長く継続していくものですので、今後の変更などのメンテナンスのことを考えると最初から適切にモジュールを分割していく必要があります。

VIPER は特定のプラットフォームやライブラリに依存しているものではなく、オブジェクト指向でアプリケーションを適切に設計していくためのガイドラインのようなものです。そのため、開発チーム内でモジュール分割の方針を共通認識として開発を進めていけると感じたため、VIPER を採用することにしました。

また、開発チームには開発経験の少ない若手メンバーもおり、VIPER での構築を実践していくことでメンバーの設計力向上につながるのではないかという期待もありました。

TUNAG アプリの VIPERによる構成

最終的に、TUNAG アプリの構成は下図のようになりました。各パーツで <~> と記述しているものは、そのパーツが準拠する Protocol です。それぞれのパーツは、その Protocol により接続されることになります。

View, Presenter, Interactor, Router は基本的に画面単位に用意します。画面を開発するときは、その画面における各パーツの役割を Protocol で定義し、それらを Contract (契約) としてまとめたファイルを作るようにしました。こうすることでこの画面の機能がまとまったカタログができます。以下が、タスクリスト画面を作るとした時の Contract の例となります。そして、Router がそれぞれの依存性を注入し、まとめて一つの画面モジュールとして組み立てます。

/// タスクリスト画面 View
protocol TaskListView: class {

    // Dependency
    var presenter: TaskListPresentation! { get }

    /// タスクリストの読み込み中の表示をする
    func showLoadingState()

    /// タスクリストを表示する
    func showTaskList(_ taskList: [TaskCellModel])

    /// タスクリストが一つもない状態を表示する
    func showEmptyState()

}

/// タスクリスト画面 Presentation
protocol TaskListPresentation: Presentation {

    // Dependency
    var view: TaskListView? { get }
    var interactor: TaskListUseCase! { get }
    var router: TaskListRouter! { get }

    /// タスクセルがタップされた
    func taskCellDidTap(taskId: Int)

    /// タスク追加ボタンがタップされた
    func addTaskButtonDidTap()

}

/// タスクリスト画面 UseCase
protocol TaskListUseCase: class {

    // Dependency
    var output: TaskListInteractorOutput? { get }

    /// すべてのタスクリストを取得する
    func fetchAllTaskList()

}

/// タスクリスト画面 InteractorOutput
protocol TaskListInteractorOutput: class {

    /// すべてのタスクリストを出力する
    func outputAllTaskList(_ taskList: [TaskModel])

}

/// タスクリスト画面 Wireframe
protocol TaskListWireframe {

    /// タスクリスト画面のビューコントローラー
    var viewController: UIViewController? { get }

    /// タスク作成画面を表示する
    func presentCreateTaskView()

} 

VIPER による基本構成に加え、データレイヤーには Repository パターンを適用しています。TUNAG のチャット機能におけるデータソースは、Firebase の Cloud Firestore、TUNAG の REST API、ローカルストレージと多岐にわたるため、Repository パターンによりそれらを隠蔽しています。Repository は特定のユースケースに依存せず、例えばユーザー情報やチャットメッセージ情報のようなシンプルなデータの CRUD のみを責務とするため、複数の画面から使用されます。

また、Cloud Firestore にあるユーザー情報やチャットメッセージ情報などのリアルタイムアップデートを受け取り、それらをマージして Interactor に流していくために RxSwift を使用しています。MVVM アーキテクチャーを採用しているわけではないですし、OS 標準ではない特定のライブラリにアプケーション全体が依存することを避けるため、RxSwift は Interactor より先のプレゼンテーション層では使用しないようにしています。

良かった点/悪かった点

このような構成で2018年10月からチャットの開発をはじめ、2019年1月初に初版をリリースしました。その後細かいアップデートをしつつ、この4月にはチャットメッセージの検索のような大きな機能追加の開発を行なっています。VIPER を採用した開発を始めて半年経った現時点における、個人的に良かった・悪かったと感じている点を箇条書きでまとめます。

良かった点

  • 画面実装時に前述の Contract ファイルを用意することで、どのパーツは何に関与して、何に関与しないのか?と常に考えながら実装していくことになり、自分もチームメンバーもオブジェクト指向での設計力が格段に上がったという実感があった(よくよく考えてみると SOLIDの原則 を忠実に実践していくことになっていた)
  • 各パーツの責務が明確で、それぞれの間が Protocol により疎結合になっていることで、ユニットテストがとても書きやすい
  • チャットの開発は最低限の機能を実装し、徐々に肉付けしていくスタイルで行なっていったが、足し算が非常にやりやすい
  • 機能追加後のコードの Diff も何の機能追加や変更を行なったかがとてもわかりやすかったため、PR のレビューもしやすい
  • 初版リリースの前にQAチームによるテストや社内でのβテストを実施したが、機能のボリュームのわりには不具合検出数が非常に少なかった

悪かった点

  • 1つの画面を作るのに多くのクラスのファイルが必要であるのはやはり面倒だった( 例えば Swift-VIPER-Module のような自動生成ツールを試すべきだった)
  • これは VIPER の問題ではなく私の問題だが、初版リリース時に部分的にしかユニットテストを書くことができなかった(ただし、その後徐々にテストを追加していっているが、実装コードを変更せずともテストを書けていけているのは VIPER のメリット)

コード量で見る成果

チャット機能の初版リリースの直前に、VIPER 適用の振り返りの意味もこめて、開発着手から初版リリースの間に書いた Swift コード量を集計してみたました。結果は次のとおりです。

  • 新規ファイル数: 約200 (≒クラス数)
  • 追加/変更LOC: 約10,000

単純に計算すると、1ファイルあたり 50 LOC くらいになっています。VIPER で他のパーツの Mediator として働く Presenter は比較的にコード量が多くなりがちなのですが、チャット機能でもっとも複雑なチャットルーム画面(「なぜ VIPER を採用したのか?」でお見せしたスクリーンショットの画面)の Presenter においても 500 LOC 程度で収めることができていました。

これも VIPER を採用したことで設計がうまくいった成果だと思っています。

おわりに

スタメンでの私の最初のミッションはまさかのチャット機能でした。とても大きな機能なのでやりきれるかという不安をなくすため、最初にこのような VIPER による設計方針を固め、それにしたがって1画面作り終えた頃に VIPER の良さを実感し、それ以降は特に不安なく淡々と機能開発を進めていくことができました。この開発期間は私のエンジニア人生の中でも特に濃密なものになりましたので、その振り返りをしながらこの記事を書いています。

TUNAG iOS アプリのチャット開発は一旦落ち着きましたが、TUNAG の iOS/Android アプリとしてやるべきことはまだまだ多く、引き続きより良いアプリにしていくために一緒になって開発してくれるエンジニアが必要です。もし興味がありましたら こちら からご連絡ください。一緒に濃密なエンジニア人生を送りましょう!