XcodeGenでマルチモジュール構成のプロジェクトを生成する

# はじめに
今までXcodeGenを使用したことがなかったので、お勉強がてらXcodeGenを用いてマルチモジュール構成のプロジェクトを生成するサンプルプロジェクトを作成しました。
他記事でも言われていましたが、そこそこ学習コストが高く色々と躓いたので、その過程で得た知見などを紹介できればと思います。

# モチベーション
XcodeGenを学習するモチベーションは以下の通りです。
- SPMでもマルチモジュール構成が実現できるが、どういった違いがあるのか知りたい

はじめはプロジェクトファイル(.pbxproj)をチーム開発でリポジトリ管理したときに、
コンフリクト祭りが起きる問題に今までのチーム開発でも感じていたので、
それを解決するツールとして導入を検討するために学習しようと思っていました。

しかし、Xcode16になってからはデフォルトでFolder構成になっていたり、Group構成からFolder構成に変換できる様になったことで、プロジェクトファイルのコンフリクト問題に頭を悩ませる必要がなくなった様です。

詳しくは以下記事をご参照ください。

https://zenn.dev/yimajo/articles/a86227cf85099f

# 環境
- Xcode: 16.2
- XcodeGen: 2.42.0

# XcodeGenのセットアップ
XcodeGenのGithubリポジトリのREADMEを頼りにセットアップしていきます。

https://github.com/yonaskolb/XcodeGen

XcodeGenはCLIツールなので、まずはツールをローカルにインストールします。
インストール方法は以下がありますが、私はどのツールも大体Homebrewで入れちゃうので今回もHomebrewに頼ります。

- Mint
- MakeFile
- Homebrew
- SwiftPackage

```
brew install xcodegen
```

バージョン確認で、きちんとバージョンが表示されたらインストール完了です。
```
% xcodegen --version 
Version: 2.42.0
```

## プロジェクトファイル生成手順

XcodeGen で xcodeproj ファイルを生成する手順は以下の通りです

1. spec ファイルを用意する
2. spec ファイルに定義したターゲットのソースディレクトリを作成する
3. `xcodegen genarate`を実行して xcodeproj ファイルを生成

※ specファイルとは、プロジェクトファイルの元となるファイルのことで、これを読み込んでXcodeGenはプロジェクトファイルを生成してくれます。

# マルチモジュール構成の xcodeproj ファイルを生成してみる

## 想定する構成
- MainApp: アプリケーションターゲット
  dependencies: [HogeFeature, Core]
  testtargets: [TestHogeFeature]
- HogeFeature: フィーチャーモジュール
  dependencies: [Core]
  testtargets: [TestHogeFeature]
- Core: コアモジュール
  dependencies: []
- TestHogeFeature: HogeFeatureの単体テスト
  dependencies: [HogeFeature]
- UITest: MainAppのUIテスト
  dependencies: [MainApp]

## spec ファイルの内容

ファイル名は`project.yml`にします。
`xcodegen genarate`実行時に、どの spec ファイルから xcodeproj ファイルを作成するかを指定する必要がありますが、デフォルトで`project.yml`を探す様になっているので、今回はデフォルトのファイル名をつけておきます。(ルートの spec ファイルはデフォルトの名前にした方が、運用としてわかりやすいし実行時もオプションを省ける)

以下が今回サンプルプロジェクトにて作成した最終的なspecファイルの内容です。

