このエントリーはCyberAgent Developers Advent Calendar 2021 の17日目の記事です。
マッチングアプリ「タップル」のiOS開発を担当している永野です。
本記事では、2019年からタップルで導入されているFeature Module開発について、導入の背景から現行の運用まで紹介します。
Feature Module開発を採用した背景
タップルのiOSアプリケーションは2014年5月にリリースされてから今年で8年目をむかえるプロダクトです。
運用・開発を重ねるにつれて、コード量が多くビルド時間が問題となっていました。特に大型の機能開発では画面を一から作り直す機会が多く、開発速度の大きなボトルネックになっていました。機能開発のイテレーションが高速化され、並列で開発しながらも、開発速度を保つために機能間の依存関係を疎結合にしたいというモチベーションから、Feature Module開発の採用に踏み切りました。
上記の理由から2018年頃からFeature Module開発を検討してきましたが、マルチモジュールの導入事例が少なかったことや、導入時の学習コスト等のトレードオフを鑑みて、チームで機能単位ではマルチモジュール化しない方針をとっていました。
一方で、近年のトピックスとして上がってきた
- XCodeのプレビュー機能
- Visual Regression Testing
といった技術は、開発時のビルド回数を減らしたい、意図せぬUI変更を防ぎたいといったチームの温度感の高い課題へのアプローチへと繋がっていました。これらを導入するために、実行する時間やターゲットサイズが小さいミニアプリの作成をFeature Module開発と合わせて取り組んでいくというモチベーションはチームにとって十分なものでした。そういった経緯がありタップル iOSチームでは「ミニアプリを利用した開発」をゴールとしてFeature Module開発をチームの方針として定めました。
タップルのマルチモジュール構成
概要
タップルでのアプリケーション全体の構成について説明します。
タップルではAPI、Entity定義、ExtensionといったCore Module層、共通のUIComponent層、アプリ全体で共有される状態を管理するFlux層が大きくレイヤーとして別れており、その下にFeature Moduleと呼んでいる機能単位でのモジュールが10以上存在しており、Feature Module間での依存がない形で構成されています。
Feature ModuleとFluxアーキテクチャ
タップルはFluxアーキテクチャ(+MVVM )を導入しており、アプリ内の状態管理は画面ごとのFluxと、Flux層に置かれたアプリ全体で共有される役割ごとのFluxの2種類で管理してています。 この構成は、1タブ単位などまとまった大きな機能があり、機能間で共有したい状態があった場合に適切なアクセススコープのFluxを作成することができない課題があります。View間で共有される状態を各画面のAction、Storeで重複して管理してしまうことや、特定機能ドメイン間でしか利用されない状態をアプリ全体のスコープで管理してしまうといった課題がありました。Feature Moduleとして機能をFrameworkとして管理することになってから、タップルではモジュール内で機能で共有される状態を1つのFluxを管理しています。このFluxはInternalのスコープで管理されており、スコープを機能モジュール内に依存を閉じ込めることができるようになりました。
Feature Moduleのミニアプリ
ミニアプリの構成
Xcode プレビュー実行時のサイズ最小化、ビルド時間の削減、API層をMock化してアプリケーションを起動できることを目的とし、ミニアプリを構築しました。
ミニアプリ構築の目的
- Xcode プレビュー実行時のターゲットサイズを小さくする
- 画面をアプリから切り離し、スナップショットテストをしやすいように
- CIのテスト実行時間の短縮
といった部分にありました。そのため、UnitTest時に利用していたMock作成用のコードをモジュールに分割し、MockのEnvironmentクラスをDIしてAPI通信を行うことなく画面操作が行えるアプリを実現しています。詳細は後述してますがミニアプリを利用してFeature ModuleのUnitTest, Visual Regression Testを行うことでCI上でのテストの実行時間を短縮しています。
新機能開発での活用
実際に今月リリースされたおでかけリニューアル版の開発で、ミニアプリの開発を活用しました。
- APIのMock層を利用して、サーバーサイドの開発を待たずに通信ロジックを実装
- ビルド時間やCIでのテスト時間の短縮
- 開発の早い段階からミニアプリを使用したドッグフーディングや仕様改善
を実現し、開発工数を当初の見積もりから3割を削減できました。
Feature Module導入で直面した課題
Feature Moduleの分割及びミニアプリの構築を実現するに向けていくつかの課題に直面しました。最適解を模索している途中の課題もありますが、参考にしていただけると幸いです。
Resourceをどこに置くか?
画面を複数のモジュールに分割する際に問題になるのが、文言や画像といったResourceをどこで管理するかという問題です。一見Feature Module内でのみ利用する文言はFeatureで管理する方が状態管理と同様に、依存が閉じ込められてスッキリしそうです。しかし、タップルではResource管理用のModuleで一括管理しています。理由は以下のとおりです。
移行コストを下げる
アプリからFeature Moduleへの分割、1つのFeature Moduleから別のModuleを切り出す場合を想定した時に、Module内で参照しているResourcesを探し出し移動させることはコストが高いです。そのため最初からどのModuleからも参照できる場所に置いています。
Interface Builderでの事故を防ぐ
Module間を移動させると、Interface Builder内の画像はXcodeの表示上は崩れが起こりませんが実行時には画像を参照することができません。タップルでは基本的にIB内で画像を参照することは禁止していますが、古い実装が残っていたり、仮で入れた画像が残ってしまっており画像のセットし忘れを隠していた場合には不具合の発生原因になってしまいます。そのため、Moduleを跨いだResourceの移動を極力避けています。
Feature Module上での依存関係の解決
タップル iOSのアーキテクチャでは循環参照を避けるためにFeature Module同士がお互いに参照を持つことを禁止しています。各モジュールは単方向の依存関係を持っており、上位のモジュールは自身より下位のモジュールしか参照することができません。制約を仕組みで実現するためにFlux層とFeature Module層の間で画面に注入したい依存をまとめたDIコンテナをEnvironmentのインターフェースとして定義し、実態をアプリ層からDIしています。
Feature Module間の画面遷移
Feature Module間で画面遷移がある場合や、Feature Moduleからアプリターゲット内にある画面に遷移がある場合は、そのままでは依存を解決することができません。タップル iOSチームでは以前Coordinatorパターンを導入し、画面遷移ロジックをViewControllerから完全に分離した後にCoordinatorを除いた画面定義をFeature Moduleに移動していくことも検討しましたが、利用を簡便にし導入をスムーズに行うことを目指して、メルカリさんの事例を参考に、アプリ層からDIされるEnvironmentを経由してViewControllerクラスのインスタンスを取り出す仕組みを用意しています。Core Module層に用意された画面クラスのインターフェースをViewControllerRequestとして定義し、その型解決をEnvironmentが担うことでFeature Module内の画面では遷移先の型定義を知ることなくUIViewControllerとして型消去された画面を取り出すことができます。
public struct WebViewControllerRequest: ViewControllerRequest {
public typealias Input = URL
public typealias Output = Never
public typealias Inject = Never
public var input: Input
public init (input: Input) {
self.input = input
}
}
CI上でのテスト対象の選択
ミニアプリのビルドはアプリ全体のビルドと比較してCI上でも3〜5割ほど高速に実行することができます。CI上でも変更のあるModuleに限定して適切なテストを実行することで高速にテストを実行することができます。ミニアプリのビルドは、アプリ本体や他のミニアプリのビルドと並列に行うことができるため、CI時間の累計は大きくなってしまいますが、並列実行で最終的なテスト終了までの時間を大幅に短縮することができます。具体的には下記手順で、UnitTest, VisualRegressionTestを実行しています。利用しているCIはBitriseです。
-
自作スクリプトで、gitのdiffから変更があったモジュールを特定し、実行対象のワークフロー名の配列を作成し環境変数に格納
-
環境変数のワークフローをbuild-router-startにて並列実行
# bitrise.yml
test:
steps:
- script:
inputs:
- content: |-
cd "$BITRISE_SOURCE_DIR"
envman add --key TEST_COMMANDS --value "$(scripts/target_feature_tests.sh)"
title: Set TEST_COMMANDS
- build-router-start:
run_if: '{{getenv "TEST_COMMANDS" | ne ""}}'
inputs:
- access_token: "$ACCESS_TOKEN"
- workflows: |-
$TEST_COMMANDS
今後アプリ内のテストをミニアプリに移行していくことで、並列化を進めテスト効率をあげていく予定です。
最後に
最後までお読みいただきありがとうございます。 Feature Module構成を導入し、ミニアプリを活用して開発していくことで様々なメリットを享受できるようになりました。導入に工数やモジュール構成に対する学習コストはかかってしまいますが、個人的にはiOSエンジニアが3、4人のチームでも、十分にやる価値があると思っています。 タップル iOSではすべての機能をFeature Moduleに切り出し、アプリターゲット内の画面クラスを0にすることを目標にしていますが、まだまだ途中です。今後もコードの自動生成や利用シーンの拡大を目指して継続的に取り組んでいきたいです。