XcodeGenを用いて中規模プロジェクトをリリースした話

## はじめに
久しぶりの投稿。
題名の通り、期間は半年、エンジニアは二人という開発体制のプロジェクトに[XcodeGen](https://github.com/yonaskolb/XcodeGen)を導入し、先日プロジェクトをリリースしましたので、その知見を共有します。

## [XcodeGen](https://github.com/yonaskolb/XcodeGen)とは？
iOSアプリをチーム開発のついて回る問題、**`project.pbxproj`ファイルがコンフリクトしまくる問題**があります。その問題解決できるツールが**XcodeGen**です。
最近取り入れてるプロジェクトは増えてきてるとはいえ、まだまだ発展途上のツールでガンガンアプデされており、機能がもりもりです。
XcodeGenは主に以下をやってくれます。(*1)

### XcodeGenがやってくれること
- コマンド一発で`.xcodeproj`を`project.yml`の設定を元に生成。
    - ライブラリ依存、フレームワークも管理
    - Build Configurationも管理
    - Development Team、Provisioning Profileも管理
    - Embedded Frameworkも管理
    - etc...
- ファイルソートもしてくれる

要は`.xcodeproj`ファイルを`project.yml`に置き換えてるだけですね。
そうするとコンフリクト問題になっていたディレクトリ、ファイル構成は実際のディレクトリ構成から作成するので、コンフリクトはほぼなくなります。
後は、`.xcodeproj`を丸ごと`.gitignore`指定してコマンドを開発ルールに取り入れれば、OKです。

https://github.com/yonaskolb/XcodeGen/blob/master/Docs/ProjectSpec.md が一番参考になります。

## 完成設定ファイル
[Mint](https://github.com/yonaskolb/mint)でXcodeGenを導入。
リリース後完成した `project.yml` は以下です。(プロジェクト名や長いライブラリ依存記述などは書き換えてますので、ご容赦を)
個別設定ファイルは`.xcconfig`に切り分けてたりしました。

```yaml:project.yml
name: test-xcodegen
# BuildConfiguration定義
configs:
  Debug: debug
  Stg: debug
  Release: release
# 別途読み込みxcconfigファイル
configFiles:
  Debug: configs/Debug.xcconfig
  Stg: configs/Stg.xcconfig
  Release: configs/Release.xcconfig
# オプション
options:
  developmentLanguage: ja
# テンプレ設定
settingGroups:
  testSettings:
    SWIFT_OBJC_BRIDGING_HEADER: ${PRODUCT_NAME}/Applications/test-Bridging-Header.h
    CODE_SIGN_STYLE: Manual
    SWIFT_VERSION: 5.0
    TARGETED_DEVICE_FAMILY: "1,2"
    INFOPLIST_FILE: test/Resources/Info.plist
    CLANG_ANALYZER_LOCALIZABILITY_NONLOCALIZED: YES
    OTHER_LINKER_FLAGS: $(inherited) -ObjC
    CODE_SIGN_ENTITLEMENTS: test/Resources/test_development.entitlements
    DEBUG_INFORMATION_FORMAT: dwarf-with-dsym
  testFrameworkSettings:
    CODE_SIGN_STYLE: Automatic
    LD_RUNPATH_SEARCH_PATHS: ${inherited} @executable_path/Frameworks @loader_path/Frameworks
    PRODUCT_BUNDLE_IDENTIFIER: test-framework.${PRODUCT_NAME}
targets:
  # メインプロジェクト
  test:
    type: application
    platform: iOS
    scheme: {}
    deploymentTarget: "11.0"
    sources:
      - test
      - path: test/Resources/Generated/Assets-Constants.swift
        optional: true
        type: file
      - path: test/Resources/Generated/Colors-Constants.swift
        optional: true
        type: file
      - path: test/Resources/Generated/L10n-Constants.swift
        optional: true
        type: file
    # メイン設定
    settings:
      groups: [testSettings]
      configs:
        Debug:
          ODE_SIGN_IDENTITY: Apple Development
          DEVELOPMENT_TEAM: hogehoge
          PROVISIONING_PROFILE_SPECIFIER: test.debug
        Stg:
          CODE_SIGN_IDENTITY: Apple Distribution
          DEVELOPMENT_TEAM: hogehoge
          PROVISIONING_PROFILE_SPECIFIER: test.stg
        Release:
          CODE_SIGN_IDENTITY: iPhone Distribution
          DEVELOPMENT_TEAM: hogehoge
          PROVISIONING_PROFILE_SPECIFIER: test.release
          CODE_SIGN_ENTITLEMENTS: test/Resources/test_production.entitlements
    # 依存ライブラリ、フレームワーク
    dependencies:
      - target: TestFramework
      - framework: SDK/Ad/test.framework
        embed: false
      - carthage: NavigationNotice
      - carthage: Nuke
      - carthage: Reusable
      - carthage: RxCocoa
      - carthage: RxRelay
      - carthage: RxSwift
      - carthage: RxSwiftExt
      - carthage: RxWebKit
      - carthage: SVProgressHUD
      - carthage: TagListView
      - carthage: TransitionableTab
    # 追加Build Phases
    preBuildScripts:
      - script: ${PODS_ROOT}/SwiftGen/bin/swiftgen config run --config swiftgen/app-swiftgen.yml
        name: Generate resources with SwiftGen
        outputFiles:
          - ${PRODUCT_NAME}/Resources/Generated/Assets-Constants.swift
          - ${PRODUCT_NAME}/Resources/Generated/Colors-Constants.swift
          - ${PRODUCT_NAME}/Resources/Generated/L10n-Constants.swift
      - script: |
                mint run mono0926/LicensePlist license-plist --output-path ${PRODUCT_NAME}/Resources/Settings.bundle
        name: Run license-plist
      - script: |
                cp "${PROJECT_DIR}/${PROJECT_NAME}/Resources/Firebase/GoogleService-Info_${CONFIGURATION}.plist" "${BUILT_PRODUCTS_DIR}/${PRODUCT_NAME}.app/GoogleService-Info.plist"
        name: Run Firebase
    postBuildScripts:
      - script: mint run SwiftLint swiftlint
        name: Run SwiftLint
      - script: "\"${PODS_ROOT}/FirebaseCrashlytics/run\""
        name: Run Crashlytics
        inputFiles:
          - ${DWARF_DSYM_FOLDER_PATH}/${DWARF_DSYM_FILE_NAME}/Contents/Resources/DWARF/${TARGET_NAME}
          - $(SRCROOT)/$(BUILT_PRODUCTS_DIR)/$(INFOPLIST_PATH)
  testTests:
    # テスト設定は省略
  testUITests:
    # テスト設定は省略
  # Embedded Framework
  TestFramework:
    type: framework
    platform: iOS
    scheme: {}
    deploymentTarget: "11.0"
    sources:
      - Datasource
      - path: "TestFramework/Resources/Generated/L10n-Constants.swift"
        optional: true
        type: file
    settings:
      groups: [testFrameworkSettings]
    dependencies:
      - carthage: APIKit
      - carthage: CryptoSwift
      - carthage: Realm
      - carthage: RealmSwift
      - carthage: SwiftProtobuf
    preBuildScripts:
      - script: ${PODS_ROOT}/SwiftGen/bin/swiftgen config run --config swiftgen/framework-swiftgen.yml
        name: Generate resources with SwiftGen
        outputFiles:
          - ${PRODUCT_NAME}/Resources/Generated/L10n-Constants.swift
  TestFrameworkTests:
    # テスト設定は省略
```

```env.xcconfig
// Debug.xcconfig
SWIFT_ACTIVE_COMPILATION_CONDITIONS = DEBUG
GCC_PREPROCESSOR_DEFINITIONS = DEBUG=1 $(inherited)
GCC_OPTIMIZATION_LEVEL = 0
ONLY_ACTIVE_ARCH = YES
ENABLE_TESTABILITY = YES
GCC_DYNAMIC_NO_PIC = NO
MTL_ENABLE_DEBUG_INFO = YES
SWIFT_OPTIMIZATION_LEVEL = -Onone
OTHER_SWIFT_FLAGS = $(inherited) -Xfrontend -debug-time-function-bodies
DISPLAY_NAME_PREFIX = debug-
PRODUCT_BUNDLE_IDENTIFIER = test.debug
ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon-debug
// Stg.xcconfig
SWIFT_ACTIVE_COMPILATION_CONDITIONS = STG
GCC_PREPROCESSOR_DEFINITIONS = STG=1 $(inherited)
GCC_OPTIMIZATION_LEVEL = 0
ONLY_ACTIVE_ARCH = YES
ENABLE_TESTABILITY = YES
GCC_DYNAMIC_NO_PIC = NO
MTL_ENABLE_DEBUG_INFO = YES
SWIFT_OPTIMIZATION_LEVEL = -Onone
OTHER_SWIFT_FLAGS = $(inherited) -Xfrontend -debug-time-function-bodies
DISPLAY_NAME_PREFIX = stg-
PRODUCT_BUNDLE_IDENTIFIER = test.stg
ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon-stg
// Release.xcconfig
ENABLE_NS_ASSERTIONS = NO
VALIDATE_PRODUCT = YES
MTL_ENABLE_DEBUG_INFO = NO
SWIFT_OPTIMIZATION_LEVEL = -Owholemodule
PRODUCT_BUNDLE_IDENTIFIER = test.release
ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon
```

## 知見
色々困ったことや工夫したことを書いていきます。とはいえ、まだまだ新しい記述なども増えていってますので、[ProjectSpec](https://github.com/yonaskolb/XcodeGen/blob/master/Docs/ProjectSpec.md)を見ながら参考にすると良いです。

### Build Configuration

```yaml:project.yml
# BuildConfiguration定義
configs:
  Debug: debug
  Stg: debug
  Release: release
```
定義は簡単に書けます。
後は、定義名をキーにして、設定を分けて書いたりできます。

#### 個別のプロジェクト設定

```yaml:project.yml
targets:
  # メインプロジェクト
  test:
    # メイン設定
    settings:
      configs:
        Debug:
          ODE_SIGN_IDENTITY: Apple Development
          DEVELOPMENT_TEAM: hogehoge
          PROVISIONING_PROFILE_SPECIFIER: test.debug
```

個別の設定は上記のように書くことができます。

```yaml:project.yml
# 別途読み込みxcconfigファイル
configFiles:
  Debug: configs/Debug.xcconfig
  Stg: configs/Stg.xcconfig
  Release: configs/Release.xcconfig
```

ですが、個別の設定を`project.yml`に埋め込むと煩雑になってしまいました。
そこで `configs/{env}.xcconfig` の記述を逃して、管理するようにしました。

### 共通のプロジェクト設定
```yaml:project.yml
# テンプレ設定
settingGroups:
  testSettings:
    # 設定
  testFrameworkSettings:
    # 設定
targets:
  # メインプロジェクト
  test:
    settings:
      groups: [testSettings]
  # EmbbededFramework
  TestFramework:
    settings:
      groups: [testSettings]
```

`settingGroups` で定義すれば、ここの`target`で用いれるようになるので便利です。
なので、基本的には共通の設定は`settingGroups`で、BuildConfigurationごとの個別設定は`.xcconfig`に分けるようにしました。

### ソースコード
```yaml:project.yml
targets:
  # メインプロジェクト
  test:
    sources:
      - test # 参照ディレクトリ
```

ソースコードはディレクトリ指定さえしておけば、再起的にCompileSourceとして読み込んでくれます。

#### ビルド時生成ファイルの参照
```yaml:project.yml
targets:
  # メインプロジェクト
  test:
    sources:
      # ビルド時生成ファイルの参照記述
      - path: test/Resources/Generated/Assets-Constants.swift
        optional: true
        type: file
      # 省略
    preBuildScripts:
      - script: ${PODS_ROOT}/SwiftGen/bin/swiftgen config run --config swiftgen/app-swiftgen.yml
        name: Generate resources with SwiftGen
        outputFiles:
          - ${PRODUCT_NAME}/Resources/Generated/Assets-Constants.swift
          # 省略
```

[SwiftGen](https://github.com/SwiftGen/SwiftGen)や[R.swift](https://github.com/mac-cain13/R.swift) などのビルド時生成ファイルが存在する場合は、参照先を上記のように加えなければなりません。
さらにBuild Phasesの追加スクリプト、`preBuildScripts.outputFiles`でそれぞれ記述する必要があります。

### 依存ライブラリ、フレームワーク
```yaml:project.yml
targets:
  # メインプロジェクト
  test:
    # 依存ライブラリ、フレームワーク
    dependencies:
      - target: TestFramework # EmbbededFramework
      - framework: SDK/Ad/test.framework # 手動読み込みフレームワーク
        embed: false
      - carthage: NavigationNotice # Carthage
      - carthage: Nuke
```
EmbbededFramework、Carthageは簡単に記入できます。
違う方法もあるかもしれませんが、手動読み込みフレームワークはプロジェクトディレクトリ外+Path指定してあげる必要がありました。(*1)
Swift Packageも`dependencies.package` の記述のみで行けるので、試してみたいですね。

#### CococaPods
当初はCocoaPodsは導入せず、Carthageだけで行こうと思いましたが、広告系の導入はCocoaPodsが必要となり、導入することになりました。ですが、XcodeGenでは、CocoaPodsの依存解決が対応してませんでした。(*1)
そこで以下のようなMakeコマンドを追加してXcodeGenした後に`pod install`をするルールに切り替えました。

```makefile:Makefile
xcodegen:
	mint run XcodeGen xcodegen
	bundler exec pod install
```

xcconfigファイル、Pod用のビルドスクリプトを記述すれば、解決できるかもですが、Podの方に依存した方が安全と判断したため、上記のやり方にしました。

### Build Phases
#### preBuildScripts
```yaml:project.yml
targets:
  # メインプロジェクト
  test:
    preBuildScripts:
      - script: ${PODS_ROOT}/SwiftGen/bin/swiftgen config run --config swiftgen/app-swiftgen.yml
        name: Generate resources with SwiftGen
        outputFiles:
          - ${PRODUCT_NAME}/Resources/Generated/Assets-Constants.swift
          - ${PRODUCT_NAME}/Resources/Generated/Colors-Constants.swift
          - ${PRODUCT_NAME}/Resources/Generated/L10n-Constants.swift
      - script: |
                mint run mono0926/LicensePlist license-plist --output-path ${PRODUCT_NAME}/Resources/Settings.bundle
        name: Run license-plist
      - script: |
                cp "${PROJECT_DIR}/${PROJECT_NAME}/Resources/Firebase/GoogleService-Info_${CONFIGURATION}.plist" "${BUILT_PRODUCTS_DIR}/${PRODUCT_NAME}.app/GoogleService-Info.plist"
        name: Run Firebase
```

ビルド前に走らせるスクリプトです。
LicensePlist、SwiftGen、GoogleService-Info.plistの環境別読み込みなどしてます。

#### postBuildScripts

```yaml:project.yml
targets:
  # メインプロジェクト
  test:
    postBuildScripts:
      - script: mint run SwiftLint swiftlint
        name: Run SwiftLint
      - script: "\"${PODS_ROOT}/FirebaseCrashlytics/run\""
        name: Run Crashlytics
        inputFiles:
          - ${DWARF_DSYM_FOLDER_PATH}/${DWARF_DSYM_FILE_NAME}/Contents/Resources/DWARF/${TARGET_NAME}
          - $(SRCROOT)/$(BUILT_PRODUCTS_DIR)/$(INFOPLIST_PATH)
```

ビルド後に走らせるスクリプトです。
SwiftLint、FirebaseCrashlyticsなどを走らせてます。

### CI
今回のプロジェクトは[Bitrise](https://go.bitrise.io/bitrise-japan?gclid=EAIaIQobChMIt_y46bni6QIVx6iWCh3WkAD-EAAYASAAEgJhyfD_BwE)を採用しており、環境ごとにdeploygateアップロード、AppStoreConnectアップロードをしています。

<img width="1831" alt="スクリーンショット 2020-06-02 15.02.27.png" src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/369939/1cdd7764-7672-bc25-10a2-3b245d9268af.png">

画像の通り、XcodeGenを走らせるコマンドstepを導入するだけで簡単です。
先述でありました、Cocoapodsのインストール前に行うのを忘れずに。

## 結論
導入コスト、学習コストに関しては基本的に[ProjectSpec](https://github.com/yonaskolb/XcodeGen/blob/master/Docs/ProjectSpec.md)を参考にしていれば、大体は大丈夫でした。
ですが、開発途中でどうしても細かいプロジェクト設定を適用する状況がありますのでその際に**一旦Xcodeより設定してみてビルド。ビルド確認してOKそうなら`project.yml`に落とし込む作業**が多々発生し、なかなか骨が折れました。(もっと良い方法があるのかも)

結局そこまでコンフリクトが問題にならないプロジェクトでした。
何よりドキュメントが少ない、導入実績も現時点(*1)でそこまで多くないので、先述の修正コストの方がでかかったです。今回のような規模のプロジェクトで初導入検討ならいらないかなと思いましたw

## 脚注
(*1) 2020年6月現在では