```yaml
name: MultimoduleSample
options:
  bundleIdPrefix: taichi.com.multimoduleSample
  minimumXcodeGenVersion: 2.41.0
  xcodeVersion: 16.1
  deploymentTarget:
    iOS: 17.0
  groupOrdering:
    - order: [MainApp, HogeFeature, Core, TestHogeFeature, UITest]
configs:
  Debug: debug
  AppStore: release
settings:
  base:
    # DEVELOPMENT_TEAM: taichi sato
    MARKETING_VERSION: 1.0.0
    CURRENT_PROJECT_VERSION: 1.0.0
    CODE_SIGN_STYLE: Automatic
    # Swiftのバージョン
    SWIFT_VERSION: 6.0
    # 以降の設定はXcode14のデフォルト設定
    ENABLE_USER_SCRIPT_SANDBOXING: YES
    GCC_DYNAMIC_NO_PIC: NO
    GCC_NO_COMMON_BLOCKS: YES
    GCC_C_LANGUAGE_STANDARD: gnu17
    CLANG_CXX_LANGUAGE_STANDARD: gnu++20
    ASSETCATALOG_COMPILER_GENERATE_SWIFT_ASSET_SYMBOL_EXTENSIONS: YES
    ENABLE_MODULE_VERIFIER: YES
    GENERATE_INFOPLIST_FILE: YES
  configs:
    debug:
      # ビルド成果物の最適化は行わずビルド速度を優先する
      SWIFT_OPTIMIZATION_LEVEL: -Onone
      # 以降の設定はXcode14のデフォルト設定
      VALIDATE_PRODUCT: NO
      GCC_OPTIMIZATION_LEVEL: "0"
    appstore:
      # ビルド速度は落ちるが、ビルド成果物の最適化を行う
      SWIFT_OPTIMIZATION_LEVEL: -Owholemodule
      # 以降の設定はXcode14のデフォルト設定
      VALIDATE_PRODUCT: YES
      GCC_OPTIMIZATION_LEVEL: "s"
targets:
  MainApp:
    type: application
    platform: iOS
    sources: [MainApp]
    settings:
      base:
        INFOPLIST_FILE: MainApp/Info.plist
        SWIFT_EMIT_LOC_STRINGS: YES
        ASSETCATALOG_COMPILER_GLOBAL_ACCENT_COLOR_NAME: AccentColor
    info:
      path: MainApp/Info.plist
      properties:
        CFBundleVersion: $(CURRENT_PROJECT_VERSION)
        CFBundleShortVersionString: $(MARKETING_VERSION)
        LSRequiresIPhoneOS: YES # iOSで実行する必要がある旨の設定
        UILaunchScreen: ""
        UIApplicationSupportsIndirectInputEvents: YES # アプリが一般的に間接入力メカニズムをサポートしていることを示すブール値(https://qiita.com/tamadeveloper/items/4be3c7e410831d2d5a4e)
        UISupportedInterfaceOrientations~iphone:
          [
            UIInterfaceOrientationPortrait,
            UIInterfaceOrientationLandscapeLeft,
            UIInterfaceOrientationLandscapeRight,
          ]
    dependencies:
      - target: HogeFeature
  HogeFeature:
    type: framework
    platform: iOS
    sources: [HogeFeature]
    dependencies:
      - target: Core
  Core:
    type: framework
    platform: iOS
    sources: [Core]
  TestHogeFeature:
    type: bundle.unit-test
    platform: iOS
    sources: [TestHogeFeature]
    dependencies:
      - target: HogeFeature
  UITest:
    type: bundle.ui-testing
    platform: iOS
    sources: [UITest]
    dependencies:
      - target: MainApp
schemes:
  MainApp:
    build:
      targets:
        MainApp: all
    run:
      config: Debug
    test:
      config: Debug
      gatherCoverageData: true
      coverageTargets: all
      targets:
        - name: TestHogeFeature
          parallelizable: true
          randomExecutionOrder: true
        - name: UITest
          parallelizable: true
          randomExecutionOrder: true
    profile:
      config: AppStore
    analyze:
      config: Debug
    archive:
      config: AppStore
  HogeFeature:
    templates:
      - FeatureModuleScheme
    templateAttributes:
      testTargetName: TestHogeFeature
schemeTemplates:
  FeatureModuleScheme:
    templates:
      - TestScheme
    build:
      targets:
        ${scheme_name}: all
  TestScheme:
    test:
      config: Debug
      gatherCoverageData: true
      coverageTargets: all
      targets:
        - name: ${testTargetName}
          parallelizable: true
          randomExecutionOrder: true
```

## 各ターゲットのディレクトリを作成する
上記でspecファイルを共有しているのですが、じゃあこれでプロジェクトファイルが作れる！というわけでもなく、まずは各ターゲット毎のディレクトリを作成する必要があります。

今回の例では、以下ターゲットがあるので同名のディレクトリを`xcodeproj`を生成したいディレクトリで作成します。

```
mkdir MainApp HogeFeature Core TestHogeFeature UITest
```

これをしないと、`xcodegen genarate`実行時に以下のように怒られます

```
% xcodegen generate
3 Spec validations errors:
	- Target "Core" has a missing source directory "~/work/swift/MultiModuleSample/Core"
	- Target "HogeFeature" has a missing source directory "~/work/swift/MultiModuleSample/HogeFeature"
	- Target "MainApp" has a missing source directory "~/work/swift/MultiModuleSample/MainApp"
	・・・省略
```

