以下の内容はhttps://tech.jcblab.jp/entry/2026/03/02/101727より取得しました。


巨大なiOSアプリをマルチモジュール化してみた

はじめに

はじめまして。JCBデジタルソリューション開発部の若林です。MyJCBアプリのiOS側のエンジニアをしています。今回は先月書いたこの記事の続きを書きます。

巨大なiOSアプリをマルチモジュール化してコンフリクトを減らしたい

前置き

前回の記事では、MyJCBアプリをマルチモジュール化したい理由と、どのような構成を目指すのかについて解説しました。今回は、実際にマルチモジュール化を実行してみた経験をお話しします。

前回のおさらい

前回の記事では、当時の理解にもとづき「この形で切れたら理想だ」と考え、下記モジュール構成のイメージを提示しました(この後で触れるとおり、実際の分割では下記モジュール構成で進めるとうまくいかない点がありました)。

前回の記事でのモジュール構成のイメージ

  1. Feature Modules(機能ごとのモジュール)

    • HomeFeature, PointFeature など
    • 各Featureモジュール内に Presentation / Domain / Data のレイヤーを持たせ、機能単位で完結させるようにしてチーム開発時のコンフリクトを極小化します
  2. Core Modules(共通基盤モジュール)

    • CoreDomain, CoreData, CorePresentation

依存関係図

口で言うなら簡単でも、実行は難しいものです。実装作業も想像以上に大変でした。今回、Feature Modulesには手を付けず、共通基盤モジュール(Core Modules)の作成のみを行いました。他の開発と並行していたため作業時間があまり取れなかったり、何度かコンフリクト解消に追われたりしたこともあり、この作業だけでも約1.5ヶ月を要しました。

どんな構成にしたのか

実際のマルチモジュール化では、当初の計画から一部変更を加えました。 結論から言うと、当初想定していた CoreDomain と CoreData の分離をそのまま進めると循環参照が発生しました。そこでまずは動く形を優先し、AppCore にひとまとめにしています(理由と経緯はこの後で説明します)。

AppCoreモジュールの作成

まず、AppCoreという単一のモジュールを作成し、そこにCoreDomainCoreDataの役割を統合しました。その後、UI系の基盤を格納するCorePresentationモジュールを別途作成しました。

依存関係図

前提として、MyJCBアプリには「各画面やロジックが継承して使う」基底クラスがいくつか存在します。

  • BaseDatasource
  • BaseUseCase
  • BasePresenter
  • BaseViewController

マルチモジュール化を始めた時点では、境界の切り方は正直まだふわっとしていて、「とりあえずレイヤーで分けてみる」くらいの温度感でした。

ただ、実装を追いながら移動先を考えていくうちに、どんな境界で分けるのが正しいのか分からなくなってきます。そこで一度立ち返れる基準として、この4つの基底クラスを起点として整理するようにしました。例えば、

  • BaseDatasourceはデータアクセス寄りなのでCoreData
  • BaseUseCaseはビジネスロジック寄りなのでCoreDomain

というように分類し、各基底クラスが使っているユーティリティや共通処理も同じモジュールへ寄せていく、という進め方です。

ただ、実装を追っていくとBaseDatasourceBaseUseCaseが(直接・間接に)依存し合っていました。BaseDatasourceCoreDataへ、BaseUseCaseCoreDomainへ移そうとすると、結果としてCoreDataCoreDomainが相互依存になり、モジュール分割の時点で循環参照が発生してしまいます。

今回は、まずは動く形で前に進めることを優先し、両者を同居させられるAppCoreを1つのモジュールにまとめる方針です。

どんな作業が発生したのか

マルチモジュール化の実装作業は、主に以下のステップで進めました。

1. モジュールの作成とコードの移動

アプリメニューから FileNewPackage を選択し、新しいSwift Packageを作成しました。その後、既存のコードを新しいモジュールにコピー&ペーストで移動していきます。

2. アクセス修飾子の追加

同一モジュール内では、クラスやメソッドにpublicopenを付けなくても参照できますが、別モジュールからアクセスする場合は明示的にアクセス修飾子を指定する必要があります。

