iOSアプリでSPMを用いたマルチモジュール構成を試してみた

近年のiOSアプリ開発では、アプリの規模が大きいアプリも増え、複数人による並行開発を安定的に行うべく、マルチモジュール構成を採用するアプリも増えてきたと思います。
特にマルチモジュールの流れはAndroidの方が先行していた印象で、2018年頃からその流れが大きくなってきた印象でした。
iOSDCなどカンファレンスのセッションなどを聞いていると、大規模アプリほどその課題にあたっているチームが多いようです。

従来のマルチモジュール構成を行うには、XcodeからFrameworkプロジェクトを作成し、ライブラリとしてメインのアプリプロジェクトに追加していくというものでした。
アプリと、ライブラリ間の依存関係にはXcode上で手動で行う方法や、CocoaPodsなどパッケージ管理ツールを使う方法もあります。

そんな中、iOSDC 2021の@d_dateさんのセッションで、Swift Package Managerを用いたマルチモジュール構成の紹介がありました。

https://speakerdeck.com/d_date/swift-package-centered-project-build-and-practice

今回この記事では、こちらの発表内容を参考にしつつ、実際にSPMでマルチモジュール構成を取ると、どのようなメリット、デメリットがあるか、サンプルアプリを作り検証してみました。

iOS15もリリースされ、2021年の年末からサポートOSをiOS14/iPadOS14以降とするアプリも多いかと思いますので、iOS14/iPadOS14以降をターゲットとしました。
また、アプリはFull SwiftUIで作成し、iOS14から利用できるコンポーネントを使ったアプリとしていますので、これからSwiftUIを勉強していく方にも参考にしていただけると思います。

> 2021/12現在の開発環境
> Mac Book Pro 16-inch 2019(2.4GHz 8-Core Intel Core i9, 32GB 2667MHz DDR4)
> macOS Big Sur v11.6
> Xcode v12.5.1

# サンプルアプリ