これで晴れて、プロジェクトファイルが生成できる様になったはずです。

## generate コマンドの実行

以下の様にコマンドを実行すると、同ディレクトリに xcodeproj ファイルが生成されます。

```
% xcodegen generate
⚙️  Generating plists...
⚙️  Generating project...
⚙️  Writing project...
Created project at ~/work/swift/MultiModuleSample/MultimoduleSample.xcodeproj
```

これでMultimoduleSample.xcodeprojが生成されたので、Xcodeで開いてみるとビルドが通るはずです。

# specファイルの各項目の説明

### `name`

```yaml
name: MultimoduleSample
```

これはプロジェクトの名前に使用されます。
今回の場合は`MultimoduleSample`を設定したので、生成される`xcodeproj`ファイルは`MultimoduleSample.xcodeproj`となっています。

### `options`

```yaml
options:
  bundleIdPrefix: taichi.com.multimoduleSample
  minimumXcodeGenVersion: 2.41.0
  xcodeVersion: 16.1
  deploymentTarget:
    iOS: 17.0
  groupOrdering:
    - order: [MainApp, HogeFeature, Core, TestHogeFeature, UITest]
```

生成するプロジェクトファイルのデフォルトの設定を、`options`に指定した項目の値に上書きできます。

例えば、今回指定しているのは以下です。

- `bundleIdPrefix`
```yaml
bundleIdPrefix: taichi.com.multimoduleSample
```

プロジェクトファイルに含まれる各ターゲットの`Bundle Identifier`の接頭辞を指定できます。
例えば、`MainApp`ターゲットの`Bundle Identifier`は`taichi.com.multimoduleSample.MainApp`になります。

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3359094/b01f65d2-70c3-a9a9-5145-0ec0165f3401.png)

- `minimumXcodeGenVersion`
```yaml
minimumXcodeGenVersion: 2.41.0
```

プロジェクトファイルを生成するときに使用する`xcodegen`コマンドの実行可能最低バージョンを指定できます。
今回は`2.42.0`を使用しているので上記設定ですと`xcodegen`でプロジェクトファイルを生成できます。
もし以下の様に設定した場合は、今回の例で使用する`xcodegen`ではプロジェクトファイルを生成できなくなります。

```yaml
minimumXcodeGenVersion: 2.41.0
```

- `deploymentTarget`
```yaml
deploymentTarget:
    iOS: 17.0
```

プラットフォーム毎(`iOS`や`macOS`など)にプロジェクト全体のデプロイメントターゲットのバージョンを指定できます。
iOSに対して設定する場合は、ビルド設定の`IPHONEOS_DEPLOYMENT_TARGET`に上書きして設定されます。
また、minimum deployments のバージョンも上書きされます。
後述しますが、ターゲット毎に設定できる項目もあります。

- `groupOrdering`
```yaml
groupOrdering:
    - order: [MainApp, HogeFeature, Core, TestHogeFeature, UITest]
```

Xcodeでプロジェクトファイルを開いたときにターゲットディレクトリの並びを指定できます。
上記例ですと、上から`MainApp, HogeFeature, Core, TestHogeFeature, UITest`となります。

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3359094/fa103c2b-9b60-4926-30c5-3fffc994be7d.png)

今回紹介した`options`の設定項目はほんの一例であり、以下ドキュメントに全量が記載されているので他に何があるか興味がある方はご参照ください。

https://github.com/yonaskolb/XcodeGen/blob/master/Docs/ProjectSpec.md#options

### `configs`

```yaml
configs:
  Debug: debug
  AppStore: release
```

Configurationの指定が行えます。
XcodeのPROJECT画面にあるConfigurationsの部分を指定できます。
今回は`Debug`と`AppStore`のConfigurationを定義しています。
各Configuration項目の右辺にある`debug`、`release`でビルド設定などのデフォルト値が決定されます。
`debug`であればデバッグ環境に合う設定(コンパイル速度を優先するような設定など)で、
`release`であればリリースする用途に合う設定(ビルドの最適化を優先してパフォーマンスの良い成果物を生成する様な設定など)にデフォルトで設定されます。

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3359094/ccf0761b-d197-92c5-97ae-b0e2d40790fc.png)

