はじめに
もともと気になっており、先日の勉強会でXcodeGenのLTを聞いて、触ってみようと思いました。
触ってみた結果、業務でも導入したいと考えています。
「プロジェクトファイルがコンフリクトしない」という特徴は、複数人による開発では非常に大きなメリットになるためです。
「XcodeGen」とは?
Xcodeプロジェクトファイル( .xcodeproj
)を定義ファイル( project.yml
)から生成する、Swift製のコマンドラインツールです。
YAMLでプロジェクトの設定を書くことで、プロジェクトファイルが自動生成されます。
そのため、プロジェクトファイルを自分たちで編集する必要がなくなります。
XcodeGenが行うことは「プロジェクトファイルを自動生成する」に尽きる ので、それがわかるとメリットやこの先の説明が理解しやすいです。
用語解説
本記事では用語を以下のように定義します。
- プロジェクトファイル
.xcodeproj
ファイルを指す - ワークスペース
.xcworkspace
ファイルを指す
CocoaPodsの使用時に必要となる - 定義ファイル
project.yml
を指す
公式では「project spec」と呼んでいる
XcodeGenのメリット
XcodeGenの導入によるメリットを紹介します。
プロジェクトファイルがコンフリクトしなくなる
一番のメリットといっても過言ではないと思います。
複数人によるiOSアプリ開発で誰もが経験する「ファイル追加によるプロジェクトファイルのコンフリクト」が発生しなくなります。
理由は、定義ファイルの source:
でフォルダを指定すると、フォルダ内のファイルを自動的にプロジェクトへ追加するためです。
targets:
Foo:
sources:
- Foo # 「Foo」フォルダ内のファイルが自動的に追加される
つまり、ファイルを追加・リネーム・削除するのみでは定義ファイルを変更する必要がありません。
プロジェクトツリーが自動ソートされる
実際のファイルパスから自動的にプロジェクトツリーを生成します。
その際、ファイル名順に自動でソートされます。
プロジェクトの設定を簡単に変更できる
ターゲットやCarthageで管理しているライブラリを簡単に追加できます。
定義ファイルはテキストなので、その他の設定も簡単に変更できます。
XcodeGenのデメリット
学習コストがかかる
ライブラリ全般にいえるデメリットですが、XcodeGenの概要や使い方のキャッチアップにコストが掛かります。
本記事で多少コストが下がれば幸いです。
プロジェクトファイルの生成に手間がかかる
メリットで「ファイル変更時に定義ファイルの修正不要」と説明しましたが、プロジェクトファイルを生成し直す必要はあります。
CocoaPodsを使っていると、さらにプロジェクトファイルからワークスペースを生成しなければいけません。
私はMakefileを作って手間を削減しています。
https://qiita.com/uhooi/items/16a870eaae24b46103fb#プロジェクトファイルの生成
XcodeGenを導入すべきケース
XcodeGenのメリットやデメリットがわかったところで、どういうケースでXcodeGenを導入すべきなのか紹介します。
あくまで私の意見なので、参考程度に捉えてください。
プロジェクトファイルのコンフリクトに悩んでいる
大人数による開発などでプロジェクトファイルのコンフリクトに悩んでいる場合、導入を検討する価値があると思います。
新規開発時
定義ファイルの作成経験があればコピペして使い回せるため、新規開発時のプロジェクト作成時間を短縮できます。
また、新規開発時はファイルの追加を頻繁に行うため、コンフリクトを防げます。
環境
- OS:macOS Mojave 10.14.6
- Swift:5.1.3
- Xcode:11.3.1 (11C504)
- XcodeGen:2.11.0
セットアップ
XcodeGenのインストール
Mintからインストールします。
+ yonaskolb/xcodegen@2.11.0
$ mint bootstrap
手動でインストールするには、公式ドキュメントをご参照ください。
https://github.com/yonaskolb/XcodeGen#installing
XcodeGenのセットアップ自体は、インストールのみで完了します。
定義ファイルの作成
定義ファイルをプロジェクトのルートフォルダに新規作成します。
本記事では「Foo」をプロジェクト名およびプロジェクトのルートフォルダとします。
$ cd Foo
$ touch project.yml
定義ファイルの説明
ここからは定義ファイルに最低限必要な記述をひとつずつ説明します。
設定できる全ての内容までは説明し切れないので、詳細は公式ドキュメントをご参照ください。
https://github.com/yonaskolb/XcodeGen/blob/master/Docs/ProjectSpec.md
name
プロジェクト名を指定します。
name: Foo
options
プロジェクトのオプションを設定します。
bundleIdPrefix
Product bundle identifier(以下「バンドルID」と呼ぶ)のプリフィクスを指定します。
本記事では「com.theuhooi」をプリフィクスとします。
options:
bundleIdPrefix: com.theuhooi
プリフィクスを指定すると、各ターゲットのバンドルIDが自動で「{プリフィクス}.{ターゲット名}」に設定されます。
そのため、ターゲットごとに PRODUCT_BUNDLE_IDENTIFIER
を指定する必要がなくなります。
options:
bundleIdPrefix: com.theuhooi
targets:
Foo:
settings:
base:
PRODUCT_BUNDLE_IDENTIFIER: com.theuhooi.Foo # 不要
FooTests:
settings:
base:
PRODUCT_BUNDLE_IDENTIFIER: com.theuhooi.FooTests # 不要
FooUITests:
settings:
base:
PRODUCT_BUNDLE_IDENTIFIER: com.theuhooi.FooUITests # 不要
マルチプラットフォームなどでターゲット名とバンドルIDを合わせない場合は、明示的に指定してください。
developmentLanguage
LocalizationsのDevelopment Languageを指定します。
デフォルトは英語で、日本語にする場合は ja
を指定します。
options:
developmentLanguage: ja
project.pbxproj
上のキーは developmentRegion
なので、指定を間違えないようご注意ください。
deploymentTarget
デプロイメントターゲットをプラットフォームごとに指定します。
iOSアプリ開発の場合、「iOS」のみ指定すればOKです。
options:
deploymentTarget:
iOS: 13.0
xcodeVersion
Xcodeのバージョンを指定します。
options:
xcodeVersion: "11.3.1" # 変わらない?
指定してもプロジェクトファイルに反映されなかったので、何か条件があるか、私が意味を理解していないかのどちらかです。
コメント で教えていただいたのですが、ダブルクォーテーションで括らないと不正な値として扱われ、デフォルト値が使われるとのことです。
findCarthageFrameworks
Carthageで管理しているフレームワークのインストール時、依存しているフレームワークも自動でインストールするかどうかの設定です。
options:
findCarthageFrameworks: true
フレームワークのつくりや、私の使い方の問題かもしれませんが、依存しているのに自動でインストールされなかったり、余計なフレームワークまでインストール(テストターゲットのみで使うのに製品ターゲットにもインストール)されたりしたため、私は指定していません。
carthageExecutablePath
Carthageの実行パスを指定します。
CarthageをMintで管理している場合、以下のように指定する必要があります。
options:
carthageExecutablePath: mint run Carthage/Carthage carthage
デフォルトが carthage
なので、CarthageをHomebrewでインストールした場合は不要です。
settings
全ターゲット共通で使うBuild Settingsを設定します。
個別に設定する場合はターゲット内の settings
に記述します。
私はバージョンとビルド番号、Development Team、Debug Information Formatを指定しています。
Debug Information FormatはBuild Configurations(以下「ビルド構成」と呼ぶ)が Debug
(以下「デバッグビルド」と呼ぶ)時のみ指定しています。
settings:
base:
MARKETING_VERSION: 1.0.0
CURRENT_PROJECT_VERSION: 1
DEVELOPMENT_TEAM: XXXXXXXXXX
config:
debug:
DEBUG_INFORMATION_FORMAT: "dwarf-with-dsym"
MARKETING_VERSION
と CURRENT_PROJECT_VERSION
をそれぞれバージョンとビルド番号に紐付けるには、 Info.plist
の修正が必要です。
(TARGETS > {製品ターゲット} > General の Version
, Build
を手動で変更すると、自動で修正されます)
<key>CFBundleShortVersionString</key>
- <string>1.0</string>
+ <string>$(MARKETING_VERSION)</string>
<key>CFBundleVersion</key>
- <string>1</string>
+ <string>$(CURRENT_PROJECT_VERSION)</string>
バージョンとビルド番号を全ターゲットで共通にしているのは、製品とApp Extensionのバージョンを合わせないと警告が発生するためです。
packages
Swift Package Manager(以下「SwiftPM」と呼ぶ)で管理するライブラリを指定します。
SwiftPMはCocoaPodsの Podfile
のようにライブラリのバージョンを管理するファイルがなく、本来はXcode上で操作してプロジェクトファイルで管理します。
XcodeGenを使うとGUIで管理しなくて済みます。
構造は以下の通りです。
packages:
{パッケージ名}:
url: {GitHubなどのURL}
version: {バージョン}
バージョンを指定する方法は他にもありますが、他の開発者とバージョンを揃えたいので version
でバージョンを指定しています。
私は以下のように指定しています。
packages:
Rswift:
url: https://github.com/mac-cain13/R.swift.Library
version: 5.3.0
Gedatsu:
url: https://github.com/bannzai/Gedatsu
version: 1.2.0
SwiftPrettyPrint:
url: https://github.com/YusukeHosonuma/SwiftPrettyPrint
version: 1.0.0
targets
各ターゲットの設定です。
構造は以下の通りです。
targets:
{ターゲット名}:
{設定…}
type
ターゲットのタイプを指定します。
targets:
Foo:
type: application
FooTests:
type: bundle.unit-test
FooUITests:
type: bundle.ui-testing
よく指定するタイプの一覧です。
type | 説明 |
---|---|
application | 製品ターゲット |
bundle.unit-test | 単体テストターゲット |
bundle.ui-testing | UIテストターゲット |
platform
対象とするプラットフォームを指定します。
targets:
Foo:
platform: iOS
配列で指定するとターゲット名の末尾にプラットフォーム名が付くため、対象プラットフォームが1つのみで bundleIdPrefix
を使う場合は配列にしない方がいいです。
targets:
Foo:
platform: [iOS] # ターゲット名が「Foo-iOS」となる
sources
ターゲットに含むファイルまたはフォルダを指定します。
基本的にはターゲットと同名のフォルダのみ指定すればOKです。
targets:
Foo:
sources:
- Foo
FooTests:
sources:
- FooTests
FooUITests:
sources:
- FooUITests
注意点として、 すでに存在するファイルを元にプロジェクトツリーを生成する ため、プロジェクトファイルの生成時に存在しないファイル(R.swiftやMockoloなどで自動生成するファイル)が追加されません。
クックパッドさんのブログの通りに指定することでこの問題を回避できます。
https://techlife.cookpad.com/entry/2019/04/26/110000
targets:
Foo:
sources:
- Foo
# Mockoloで生成するファイル
- path: "Foo/Generated/MockResults.swift"
optional: true
type: file
# R.swiftで生成するファイル
- path: "Foo/Generated/R.generated.swift"
optional: true
type: file
settings
ターゲットのBuild Settingsを設定します。
私は Info.plist
のパス、 {プロジェクト名}.entitlements
のパス、開発言語、Other Linker Flags、Other Swift Flagsを指定しています。
targets:
Foo:
settings:
base:
INFOPLIST_FILE: Foo/Info.plist
CODE_SIGN_ENTITLEMENTS: Foo/Foo.entitlements
DEVELOPMENT_LANGUAGE: jp
OTHER_LDFLAGS: $(inherited) $(OTHER_LDFLAGS) -ObjC
configs:
debug:
OTHER_SWIFT_FLAGS: $(inherited) -Xfrontend -warn-long-expression-type-checking=500 -Xfrontend -warn-long-function-bodies=500
FooTests:
settings:
base:
INFOPLIST_FILE: FooTests/Info.plist
FooUITests:
settings:
base:
INFOPLIST_FILE: FooUITests/Info.plist
XcodeGenでは Info.plist
の生成も行えますが、コンフリクトしにくいので私は既存のファイルを読み込むのみに留めています。
CODE_SIGN_ENTITLEMENTS
について、NFCタグの読み取りなど、特別に許可している場合のみ設定が必要です。
DEVELOPMENT_LANGUAGE
について、 jp
を指定しないとiOSの使用言語が日本語でも UIActivityViewController
が日本語表記にならなかったので指定しています。
OTHER_LDFLAGS
について、Firebase Analyticsで必要です。
OTHER_SWIFT_FLAGS
について、型推論と関数のビルドに時間がかかる場合に警告を発生するようにしています。
dependencies
各ターゲットが依存しているフレームワークなどを指定します。
Carthageで管理しているフレームワークを指定する場合、 - carthage: {フレームワーク名}
と記述するのみで済むのが便利です。
同様にSwiftPMで管理しているライブラリは - package: {ライブラリ名}
と記述します。
私の場合、製品ターゲットはCarthageやSwiftPMで管理しているフレームワーク、テストターゲットは製品ターゲットのみを指定しています。
targets:
Foo:
dependencies:
- package: Rswift
- package: Gedatsu
- package: SwiftPrettyPrint
- carthage: Alamofire
FooTests:
dependencies:
- target: Foo
FooUITests:
dependencies:
- target: Foo
preBuildScripts
Build Phasesの最初に実行するスクリプトを指定します。
私の場合、製品ターゲットでR.swiftとMockoloのファイル生成スクリプトを記述しています。
targets:
Foo:
preBuildScripts:
# R.swiftでリソースファイルの生成
- path: ./Scripts/XcodeGen/rswift.sh
name: Generate Resources with R.swift
inputFiles:
- $TEMP_DIR/rswift-lastrun
outputFiles:
- $SRCROOT/$TARGET_NAME/Generated/R.generated.swift
# Mockoloでモックの生成
- path: ./Scripts/XcodeGen/mockolo.sh
name: Generate Mocks with Mockolo
outputFiles:
- $SRCROOT/$TARGET_NAME/Generated/MockResults.swift
script
で直接スクリプトを記述できますが、定義ファイルが長くなるため、私はルートフォルダに Scripts/XcodeGen
フォルダを作成し、そこにXcodeGenで使うスクリプトをまとめて格納しています。
その場合、 path
でスクリプトのパスを指定します。
if which mint >/dev/null; then
xcrun --sdk macosx mint run R.swift rswift generate "$SRCROOT/$TARGET_NAME/Generated/R.generated.swift"
else
echo "warning: Mint not installed, download from https://github.com/yonaskolb/Mint"
fi
if which mint >/dev/null; then
rm -f $SRCROOT/$TARGET_NAME/Generated/MockResults.swift
xcrun --sdk macosx mint run uber/mockolo mockolo --sourcedirs $SRCROOT/$TARGET_NAME --destination $SRCROOT/$TARGET_NAME/Generated/MockResults.swift --mock-final
else
echo "warning: Mint not installed, download from https://github.com/yonaskolb/Mint"
fi
postCompileScripts
Build PhasesでCompile Sourcesの直後に実行するスクリプトを指定します。
私の場合、製品ターゲットでSwiftLintとSpellCheckerのチェックスクリプトを記述しています。
targets:
Foo:
postCompileScripts:
# SwiftLintで静的解析と自動修正
- path: ./Scripts/XcodeGen/swiftlint.sh
name: Run SwiftLint
# SpellCheckerでスペルチェック
- path: ./Scripts/XcodeGen/spellchecker.sh
name: Run SpellChecker
if which mint >/dev/null; then
xcrun --sdk macosx mint run swiftlint swiftlint autocorrect --format
xcrun --sdk macosx mint run swiftlint swiftlint
else
echo "warning: Mint not installed, download from https://github.com/yonaskolb/Mint"
fi
if ! which mint >/dev/null; then
echo "warning: Mint not installed, download from https://github.com/yonaskolb/Mint"
exit 0
fi
git_path=/usr/local/bin/git
files=$($git_path diff --diff-filter=d --name-only -- "*.swift" "*.h" "*.m")
if (test -z $files) || (test ${#files[@]} -eq 0); then
echo "no files changed."
exit 0
fi
options=""
for file in $files
do
options="$options $SRCROOT/$file"
done
xcrun --sdk macosx mint run SpellChecker SpellChecker --yml $SRCROOT/spell-checker.yml -- $options
schemes
スキームの設定です。
私はデフォルト(製品ターゲットと同名)のスキームのみ設定しており、 説明に疲れてきた 定義ファイルの全体像とXcode上のスキーム編集画面を見比べた方がわかりやすいので、本記事で詳細な解説はしません。
schemes:
Foo:
build:
targets:
Foo: all
run:
config: Debug
test:
config: Debug
gatherCoverageData: true
coverageTargets:
- Foo
targets:
- name: FooTests
parallelizable: true
randomExecutionOrder: true
- name: FooUITests
parallelizable: true
randomExecutionOrder: true
profile:
config: Release
analyze:
config: Debug
archive:
config: Release
定義ファイルの全体像
以上を踏まえ、私の定義ファイルの全体像を載せます。
name: Foo
options:
bundleIdPrefix: com.theuhooi
deploymentTarget:
iOS: 13.0
developmentLanguage: ja
xcodeVersion: "11.3.1"
carthageExecutablePath: mint run Carthage/Carthage carthage
settings:
base:
MARKETING_VERSION: 1.0.0
CURRENT_PROJECT_VERSION: 1
DEVELOPMENT_TEAM: XXXXXXXXXX
configs:
debug:
DEBUG_INFORMATION_FORMAT: "dwarf-with-dsym"
packages:
Rswift:
url: https://github.com/mac-cain13/R.swift.Library
version: 5.3.0
Gedatsu:
url: https://github.com/bannzai/Gedatsu
version: 1.2.0
SwiftPrettyPrint:
url: https://github.com/YusukeHosonuma/SwiftPrettyPrint
version: 1.0.0
targets:
Foo:
type: application
platform: iOS
sources:
- Foo
- path: "Foo/Generated/MockResults.swift"
optional: true
type: file
- path: "Foo/Generated/R.generated.swift"
optional: true
type: file
settings:
base:
INFOPLIST_FILE: Foo/Info.plist
CODE_SIGN_ENTITLEMENTS: Foo/Foo.entitlements
DEVELOPMENT_LANGUAGE: jp
OTHER_LDFLAGS: $(inherited) $(OTHER_LDFLAGS) -ObjC
configs:
debug:
OTHER_SWIFT_FLAGS: $(inherited) -Xfrontend -warn-long-expression-type-checking=500 -Xfrontend -warn-long-function-bodies=500
dependencies:
- package: Rswift
- package: Gedatsu
- package: SwiftPrettyPrint
- carthage: Alamofire
preBuildScripts:
- path: ./Scripts/XcodeGen/rswift.sh
name: Generate Resources with R.swift
inputFiles:
- $TEMP_DIR/rswift-lastrun
outputFiles:
- $SRCROOT/$TARGET_NAME/Generated/R.generated.swift
- path: ./Scripts/XcodeGen/mockolo.sh
name: Generate Mocks with Mockolo
outputFiles:
- $SRCROOT/$TARGET_NAME/Generated/MockResults.swift
postCompileScripts:
- path: ./Scripts/XcodeGen/swiftlint.sh
name: Run SwiftLint
- path: ./Scripts/XcodeGen/spellchecker.sh
name: Run SpellChecker
FooTests:
type: bundle.unit-test
platform: iOS
sources:
- FooTests
settings:
base:
INFOPLIST_FILE: FooTests/Info.plist
dependencies:
- target: Foo
FooUITests:
type: bundle.ui-testing
platform: iOS
sources:
- FooUITests
settings:
base:
INFOPLIST_FILE: FooUITests/Info.plist
dependencies:
- target: Foo
schemes:
Foo:
build:
targets:
Foo: all
run:
config: Debug
test:
config: Debug
gatherCoverageData: true
coverageTargets:
- Foo
targets:
- name: FooTests
parallelizable: true
randomExecutionOrder: true
- name: FooUITests
parallelizable: true
randomExecutionOrder: true
profile:
config: Release
analyze:
config: Debug
archive:
config: Release
GitHub Gistにも上げています。
https://gist.github.com/uhooi/730c7aa286e730b392796a159e4d75f5
バージョン管理から無視する
プロジェクトファイルのコンフリクトを防ぐため、生成されたプロジェクトファイルとワークスペースをバージョン管理の対象外にします。
Gitを使っている場合、以下を「.gitignore」に追加するのみでOKです。
+ *.xcodeproj
+ *.xcworkspace
操作方法
プロジェクトファイルの生成
定義ファイルが存在するフォルダへ移動し、 xcodegen generate
を実行するのみです。
Mintで管理している場合は、先頭に mint run xcodegen
を付けます。
$ mint run xcodegen xcodegen generate
私はMakefileを作成し、 make generate-xcodeproj
を実行すると「プロジェクトファイルの生成→CocoaPods用ワークスペースの生成→ワークスペースをXcodeで開く」を一括で行うようにしています。
PRODUCT_NAME := Foo
.PHONY: generate-xcodeproj
generate-xcodeproj:
mint run xcodegen xcodegen generate
bundle exec pod install
make open
.PHONY: open
open:
open ./${PRODUCT_NAME}.xcworkspace
注意:Xcode 11.5以上でMintを使うにはワークアラウンドが必要
2020/08/03現在、Xcode 11.5以上でMintをRun Script Phaseで使う場合、ワークアラウンドが必要です。
Mintの記事に詳細を記載したのでご参照ください。
https://qiita.com/uhooi/items/6a41a623b13f6ef4ddf0#注意xcode-115以上のrun-script-phaseで使うにはワークアラウンドが必要
おわりに
最後まで読み、XcodeGenを使ってみようと思っていただければ幸いです☺️
まだ業務で使っておらず、個人でも使い倒していないため、問題などありましたらご指摘くださると嬉しいです。
参考リンク
- yonaskolb/XcodeGen: A Swift command line tool for generating your Xcode project
- XcodeGen超入門 - Speaker Deck
- XcodeGenによる新時代のiOSプロジェクト管理 - クックパッド開発者ブログ
-
2019年上半期のiOSアプリ開発振り返り – 株式会社キュリオシティソフトウェア
本記事よりXcodeGenのメリデメが詳しく書かれています - XcodeGen Example
- ShareExtension/project.yml at master · YutoMizutani/ShareExtension
- UhooiPicBook/project.yml at develop · uhooi/UhooiPicBook