はじめに
久しぶりの投稿。
題名の通り、期間は半年、エンジニアは二人という開発体制のプロジェクトにXcodeGenを導入し、先日プロジェクトをリリースしましたので、その知見を共有します。
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でXcodeGenを導入。
リリース後完成した project.yml
は以下です。(プロジェクト名や長いライブラリ依存記述などは書き換えてますので、ご容赦を)
個別設定ファイルは.xcconfig
に切り分けてたりしました。
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:
# テスト設定は省略
// 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を見ながら参考にすると良いです。
Build Configuration
# BuildConfiguration定義
configs:
Debug: debug
Stg: debug
Release: release
定義は簡単に書けます。
後は、定義名をキーにして、設定を分けて書いたりできます。
個別のプロジェクト設定
targets:
# メインプロジェクト
test:
# メイン設定
settings:
configs:
Debug:
ODE_SIGN_IDENTITY: Apple Development
DEVELOPMENT_TEAM: hogehoge
PROVISIONING_PROFILE_SPECIFIER: test.debug
個別の設定は上記のように書くことができます。
# 別途読み込みxcconfigファイル
configFiles:
Debug: configs/Debug.xcconfig
Stg: configs/Stg.xcconfig
Release: configs/Release.xcconfig
ですが、個別の設定をproject.yml
に埋め込むと煩雑になってしまいました。
そこで configs/{env}.xcconfig
の記述を逃して、管理するようにしました。
共通のプロジェクト設定
# テンプレ設定
settingGroups:
testSettings:
# 設定
testFrameworkSettings:
# 設定
targets:
# メインプロジェクト
test:
settings:
groups: [testSettings]
# EmbbededFramework
TestFramework:
settings:
groups: [testSettings]
settingGroups
で定義すれば、ここのtarget
で用いれるようになるので便利です。
なので、基本的には共通の設定はsettingGroups
で、BuildConfigurationごとの個別設定は.xcconfig
に分けるようにしました。
ソースコード
targets:
# メインプロジェクト
test:
sources:
- test # 参照ディレクトリ
ソースコードはディレクトリ指定さえしておけば、再起的にCompileSourceとして読み込んでくれます。
ビルド時生成ファイルの参照
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やR.swift などのビルド時生成ファイルが存在する場合は、参照先を上記のように加えなければなりません。
さらにBuild Phasesの追加スクリプト、preBuildScripts.outputFiles
でそれぞれ記述する必要があります。
依存ライブラリ、フレームワーク
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
をするルールに切り替えました。
xcodegen:
mint run XcodeGen xcodegen
bundler exec pod install
xcconfigファイル、Pod用のビルドスクリプトを記述すれば、解決できるかもですが、Podの方に依存した方が安全と判断したため、上記のやり方にしました。
Build Phases
preBuildScripts
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
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を採用しており、環境ごとにdeploygateアップロード、AppStoreConnectアップロードをしています。
画像の通り、XcodeGenを走らせるコマンドstepを導入するだけで簡単です。
先述でありました、Cocoapodsのインストール前に行うのを忘れずに。
結論
導入コスト、学習コストに関しては基本的にProjectSpecを参考にしていれば、大体は大丈夫でした。
ですが、開発途中でどうしても細かいプロジェクト設定を適用する状況がありますのでその際に一旦Xcodeより設定してみてビルド。ビルド確認してOKそうならproject.yml
に落とし込む作業が多々発生し、なかなか骨が折れました。(もっと良い方法があるのかも)
結局そこまでコンフリクトが問題にならないプロジェクトでした。
何よりドキュメントが少ない、導入実績も現時点(*1)でそこまで多くないので、先述の修正コストの方がでかかったです。今回のような規模のプロジェクトで初導入検討ならいらないかなと思いましたw
脚注
(*1) 2020年6月現在では