ちなみに、こちらの設定を行わない場合はデフォルトで以下のような設定になります。(Xcodeからプロジェクトを生成した時と同じConfiguration設定)
```yaml
configs:
  Debug: debug
  Release: release
```

https://github.com/yonaskolb/XcodeGen/blob/master/Docs/ProjectSpec.md#configs

## `settings`

```yaml
settings:
  base:
    # DEVELOPMENT_TEAM: taichi sato
    MARKETING_VERSION: 1.0.0
    CURRENT_PROJECT_VERSION: 1.0.0
    CODE_SIGN_STYLE: Automatic
    # Swiftのバージョン
    SWIFT_VERSION: 6.0
    # 以降の設定はXcode14のデフォルト設定
    ENABLE_USER_SCRIPT_SANDBOXING: YES
    GCC_DYNAMIC_NO_PIC: NO
    GCC_NO_COMMON_BLOCKS: YES
    GCC_C_LANGUAGE_STANDARD: gnu17
    CLANG_CXX_LANGUAGE_STANDARD: gnu++20
    ASSETCATALOG_COMPILER_GENERATE_SWIFT_ASSET_SYMBOL_EXTENSIONS: YES
    ENABLE_MODULE_VERIFIER: YES
    GENERATE_INFOPLIST_FILE: YES
  configs:
    debug:
      # ビルド成果物の最適化は行わずビルド速度を優先する
      SWIFT_OPTIMIZATION_LEVEL: -Onone
      # 以降の設定はXcode14のデフォルト設定
      VALIDATE_PRODUCT: NO
      GCC_OPTIMIZATION_LEVEL: "0"
    appstore:
      # ビルド速度は落ちるが、ビルド成果物の最適化を行う
      SWIFT_OPTIMIZATION_LEVEL: -Owholemodule
      # 以降の設定はXcode14のデフォルト設定
      VALIDATE_PRODUCT: YES
      GCC_OPTIMIZATION_LEVEL: "s"
```

`settings`では、プロジェクト全体のビルド設定(Build Settingsタブの設定)を上書きできるようになります。
ここで設定した項目は基本的にはプロジェクトに限らず各ターゲットのビルド設定にも上書きされますが、後述するターゲット毎にビルド設定を上書きした場合は、ターゲット単位の設定の方が適用されます。

`settings`の下階層は以下に分かれます。
- `base`
```yaml
  base:
    MARKETING_VERSION: 1.0.0
    CURRENT_PROJECT_VERSION: 1.0.0
    CODE_SIGN_STYLE: Automatic
    # Swiftのバージョン
    SWIFT_VERSION: 6.0
    # 以降の設定はXcode14のデフォルト設定
    ENABLE_USER_SCRIPT_SANDBOXING: YES
    GCC_DYNAMIC_NO_PIC: NO
    GCC_NO_COMMON_BLOCKS: YES
    GCC_C_LANGUAGE_STANDARD: gnu17
    CLANG_CXX_LANGUAGE_STANDARD: gnu++20
    ASSETCATALOG_COMPILER_GENERATE_SWIFT_ASSET_SYMBOL_EXTENSIONS: YES
    ENABLE_MODULE_VERIFIER: YES
```
`base`の下階層に定義されたビルド設定は、Configurations関係なく同じ値が設定されることになります。
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3359094/bc6a8dc3-82b9-4244-f62e-1687242a42a6.png)


- `configs`
```yaml
  configs:
    debug:
      # ビルド成果物の最適化は行わずビルド速度を優先する
      SWIFT_OPTIMIZATION_LEVEL: -Onone
      # 以降の設定はXcode14のデフォルト設定
      VALIDATE_PRODUCT: NO
      GCC_OPTIMIZATION_LEVEL: "0"
    appstore:
      # ビルド速度は落ちるが、ビルド成果物の最適化を行う
      SWIFT_OPTIMIZATION_LEVEL: -Owholemodule
      # 以降の設定はXcode14のデフォルト設定
      VALIDATE_PRODUCT: YES
      GCC_OPTIMIZATION_LEVEL: "s"
```
`configs`ではConfiguration毎に異なる値をビルド設定の項目に定義することができます。
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3359094/0bb1fa50-ebfc-c96f-26d1-64cdba37dcc4.png)


- `groups`
`settingsGroups`で定義したビルド設定をグループ名を指定して適用することができる項目です。

