every Tech Blog

株式会社エブリーのTech Blogです。

SPMでマルチモジュール/マルチターゲット開発

はじめに

この記事は every Tech Blog Advent Calendar 2023 の 8 日目です。

トモニテのiOSアプリは今年、トモニテ妊娠アプリの開発を期にSPMを用いたマルチモジュール構成に移行しました。 これらのアプリにはアカウント管理やデザインシステムなど共通部分が多くあります。また一部機能は重複しているため、コード共通化をしやすくするのが主目的でした。

この記事ではマルチモジュール構成への移行をどのように進めたかと結果について書きたいと思います。

コード共通化の方針

以下の選択肢がありました。

  • A: コード共通化をしない
  • B: 共通部分を別リポジトリに切り出し、アプリもそれぞれ別リポジトリで運用する
  • C: 1プロジェクト(1リポジトリ)で複数アプリを開発する

コード共通化にはデメリットもあります。片方のアプリを修正した時もう一方のアプリにも影響する可能性があり、本来は必要のなかった調査や動作確認が必要になるかもしれません。しかし今回は共通部分が多く共通化のメリットの方が大きいと判断したためAは選びませんでした。

Bは、共通部分がアプリ本体と疎で、変更頻度が低ければ良い選択肢だったと思いますが、今回は選びませんでした。

モジュール構成を決める

モジュール間の依存が循環しないように関係を整理しつつ、分割方法を決めます。

まず、共通部分を以下のように、レイヤーに沿って分割しました。

  • Utilities: 便利クラス、Extensionなど
  • Network: 外部通信
  • Model: モデル
  • Core: Feature間の画面遷移の仕組みなどを提供する

一方、機能ごとに Feature モジュール群を作成しました。

  • Home: ホーム
  • Media: 記事や記事検索機能など
  • Childcare: 育児記録機能
  • Babyfood: 離乳食機能

これらの Feature モジュールは共通モジュール群に依存しますが、各Feature が相互に依存することは禁止しています。

Featureモジュール間の参照をせずに、Feature間の画面遷移を可能にする実装についてはこちらの記事を参考にさせていただきました。

メルペイのスケーラビリティを支えるマルチモジュール開発

この仕組みによってFeatureモジュールの独立性が保たれ、必要なモジュールだけをターゲットに組み込むことができるようになりました。

モジュールを切り出す

方針に沿ってモジュールを作成していきました。他への依存が少ない部分から順番に進める必要があります。Utilities -> Network -> Model -> Core -> 各Feature のような順序です。

Xcodeでプロジェクト内にパッケージを作成し、 Package.swift ファイルにパッケージの定義と依存関係を記述します。

import PackageDescription

let package = Package(
    name: "Network",
    platforms: [
        .iOS(.v14)
    ],
    products: [
        .library(
            name: "Network",
            targets: ["Network"])
    ],
    dependencies: [
        .package(url: "https://github.com/Moya/Moya.git", .upToNextMajor(from: "15.0.0")),
        .package(path: "Utilities")
    ],
    targets: [
        .target(
            name: "Network",
            dependencies: [
                "Moya",
                "Utilities"
            ],
            resources: [.copy("Stab")]
        )
    ]
)

あとはファイルをパッケージの中に移動し、外部から参照される宣言をpublicにするなど、アクセス修飾子を修正します。 移動したコードに本来あるべきでない依存があった場合は、依存関係をなくすための修正が必要になる場合も多々あります。

作業量がかなり多くなりますが、一度に終わらせる必要はなくパッケージ単位でリリースが可能です。通常の開発と並行して少しずつ進め、3ヶ月程度かかりました。

開発用の minimumTarget

マルチモジュールの利点を活かし、新規機能や新規画面を作る場合の開発効率を上げるために minimumTarget というものを作りました。 開発対象の Feature と最低限の共通部分だけをターゲットに組み込んで開発でき、以下のような利点があります。

  • ビルド時間短縮
    • トモニテ本体と比較して、クリーンビルド時間 141 秒 → 38 秒
  • シミュレータでのデバッグ開始が早い
  • SwiftUIのプレビューを利用可能
    • 本体ではプレビューが表示されるまでの時間が非常に長く実質利用できなかったのが利用可能になります

Xcode Cloud

デバッグ用の Firebase App Distribution 配布と App Store Connect へのサブミットを Xcode Cloud で自動化していて、リポジトリに変更を加えると二つのアプリが配布/サブミットされます。

結果

これまで書いてきたとおり開発効率を向上できました。

  • 複数のアプリに同様の変更を加える場合、重複した開発をせずに済む
  • モジュール間の依存関係が整理され、コード変更の影響範囲を把握しやすい
  • パッケージ内のファイルは xcodeproj ファイルで管理されなくなるので、 xcodeproj のコンフリクトがほぼ無くなる
  • minimumTarget で時間短縮

一方よくない点もありました。

  • 複数のアプリへの影響を考慮しながら開発する必要がある
  • 本来開発対象ではないアプリへの影響を調査したり、動作確認が必要になる可能性がある

しかし全体としては利点が大きく、移行した価値があったと判断しています。 この記事がどなたかの参考になれば幸いです。