そのため、他のモジュールや本体アプリから参照されるクラス、継承されるクラスに対して、ひたすらpublicopenを追加していきました。

// 修正前
class BaseViewController: UIViewController {
    // ...
}

// 修正後
open class BaseViewController: UIViewController {
    // ...
}

3. Dependency Injectionパターンの導入

AppCoreの中で使用されているユーティリティクラスの一部は、画面固有のModelなど、詳細な実装に依存していました。これらをそのままAppCoreに含めると、モジュールの境界が曖昧になってしまいます。

そこで、Protocol(プロトコル)を使ったDependency Injectionを採用しました。

AppCore側(プロトコルの定義)

public protocol ErrorRouting {
    func navigateToHome(from viewController: UIViewController)
}

open class BaseViewController: UIViewController {
    // BaseViewControllerにstaticプロパティとして保持
    public static var errorRouter: ErrorRouting?
}
// BaseViewController内での使用例
if let errorRouter = BaseViewController.errorRouter {
    errorRouter.navigateToHome(from: self)
}

本体アプリ側(実装の提供)

class ErrorRoutingAdapter: ErrorRouting {
    func navigateToHome(from viewController: UIViewController) {
        // 実際のナビゲーション処理
        // 画面固有のロジックをここに記述
    }
}

AppDelegateでの注入

func application(
    _ application: UIApplication,
    didFinishLaunchingWithOptions launchOptions: [UIApplication.LaunchOptionsKey: Any]?
) -> Bool {
    if BaseViewController.errorRouter == nil {
        BaseViewController.errorRouter = ErrorRoutingAdapter()
    }
    return true
}

このパターンにより、AppCoreは詳細な実装を知らなくても機能を提供できるようになり、モジュールの独立性を保つことができました。

マルチモジュール化の注意点

AppCoreモジュールの作成は比較的スムーズに進みましたが、CorePresentationモジュールで大きな問題に直面しました。ビルドは通るのにアプリの挙動がおかしいというものでした。今までの作業も大変でしたが、全てエラーとの戦いでエラーを読めば解決策がわかりました。しかし、今回の問題はエラーが出ないため解決に時間がかかったので発生した問題の詳細と原因、解決策を提示します。

CustomViewのBundle問題

MyJCBアプリでは、再利用可能なCustomViewをいくつか作成しており、それらをStoryboardから利用しています。

マルチモジュール化前は、すべてのコードが同一のメインターゲットに含まれていたため、Bundle.mainを使ってリソースを読み込むことができました。また、特に指定しなければ自動的にBundle.mainが使用されます。

しかし、CustomViewをCorePresentationモジュールに移動したところ、Storyboardから正しく読み込まれなくなるという問題が発生しました。

原因

Storyboard内でCustom Classを指定する際、Moduleの指定がデフォルトで空白(= Bundle.main)になっていました。しかし、CustomViewの実体はもはやメインバンドルにはなく、CorePresentationモジュール内に存在します。

そのため、Storyboard上でModuleをCorePresentationに明示的に指定する必要がありました。

解決方法

XcodeのStoryboardで、CustomViewを選択し、Identity Inspectorから以下を設定します。

  • Module: CorePresentation

この設定により、StoryboardがCorePresentationモジュール内のCustomViewを正しく参照できるようになり、問題が解消しました。

終わりに

1.5ヶ月かけて、AppCoreとCorePresentationという共通基盤をモジュールとして切り出せました。今後はこれらの基盤モジュールを土台に、機能単位(Feature Modules)の切り出しを進めていく予定です。

最後になりますが、JCBでは我々と一緒に働きたいという人材を募集しています。 詳しい募集要項等については採用ページをご覧ください。
www.saiyo.jcb.co.jp




以上の内容はhttps://tech.jcblab.jp/entry/2026/03/02/101727より取得しました。
このページはhttp://font.textar.tv/のウェブフォントを使用してます

不具合報告/要望等はこちらへお願いします。
モバイルやる夫Viewer Ver0.14