`settingsGroups`では再利用可能なビルド設定のまとまりを定義することができます。
例えば以下例では、`common`というグループ名を定義し、プロジェクトのビルド設定に適用しています。

```diff_yaml
+ settingGroups:
+   common:
+     ENABLE_MODULE_VERIFIER: NO
  settings:
+   groups:
+     - common
    base:
      # DEVELOPMENT_TEAM: taichi sato
      MARKETING_VERSION: 1.0.0
      CURRENT_PROJECT_VERSION: 1.0.0
      CODE_SIGN_STYLE: Automatic
      # Swiftのバージョン
      SWIFT_VERSION: 6.0
      # Info.plistがなければ自動で生成する
      GENERATE_INFOPLIST_FILE: YES
      # 以降の設定はXcode14のデフォルト設定
      ENABLE_USER_SCRIPT_SANDBOXING: YES
      GCC_DYNAMIC_NO_PIC: NO
      GCC_NO_COMMON_BLOCKS: YES
      GCC_C_LANGUAGE_STANDARD: gnu17
      CLANG_CXX_LANGUAGE_STANDARD: gnu++20
      ASSETCATALOG_COMPILER_GENERATE_SWIFT_ASSET_SYMBOL_EXTENSIONS: YES
      ENABLE_MODULE_VERIFIER: YES
    configs:
      debug:
        # ビルド成果物の最適化は行わずビルド速度を優先する
        SWIFT_OPTIMIZATION_LEVEL: -Onone
        # 以降の設定はXcode14のデフォルト設定
        VALIDATE_PRODUCT: NO
        GCC_OPTIMIZATION_LEVEL: "0"
      appstore:
        # ビルド速度は落ちるが、ビルド成果物の最適化を行う
        SWIFT_OPTIMIZATION_LEVEL: -Owholemodule
        # 以降の設定はXcode14のデフォルト設定
        VALIDATE_PRODUCT: YES
        GCC_OPTIMIZATION_LEVEL: "s"
```

`base`、`configs`、`groups`では同じビルド設定を定義することもできますが、そうなった時に適用される優先順位は以下順になります。
1. `configs`
1. `base`
1. `groups`

つまり`configs`に設定した値は優先度が高く、`groups`に設定した値は優先度が低いです。

ビルド設定の項目は以下Appleの公式ドキュメントを見たり、Xcodeでプロジェクトを生成したときのデフォルトのビルド設定を見ながら設定しました。(設定項目を一つ一つ説明するのは流石に大変なので、ここではどうやって設定項目を決めたかというところにとどめておきます。)

https://developer.apple.com/documentation/xcode/build-settings-reference

以下はXcodeGenの`settings`関連のドキュメントです。

#### settingsGroups
https://github.com/yonaskolb/XcodeGen/blob/master/Docs/ProjectSpec.md#setting-groups

#### settings
https://github.com/yonaskolb/XcodeGen/blob/master/Docs/ProjectSpec.md#settings

### `targets`

```yaml
targets:
  MainApp:
    type: application
    platform: iOS
    sources: [MainApp]
    settings:
      base:
        INFOPLIST_FILE: MainApp/Info.plist
        SWIFT_EMIT_LOC_STRINGS: YES
        ASSETCATALOG_COMPILER_GLOBAL_ACCENT_COLOR_NAME: AccentColor
    info:
      path: MainApp/Info.plist
      properties:
        CFBundleVersion: $(CURRENT_PROJECT_VERSION)
        CFBundleShortVersionString: $(MARKETING_VERSION)
        LSRequiresIPhoneOS: YES # iOSで実行する必要がある旨の設定
        UILaunchScreen: ""
        UIApplicationSupportsIndirectInputEvents: YES # アプリが一般的に間接入力メカニズムをサポートしていることを示すブール値(https://qiita.com/tamadeveloper/items/4be3c7e410831d2d5a4e)
        UISupportedInterfaceOrientations~iphone:
          [
            UIInterfaceOrientationPortrait,
            UIInterfaceOrientationLandscapeLeft,
            UIInterfaceOrientationLandscapeRight,
          ]
    dependencies:
      - target: HogeFeature
  HogeFeature:
    type: framework
    platform: iOS
    sources: [HogeFeature]
    dependencies:
      - target: Core
  Core:
    type: framework
    platform: iOS
    sources: [Core]
  TestHogeFeature:
    type: bundle.unit-test
    platform: iOS
    sources: [TestHogeFeature]
    dependencies:
      - target: HogeFeature
  UITest:
    type: bundle.ui-testing
    platform: iOS
    sources: [UITest]
    dependencies:
      - target: MainApp
```