[GitHubのSearch API](https://docs.github.com/ja/rest/reference/search)を利用した、リポジトリを検索&表示するアプリです。

https://github.com/darquro/GithubViewer

**iOS**

| Home | Search | WebContent |
| ---- | ---- | ---- |
| <img width="250" src="https://storage.googleapis.com/zenn-user-upload/73475ccaeff928bb42bf86af.png"> | <img width="250" src="https://storage.googleapis.com/zenn-user-upload/13d95b3804ead90d7167b38f.png"> | <img width="250" src="https://storage.googleapis.com/zenn-user-upload/73264652547986d76190e31d.png"> |

**iPadOS**

![iPadOS](https://storage.googleapis.com/zenn-user-upload/b885d7b6c3b9a003c02cc79c.png)

全体のモジュール構成とアーキテクチャは以下のようになっており、MVVM+Clean Architedtureを採用しました。

![アーキテクチャ](https://storage.googleapis.com/zenn-user-upload/39d31510909f3aec6f6a8812.jpeg)


Feature Modulesという4つの画面モジュールグループと、Core Modulesというそれ以外のモジュールグループとなっています。

| Module Type | Module Name | Description |
| ---- | ---- | ---- |
| App | App | アプリエントリポイントを持つのモジュール。Rootのみ依存を持つ。 |
| Feature Modules | Root | TabViewを持つViewで、HomeとSearchの画面の依存を持つ。 |
| 〃 | Home | Home画面のモジュール。WebContent、ViewComponents、Repositoriesに依存を持つ。 |
| 〃 | Search | Search画面のモジュール。WebContent、ViewComponents、Repositoriesに依存を持つ。 |
| 〃 | WebContent | WebView画面のモジュール。ViewComponentsに依存を持つ。 |
| Core Modules | ViewComponents | 共通で使われる画面コンポーネントを集めたモジュール。今回はImageライブラリNukeUIを利用しているコンポーネントがあるため、NukeUIの依存を持つ。 |
| 〃 | Repositories | APIアクセスやローカルデータアクセスを抽象化したクラスを集めたモジュール。GitHubAPIRequestの依存を持つ。 |
| 〃 | GitHubAPIRequest | GitHubAPIのリクエストクラスやレスポンスEntityを集めたモジュール。APIClientの依存を持つ。 |
| 〃 | APIClient | APIClientを含むモジュール |

Xcode上のプロジェクトツリーはこのような見た目になっています。

<img width="250" src="https://storage.googleapis.com/zenn-user-upload/fd713b6afcff27d421e91d01.png">

# SPMモジュールの作成の仕方

`File > New > Swift Package...`を選択します。

![SwiftPackage作成](https://storage.googleapis.com/zenn-user-upload/bbb6fe41833e0598b5e784df.png)


そうすると、ダイアログが表示されますので、名前、ディレクトリ、追加プロジェクト、プロジェクトツリーのグループを指定するだけです。

![SwiftPackage作成](https://storage.googleapis.com/zenn-user-upload/6f01eafb83426ed6c94e1bcc.png)


それでは、ここからは、SPMマルチモジュール構成のプロジェクトのメリット、デメリット、課題などをまとめていきます。

# SPMマルチモジュールによるメリット

## メリット1: プロジェクトファイルのコンフリクト問題からの開放

ここで注目したいのが、SPMで追加した場合、"ディレクトリの参照"としてプロジェクトツリーに追加されるという点です。（フォルダが青）

<img width="250" src="https://storage.googleapis.com/zenn-user-upload/93bf0ee701359912edcd4889.png">


通常Xcodeのファイルはプロジェクトファイル(project.pbxproj)にツリー構成が書き込まれ、ファイルの追加、削除、移動を行うとプロジェクトファイルも自動更新され、複数人で開発する場合のコンフリクトの要因となり、アプリ開発者は悩まされてきました。
その解決策として、[XcodeGen](https://github.com/yonaskolb/XcodeGen)が登場し、自動的にディレクトリとpbxprojファイルを同期させるアプローチが生まれました。

それに対し、SPMのディレクトリ参照となるため、SPMモジュールのルートディレクトリだけがpbxprojファイルに記述され、そこから配下のファイルはpbxprojファイルに依存を持ちません。
従って、SPMモジュール内のファイルの追加、削除、移動をしてもpbxprojファイルのコンフリクトは発生しなくなります。

エントリポイントのAppモジュールはRootモジュールだけ依存を持ち、以下のコードのみとなります。

```swift:App/Main.swift
import SwiftUI
import Root

@main
struct Main: App {
    var body: some Scene {
        WindowGroup {
            RootView()
        }
    }
}
```

![アプリターゲットはRootモジュールのみライブラリの参照がされている](https://storage.googleapis.com/zenn-user-upload/cf98307694ce8d771baef487.png)

## メリット2: Package.swiftによる依存管理

各モジュールがどのモジュールに依存するかが、`Package.swift`で管理できるため、とても明確でシンプルになります。

```swift:FeatureModules/Home/Package.swift
// swift-tools-version:5.3
// The swift-tools-version declares the minimum version of Swift required to build this package.

import PackageDescription

let package = Package(
    name: "Home",
    platforms: [
        .iOS(.v14),
    ],
    products: [
        // Products define the executables and libraries a package produces, and make them visible to other packages.
        .library(
            name: "Home",
            targets: ["Home"]),
    ],
    dependencies: [
        // Dependencies declare other packages that this package depends on.
        // .package(url: /* package url */, from: "1.0.0"),
        .package(path: "WebContent"),
        .package(path: "../CoreModules/ViewComponents"),
        .package(path: "../CoreModules/Repositories"),
    ],
    targets: [
        // Targets are the basic building blocks of a package. A target can define a module or a test suite.
        // Targets can depend on other targets in this package, and on products in packages this package depends on.
        .target(
            name: "Home",
            dependencies: [
                "WebContent",
                "ViewComponents",
                "Repositories",
            ]),
        .testTarget(
            name: "HomeTests",
            dependencies: ["Home"]),
    ]
)
```

また、`Package.swift`に変更を加えると、Xcode自動的に構文チェックをしてくれ、エラーや警告を表示してくれます。

![存在しないPackageを指定している場合のエラー](https://storage.googleapis.com/zenn-user-upload/80f5bfbd5429b7b89d3429d8.png)

![dependenciesに追加しているが、どのtargetからも参照されてないときの警告](https://storage.googleapis.com/zenn-user-upload/bc614d816f44270bd1625a73.png)

このあたり、Frameworkを作成して、依存管理する場合に比べ、ビルドしなくても依存問題を検知できるのは大きなメリットだと思います。

## メリット3: Frameworkに比べ管理ファイルや変更点が少ない

ここで、従来からあるFramework追加の場合と比較してみましょう。
新しくTargetの追加からFrameworkを選び、アプリのライブラリとして追加していきます。

![Frameworkの追加](https://storage.googleapis.com/zenn-user-upload/c7b907bd82262a72fcc0b77d.png)
*Frameworkの追加*

こうした場合、Swiftコード以外にもヘッダーファイル(.h)、`Info.plist`のファイルがあり、pbxprojファイルにも大量のツリー構成が挿入されます。

![Frameworkの追加した場合の構成](https://storage.googleapis.com/zenn-user-upload/a129035c6c6ec0221a7d04b0.png)

![Frameworkの追加した場合のpbxprojファイルの変更](https://storage.googleapis.com/zenn-user-upload/7a200054b82863c9c99e05f4.png)

こういった余計なファイルや、コンフリクトの要因となるファイルから脱却でき、pbxprojファイルをシンプルに保つことができます。

![新しくSwift Packageを追加した際のpbxprojの変更量](https://storage.googleapis.com/zenn-user-upload/5654047b0e2c5d2fe81c213c.png)

## メリット4: モジュール単位のビルド時間

これは、Frameworkの場合でも恩恵を受けられるメリットですが、モジュール単位でビルドができるため、フルビルドに比べ修正した個別のモジュールを選んだビルドは早くなりますので、大規模アプリになるほど恩恵は大きくなると思います。

![Schmes](https://storage.googleapis.com/zenn-user-upload/9edc3a9219c0b9bce4db6feb.png)


ここからは、SPMマルチモジュールによるデメリットを考えていきたいと思います。

# SPMマルチモジュールによるデメリット

## デメリット1: SPMの学習コスト

当然Package.swiftの書き方を学ぶ必要があるため、シングルモジュールに比べると追加の学習コストとなると思います。

## デメリット2: CI環境の変更

マルチモジュールになり、テストコードもモジュール単位となるため、既存のCIがある場合、各モジュールごとのテストも実行するように変更が必要となってくるでしょう。

と2つほど上げてみましたが、逆にそれ以外のデメリットはなさそうという印象です。
XcodeGenなどに比べても管理ファイルがPackage.swiftだけなので、今後のXcodeのアップデートによるメンテナンスコストの影響も抑えられそうです。

最後に今回アプリを開発していて、分かってきたSPMの懸念点とSwiftUIの懸念点をお伝えしたいと思います。

# 懸念点

## 懸念点1: Swift Packgeのモジュールを追加した際、Xcodeが怪しい挙動をする場合がある

新たにSwift Packageを追加した際、XcodeのSchemeが突如表示されなくなったり、それまで通っていた`import Foo` が `No such module`のエラーになることがありました。

こういう場合は一度Xcodeを立ち上げ直すと直ったり、変更したファイルをgit上で戻して追加し直すなどやっていると解消したりしました。

SPMあたりはまだ若干Xcodeが不安定な感じがします。

## 懸念点2: SwiftUIプレビューは頻繁に壊れる

これはSPMマルチモジュールとは別の問題なのですが、今回作成したFull SwiftUIのアプリのコード量でもプレビューはかなり不安定でした。

ViewComponentsあたりの軽いモジュールは安定しやすいですが、画面全体のモジュールになってくると、エラーがよく発生し、クリーンをしてみる、実機ビルドをしてみる、Xcodeを再起動してみるといったことを頻繁に繰り返す必要がありました。
また、プレビュー時のビルドはモジュールがキャッシュされているような感じはなく、フルビルドがよく走っている印象です。
そのため、もし既存のアプリで画面単位でのSwiftUIプレビューの安定化を期待しても、変わらない可能性が高いと思われます。
もしかしたら今後のXcodeのバージョンアップで改善されるかもしれません。（改善してほしい...）

# 最後に

今回サンプルアプリを作成するにあたり、主に以下の技術書、資料を参考にさせていただきました。

https://www.amazon.co.jp/gp/product/B08NCXF81P

https://tech.toreta.in/entry/2019/12/24/104612

https://speakerdeck.com/d_date/swift-package-centered-project-build-and-practice

もし何か間違いや、ご意見、感想などありましたらコメント頂けたら嬉しいです。