`targets`ではプロジェクトの各ターゲットの定義を行うことができます。
`targets`の一つ下の階層にターゲット名を定義し、それぞれのターゲット名の下階層に必要な設定項目を追加していきます。

今回定義した設定項目について、軽く見ていきます。

#### `type`
ターゲットの種類を指定します。

例として以下ターゲットの種類があります。
| product type | 意味 |
| - | - |
| application | アプリケーションターゲット(`@main`が含まれる様なターゲットのイメージ) |
| framework | Embed Framework用のターゲット(今回の例では子モジュールとして使用)|
| bundle.ui-testing | UIテスト用のターゲット |
| bundle.unit-test | 単体テスト用のターゲット |

その他にもさまざまな種類があり、以下公式ドキュメントに全量が記載されています。

https://github.com/yonaskolb/XcodeGen/blob/master/Docs/ProjectSpec.md#product-type

#### `platform`
ターゲットのプラットフォームを指定できます。これにより、ビルド設定に指定したプラットフォームのデフォルト設定がされます。

今回の例ではiOSアプリ開発を想定しているので、指定しているプラットフォームは`iOS`にしていますが、
以下例のようにマルチプラットフォーム対応にすることも可能です。

```yaml
targets:
  Sample:
    type: framework
    platform: [iOS, tvOS]
    sources: [Sample]
```

こうした場合、`Sample_iOS`と`Sample_tvOS`二つのターゲットが生成されプラットフォーム毎のターゲット構成になります。

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3359094/d79609fc-b168-9860-d104-bef39d4be748.png)

https://github.com/yonaskolb/XcodeGen/blob/master/Docs/ProjectSpec.md#platform

#### `supportedDestinations`
こちらも先ほどの`platform`と似ていますが、ターゲットでサポートするプラットフォームを指定します。
違うこととしては、複数のプラットフォームを選択しても、ターゲットがプラットフォーム毎に生成されるのではなく、一つのターゲットで複数のプラットフォームをサポートすることになります。

例えば、以下の様な定義では、`Sample`というターゲットでは`iOS`と`tvOS`の両プラットフォームをサポートする様になります。
```yaml
  Sample:
    type: framework
    supportedDestinations: [iOS, tvOS]
    sources: [Sample]
```

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3359094/428b29e3-2350-1e01-9bf5-4898cf86927e.png)

https://github.com/yonaskolb/XcodeGen/blob/master/Docs/ProjectSpec.md#supported-destinations

#### `sources`
ターゲットに含めるソースディレクトリを指定できます。
今回の例で`HogeFeature`では`HogeFeature`というディレクトリをソースディレクトリと扱っています。

```yaml
 HogeFeature:
    type: framework
    platform: iOS
    sources: [HogeFeature]
    dependencies:
      - target: Core
```

上記の様な記載にした場合、プロジェクトファイルが生成されるディレクトリと同階層にある`HogeFeature`ディレクトリをソースディレクトリとして扱い、`HogeFeature`内も再起的に解析してくれます。

一つのターゲットに対して、複数のソースディレクトリを指定することも可能で、以下の様にすることで`Sample`、`Sample2`両方のソースファイルを`Sample`ターゲットでコンパイルすることになります。

```yaml
Sample:
    type: framework
    supportedDestinations: [iOS]
    sources:
      - Sample
      - Sample2
```

他にもソースディレクトリやソースファイルの指定方法がありますので、そちらは公式ドキュメントをご覧ください。

https://github.com/yonaskolb/XcodeGen/blob/master/Docs/ProjectSpec.md#sources

#### `dependencies`
ターゲットの依存関係を定義することができます。

今回の例では、`MainApp`は`HogeFeature`と`Core`に、`HogeFeature`は`Core`に依存させるようにしているのですが、その時の依存関係の定義は以下になります。

```yaml
targets:
  MainApp:
    ・・・省略
    dependencies:
      - target: HogeFeature
　HogeFeature:
    ・・・省略
    dependencies:
      - target: Core
　Core:
    ・・・省略
```

`Core`が`MainApp`の`dependencies`に含まれていないのは、`HogeFeature`側で`Core`に依存しているためです。

今回はマルチモジュールにすることを目標としているため、ターゲット間での依存関係しか取り扱っていませんが、ビルド済みのFrameworkやSDK、SPMで管理されているライブラリなどの依存関係の定義をする方法ありますので、そちらは以下公式ドキュメントをご覧ください。

https://github.com/yonaskolb/XcodeGen/blob/master/Docs/ProjectSpec.md#dependency

#### `settings`
ターゲット毎のビルド設定を定義することができます。
ここで定義したビルド設定は、上述しているプロジェクト全体に適用するビルド設定よりも優先度が高いです。

こちらの設定方法は、上述しているプロジェクト全体に適用するビルド設定方法と同じなので、設定方法は省略します。

#### `info`
Info.plistを自動生成し、Info.plistの項目の値を上書きすることができます。
今回の例では、`MainApp`ターゲットにおいて、`MainApp/Info.plist`にInfo.plistを生成し、`properties`の下階層で上書きする項目と値を定義しています。

```yaml
  MainApp:
    ・・・省略
    info:
      path: MainApp/Info.plist
      properties:
        CFBundleVersion: $(CURRENT_PROJECT_VERSION)
        CFBundleShortVersionString: $(MARKETING_VERSION)
        LSRequiresIPhoneOS: YES # iOSで実行する必要がある旨の設定
        UILaunchScreen: ""
        UIApplicationSupportsIndirectInputEvents: YES # アプリが一般的に間接入力メカニズムをサポートしていることを示すブール値(https://qiita.com/tamadeveloper/items/4be3c7e410831d2d5a4e)
        UISupportedInterfaceOrientations~iphone:
          [
            UIInterfaceOrientationPortrait,
            UIInterfaceOrientationLandscapeLeft,
            UIInterfaceOrientationLandscapeRight,
          ]
```

上記で設定している`properties`の内容は下記記事を参考にしています。

https://qiita.com/macneko/items/0d9bbf1dfd857e2375fc

https://zenn.dev/nkysyuichi/articles/c3c2a6a63e1404#info.plist-%E3%81%8B%E3%82%89-uiapplicationscenemanifest-%E3%82%92%E3%81%99%E3%81%B9%E3%81%A6%E6%B6%88%E3%81%99

### `schemes`
プロジェクトのスキーム構成を定義することができます。
スキームとは、Xcodeでいうと以下部分に当たります。

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3359094/bfa0d743-2026-b945-f55d-33b4dee3224b.png)


```yaml
schemes:
  MainApp:
    build:
      targets:
        MainApp: all
    run:
      config: Debug
    test:
      config: Debug
      gatherCoverageData: true
      coverageTargets: all
      targets:
        - name: TestHogeFeature
          parallelizable: true
          randomExecutionOrder: true
        - name: UITest
          parallelizable: true
          randomExecutionOrder: true
    profile:
      config: AppStore
    analyze:
      config: Debug
    archive:
      config: AppStore
  HogeFeature:
    templates:
      - FeatureModuleScheme
    templateAttributes:
      testTargetName: TestHogeFeature
schemeTemplates:
  FeatureModuleScheme:
    templates:
      - TestScheme
    build:
      targets:
        ${scheme_name}: all
  TestScheme:
    test:
      config: Debug
      gatherCoverageData: true
      coverageTargets: all
      targets:
        - name: ${testTargetName}
          parallelizable: true
          randomExecutionOrder: true
```

`schemes`の一つ下の階層で、任意のスキーム名を定義し、各スキームの各アクション(`build`, `run`, `test`, `profile`, `analyze`, `archive`)の設定などを定義していきます。

今回の例ではテストアクションの設定が中心でしたので、そちらの設定に関わる以下項目について説明します。

- `gatherCoverageData`
コードカバレッジを取得するかしないかを設定します。(`true`にすると取得するようになります。)
- `coverageTargets`
コードカバレッジを取得するターゲットを指定します。
基本的に依存しているターゲット全てを指定する`all`で良いと考えていますが、配列でターゲット名を指定することもできます。
- `targets`
テストターゲット(UIテストターゲットや単体テストターゲットなど)を指定します。
ターゲットは`name`でターゲット名を指定します。
また、`parallelizable`(並列テストをするかどうか)、`randomExecutionOrder`(テストケース実行順をランダムにする可動化」)という設定も`targets`の下階層で設定できます。
テストは実行時間が短い方が良いというのと、テストケース毎に結合していることは望ましい状態ではないという観点から、例では両方の設定を`true`にしています。

また`HogeFeature`には`templete`という再利用可能なスキーム定義を用いています。
再利用可能なスキームは`schemeTemplates`で設定することができます。

`schemeTemplates`の一つ下の階層にテンプレート名を定義し、さらにテンプレート名の下の階層に`schemes`の各ターゲットに行っていた方法と同じようにスキーム構造を定義することができます。

`${scheme_name}`というものを`schemeTemplates`の定義内で使用していますが、こちらはテンプレート使用元のスキーム名が展開されるようになっています。
また似たもので`${testTargetName}`という記載もありますが、こちらはテンプレート使用元で`templateAttributes`から何を渡すかを指定することができます。

結局`HogeFeature`のスキームでは以下のような定義になっています。

>全てのアクションでビルドすることができるようにして、テスト時は`TestHogeFeature`をテスト対象とするようにする

`FeatureModuleScheme`という再利用可能なスキーム定義を定義した理由としては、`FeatureModule`対応する単体テストターゲットを作成するようにしたいからです。(昨日毎にテストを分けることで複数人で開発する時のコンフリクトのリスクを軽減したい)
ただ対応するテストターゲットを紐づけるのは定型的な定義を書く必要があるので、`schemeTemplates`によって定型的な定義の記述量を減らすようにしました。

そのほかスキーム定義についての詳細は以下公式ドキュメントをご確認ください。

https://github.com/yonaskolb/XcodeGen/blob/master/Docs/ProjectSpec.md#scheme

# XcodeGenは現環境で使う必要があるのか
一通りXcodeGenを使う方法を説明してみましたが、やはり学習コストは高いという意見に関しては同意です。
今までXcodeがよしなに設定したりしてくれていたものを、specファイルで自分で設定していくことになるので、普段意識しないビルド設定などにも目を向けなきゃだったり、specファイルの使用も膨大なのが原因かなと感じました。

そんなXcodeGenですが、大きな学習コストをかけてまで導入したいかというと、私は新しく立ち上げるプロジェクトに対して使用する必要はないかなと思いました。
その理由としては、以下のとおりです。

- プロジェクトファイルのコンフリクト問題を解決することができるツールではあるが、Xcode16からはツールを使わずともコンフリクトのリスクが激減したため、学習コストをかけてまで得られる旨みがない
- マルチモジュール構成において、依存関係がspecファイルで一目見てわかるメリットはあるが、SPMでもそれは同じで、SPMと比較したときに学習コストという観点ではSPMの方が直感的で低いという感想なのでSPMで対応した方が手軽なのではと思う
- XcodeGenは純正のツールではないので、Xcodeのアップデートなどにシームレスに対応できない場合が多い(そういう意味でもマルチモジュール構成という文脈ではSPMを支持したい)

当初の目標であった、SPMとの違いだったりにも若干上記で触れましたが、もう一つの違いとしてSPMではSPMに対応したライブラリしか扱えない（私が知る限り、、）のに対して、XcodeGenであればSPMだけでなくメジャーどころであるCocoaPodsやCarthageのライブラリにも対応していることです。
そのため、マルチモジュール構成を実現するときに、SPMに対応していないライブラリを扱う場合はXcodeGenを使用することを検討するのはアリかもです。

ただ、Apple純正のパッケージ管理ツールということもあり、SPMに対応しているライブラリは増えているのと、XcodeなどiOS開発周りのツール互換を考えても、マルチモジュール構成を行う際はSPMでできるならSPMを採用するでいいのかなというのが感想です。

# おわり
なかなか長ったらしい記事になってしまいました、、
XcodeGen想像以上に難しかったですが、普段意識しないビルド設定だったりを意識することが増えたので、得るものはあったなと思います。
少しですが、ビルド設定周りの嫌悪感が減ったかなと思います。

今回サンプルで作成したプロジェクトはGitHubリポジトリにも上げていますので、よければご覧ください。

https://github.com/stotic-dev/MultimoduleSampleApp

基本的に公式ドキュメントを参照してキャッチアップした内容ですが、解釈違いなどありましたらご指摘いただけますと幸いです