- はじめに
- この記事で解決すること
- 背景と課題
- multi-xcodeproj + xcworkspace 構成
- 実際の移行ステップ
- ステップ1: xcworkspaceの作成
- ステップ2: Swift Packageの登録
- ステップ3: UIモジュールのリネーム
- ステップ4: iPadプロジェクトのディレクトリ化
- ステップ5: pbxprojのターゲット名変更
- ステップ6: ビルドの確認
- ステップ7: Staging / Local の複製
- ステップ8: Schemeの設定
- ステップ9: xcconfigの共通化
- ステップ10: GoogleService-Info.plistの管理
- ステップ11: アプリアイコンの分割
- ステップ12: UITests専用プロジェクトの分離
- ステップ13: CI/CDの修正
- ステップ14: 動作確認
- 今後の展望
- さいごに
はじめに
こんにちは、@marcy731 です。
STORES レジ のモバイルチームのマネージャー兼iOSエンジニアをしています。
STORES レジでは、以前からSwift Package中心の構成を採用していましたが、Production / Staging / Localといった3つの環境ごとのBuild Configurationに依存した、古いxcodeprojベースの管理が残っていました。
そこから xcworkspaceを軸にした複数xcodeproj管理へ置き換える 取り組みを進めました。
iOSアプリのプロジェクト構成を考えるときに欠かせないのが「xcodeproj」と「xcworkspace」です。
名前は聞いたことがあっても、それぞれの役割の違いが意外とピンとこない方もいるかもしれません。
ここで簡単に整理しておきます。
xcodeproj とは?
- Xcodeで作成される基本的な「プロジェクトファイル」
- アプリ本体のターゲットや、ビルドに必要な設定、ファイルの構成を管理
- 1つのアプリをビルドするなら基本的にこの xcodeproj で十分
つまり「アプリの箱」とイメージするとわかりやすいです。
xcworkspace とは?
- 複数の xcodeproj や Swift Package などを まとめて管理 するための「ワークスペースファイル」
- 依存するライブラリや別モジュールなどを、一つの画面で横断的に編集・管理できる
- 規模が大きくなり複数のモジュールを組み合わせる場合には必須
簡単にいえば「たくさんの箱(xcodeproj)をまとめておける大きな棚」とイメージすると良いです。
この記事で解決すること
近年のiOS開発では、
- Swift Packageを活用したモジュール分割
- チーム全体の依存管理の一元化
- ビルドの安定性向上
といったニーズが非常に高まっています。
一方で、世の中で多く紹介されているのは「新規にSwift Package中心のプロジェクトを立ち上げる」といったケースがほとんどで、
既存の単一のxcodeprojベースのプロジェクトを、段階的にSwift Package中心+xcworkspace構成へ移行するにはどうすればよいのか
についての具体的な事例はあまり多くありません。
本記事では、既存のiOSアプリを Swift Package中心の構成に移行し、xcworkspaceを軸にした複数xcodeproj管理へ置き換える という取り組みについて、実践的なノウハウを共有します。
なお STORES レジ では Swift Package中心の構成 にはなっていたため、おもに xcworkspaceを軸にした複数xcodeproj管理へ置き換える ことが焦点になります。
この記事は、
- 既存のxcodeprojで運用している
- StagingやQA向けのビルドで苦労している
という方に向けて、実際に私たちが行った
- 既存プロジェクトをmulti-xcodeproj + xcworkspace構成へ移行する手順
- その中で直面した課題とハマりポイント
をすべて公開し、参考にしていただくことを目的としています。
背景と課題
STOERS レジ の当初の構成
私たちの STOERS レジ アプリは、
- iPad専用
- 店舗向けの注文管理・決済・在庫管理などを一体化した POSレジ システム
として2019年から継続して開発されてきました。
技術スタックとしては
- Swift / SwiftUI
- CocoaPods依存
からスタートし、その後 Swift Package をモジュールとして段階的に取り込むようになりました。
ただし、その過程でも メインのアプリはあくまで1つのxcodeproj にまとめられており、 以下のような構成になっていました。
Build Configuration
Debug Staging Release
Debug
- ローカル開発環境
- デバッグ機能ON
Staging
- ステージング開発環境
- デバッグ機能ON
Release
- 本番環境 (AppStore配布)
- デバッグ機能OFF
といった目的で3つのBuild Configurationを長らく利用してきました。
Stagingで起こっていた問題
一見この構成は便利に思えますが、Swift Packageを積極的に使い始めたタイミングで
StagingというBuild Configurationが問題を引き起こす
ようになりました。
Swift Packageの制約
AppleのSwift Package Managerは
debug
release
の2種類しかBuild Configurationを持ちません。
Xcode側では
- Debug
- Release
- Staging
- QA
など自由にBuild Configuration名を付けられますが
Swift Packageにとっては
- 名前に「Debug」または「Development」が入っていれば debug
- それ以外は release
としてしか扱えないという仕様があります。
具体的に何が起きるのか?
Xcodeのスキームで「Staging」を選ぶ と、Swift Package側は release
と認識してビルドしてしまいます。
その結果
#if DEBUG showDebugMenu() #endif
のように書いていたデバッグ用コードが
Staging環境では完全に取り除かれる
という問題に直面しました。
SwiftUI Previewの問題
さらに
- SwiftUI Preview
は DEBUG
が有効であることを前提に動作します。
Stagingでビルドすると
- SwiftUI Previewが起動しない
- Canvasが真っ白
という開発者体験の低下が起きていました。
ビルド速度の問題
Swift Packageは
debug
: 最適化なし-Onone
release
: 最大最適化-Owholemodule
というビルドフラグでコンパイルされます。
Stagingがrelease
として認識されることで
- 最適化がフルでかかる
- モジュールキャッシュが効かない
- ビルド時間が大幅に増える
といった負担も出ていました。
その他の問題
Scheme ごとに GoogleService-Info.plist やアプリアイコンを切り替えるために、以下の方法で対応していました:
- xcassets の上書き によりアイコンなどのリソースを差し替える
- Build Phase でのスクリプト処理 により GoogleService-Info.plist などの設定ファイルを切り替える
今回の multi-xcodeproj + xcworkspace 構成にすることでこれらの設定も xcodeproj ごとに設定できるので複雑な切り替え処理が不要になります。
multi-xcodeproj + xcworkspace 構成
考え方
multi-xcodeproj + xcworkspace 構成の核は
- xcworkspace をハブに置く
- 環境ごとに xcodeproj を切り出す
- 共通のモジュールはSwift Packageで管理する
という3点です。
この組み合わせにより
- 各環境(本番/ステージング/ローカル)で完全に独立した設定が可能
- Swift Packageの
debug/release
の挙動を正しく統一 - GoogleService-Info.plist やアイコンもプロジェクト単位で管理 が一気に解決できます。
この辺りは @d_date さんの Swift Package中心のプロジェクト構成とその実践 に詳しく書いてありますで、まだみたことのない方がいましたら参照してください。
構成の全体像
新しい構成のフォルダ構成イメージは以下のようになります。
├── Sample.xcworkspace ├── iPad/ │ └── iPad.xcodeproj ├── iPad-Staging/ │ └── iPad-Staging.xcodeproj ├── iPad-Local/ │ └── iPad-Local.xcodeproj ├── iPad-UITests/ │ └── iPad-UITests.xcodeproj ├── iPadUI ├── Repository ├── UseCase └── ...
- iPad.xcodeproj … Production(本番環境)用
- iPad-Staging.xcodeproj … QA(ステージング)用
- iPad-Local.xcodeproj … ローカル開発用
- iPad-UITests.xcodeproj … テスト専用
- Repository / UseCase / UI … 共通のモジュール
このように「環境=プロジェクト」とすることで
- プロジェクトごとに Info.plist を独立管理
- プロジェクトごとにアイコン・バンドルID・証明書を分ける
といった運用がとてもシンプルになります。
xcworkspace の役割
xcworkspaceは
- すべてのxcodeproj
- すべてのSwiftPackage
を束ねる「ハブ」です。
開発者にとっては Sample.xcworkspaceを開けば全ての環境を横断的に見られる という状態になり、 個別のプロジェクトを開き直す必要がなくなります。
また
- モジュール依存
- テストターゲット
の管理もworkspace上で一括で可視化できます。
複数xcodeprojに分けるメリット
1. Swift PackageのBuild Configuration問題を回避
Build Configurationは
- Debug
- Release
だけに揃え、 それぞれのプロジェクトに
- iPad(Release)
- iPad-Staging(Debug)
- iPad-Local(Debug)
を割り当てます。
これにより
- Swift Package側で常に正しい
#if DEBUG
が効く - Previewも安定
- ビルド速度も最適化
されます。
2. 環境依存ファイルの安全な管理
GoogleService-Info.plistや アプリアイコンなど環境依存のファイルは プロジェクトごとに配置できるので、 Build Phaseでコピーする必要が一切なくなります。
さらに
- Info.plist
- entitlements
などの証明書管理も 環境ごとに安全に分けられるようになりました。
3. iPhone版や新規デバイスへの展開が楽に
iPadだけでなく
- iPhone
- Watch
- Vision Pro
といった新しいデバイス向けに展開する際も、同じxcworkspaceに
iPhone/ └─ iPhone.xcodeproj
を追加すれば共通のモジュールを活かせます。
モジュール再利用性を最大化しつつ、環境単位での管理も明確にできる という柔軟性が、この構成の大きな強みです。
xcconfigの設計
プロジェクトが増えると
- Bundle ID
- バージョン
- 表示名
のような設定がバラバラになりがちです。
そこで
Root/ └─ Base-iPad.xcconfig
に共通設定を書き 各プロジェクトでは
#include? "Root/Base-iPad.xcconfig"
で読み込む形にしました。
これにより
- バージョンアップ
- 証明書変更
が 1ファイルで管理可能 になり、 ヒューマンエラーのリスクを大幅に減らしています。
テストターゲットの分割
さらに
- iPad-UITests.xcodeproj
を切り出し、E2EやSnapshotのテストだけ独立管理できる構造にしました。
これにより
- 本番用のアプリにテスト専用の依存を載せない
- CI/CDのジョブでテストだけ分けてビルドできる
といった安全性・効率性を向上させています。
実際の移行ステップ
では実際に私たちがどのように
- xcworkspaceを新規で構築し
- 既存のxcodeprojを分割していったのか
を、なるべく具体的に、サンプルコードやコマンドを交えて解説します。
ステップ1: xcworkspaceの作成
まず最初に
Sample.xcworkspace
を作成しました。
(説明上、本記事では Sample.xcworkspace として進めさせてください。)
Xcode上で
「File > New > Workspace」
から新規作成するのがもっとも手軽です。
ステップ2: Swift Packageの登録
Sample.xcworkspaceを開き File > Add Files to Workspace で
- Repository
- UseCase
- UI
- Analytics
などの既存の Swift Package をドラッグ&ドロップして登録します。
これにより
- Swift Packageの依存
- 各ターゲットのリンク
を workspace単位 で一元管理できるようになります。
参考までに Sample.xcworkspace/contents.xcworkspacedata は以下のようになります。
<?xml version="1.0" encoding="UTF-8"?> <Workspace version = "1.0"> <FileRef location = "group:Base-iPad.xcconfig"> </FileRef> <FileRef location = "group:Analytics"> </FileRef> <FileRef location = "group:Hardware"> </FileRef> <FileRef location = "group:Repository"> </FileRef> <FileRef location = "group:UseCase"> </FileRef> <FileRef location = "group:iUI"> </FileRef> <FileRef location = "group:Sample/Sample.xcodeproj"> </FileRef> </Workspace>
ステップ3: UIモジュールのリネーム
元々
import UI
としていたモジュールは他デバイス展開を見据えて
import iPadUI
にリネームしました。
手順としては
- Swift Packageの
Package.swift
で iPadUI に変更
.target(name: "iPadUI", ...)
- ディレクトリも以下に変更
- Sources/UI/ + Sources/iPadUI/
- 既存のimport文を一括置換
find . -type f -name "*.swift" -exec sed -i '' 's/import UI/import iPadUI/g' {} \;
これで可搬性の高いモジュール名に統一できました。
ステップ4: iPadプロジェクトのディレクトリ化
Sample.xcodeproj を
iPad/iPad.xcodeproj
として整理しました。
mkdir iPad mv Sample.xcodeproj iPad/iPad.xcodeproj
さらに AppDelegate
や Resources
などのアプリ本体のコードも、iPad/配下に移動して構造を整理しました。
ステップ5: pbxprojのターゲット名変更
ここが一番大変でしたが
project.pbxproj
内に含まれる
- Sample
- SampleUITests
- SampleSnapshotTests
といったターゲット名を
- iPad
- iPadUITests
- iPadSnapshotTests
に置換しました。
例えばターミナルで
sed -i '' 's/Sample/iPad/g' iPad/iPad.xcodeproj/project.pbxproj sed -i '' 's/SampleUITests/iPadUITests/g' iPad/iPad.xcodeproj/project.pbxproj sed -i '' 's/SampleSnapshotTests/iPadSnapshotTests/g' iPad/iPad.xcodeproj/project.pbxproj
のように一括置換しています。
注意:
pbxprojはXcodeで差分を見ながら最後に必ず手で確認することを強くおすすめします。
ステップ6: ビルドの確認
ここまでの状態で
- iPad.xcodeproj
- Swift Package
のビルドが通るか一度確認します。
この時点でありがちなミスは
- Info.plistのパスがずれている
- xcassetsのパスが変わっている
- modulemapの参照が切れる
といったものです。
エラーを一つずつ地道に修正しました。
ステップ7: Staging / Local の複製
Production用(iPad.xcodeproj)ができたら、
これを
cp -R iPad iPad-Staging cp -R iPad iPad-Local
で複製しました。
その後
mv iPad-Staging/iPad.xcodeproj iPad-Staging/iPad-Staging.xcodeproj mv iPad-Local/iPad.xcodeproj iPad-Local/iPad-Local.xcodeproj
でプロジェクト名を変え
さらにpbxprojを
sed -i '' 's/iPad/iPad-Staging/g' iPad-Staging/iPad-Staging.xcodeproj/project.pbxproj sed -i '' 's/iPad/iPad-Local/g' iPad-Local/iPad-Local.xcodeproj/project.pbxproj
で置換しました。
ステップ8: Schemeの設定
- iPad.xcodeproj → Release
- iPad-Staging.xcodeproj → Debug
- iPad-Local.xcodeproj → Debug
とし、Xcodeの Product > Scheme > Manage Schemes
から
- ビルド対象
- 実行対象
をそれぞれ整理しました。
ステップ9: xcconfigの共通化
各プロジェクトで
iPad/Supporting Files/Debug.xcconfig iPad/Supporting Files/Release.xcconfig
などを作り 共通設定は
Root/Base-iPad.xcconfig
にまとめます。
Base-iPad.xcconfig例
VERSION_NUMBER = 1.0.0 MARKETING_VERSION = 1.0 DISPLAY_NAME = STORES レジ SWIFT_VERSION = 6.0 CODE_SIGN_STYLE = Automatic
個別の環境ごとの設定を最小限にしてメンテナンスを簡単にしました。
ステップ10: GoogleService-Info.plistの管理
Build Phaseで
cp GoogleService-Info-Staging.plist GoogleService-Info.plist
のように上書きしていた方式を廃止し、各プロジェクトの
- iPad
- iPad-Staging
- iPad-Local
それぞれに正しいGoogleService-Info.plistを配置するようにしました。
これで Buildスクリプトが不要になり、管理ミスがなくなる 効果が非常に大きかったです。
ステップ11: アプリアイコンの分割
AppIconも
- iPad
- iPad-Staging
- iPad-Local
で別々に分けて間違って本番アイコンを出さないようにしました。
ステップ12: UITests専用プロジェクトの分離
最後に
iPad-UITests/iPad-UITests.xcodeproj
を新規に作り、UIテストターゲットだけを切り出しました。
- テスト専用のモック
- テスト専用の依存
をiPad-UITests.xcodeprojにまとめ、本番用アプリには含めない構成にしました。
ステップ13: CI/CDの修正
XcodeCloud や GitHub Actions、Fastlaneでも
--project
--scheme
--configuration
のパラメータを環境ごとに切り替えるように修正しました。
- 例: Fastlane
gym( workspace: "Sample.xcworkspace", project: "iPad-Staging/iPad-Staging.xcodeproj", scheme: "iPad-Staging", configuration: "Debug" )
- 例: GitHub Actions
- name: Build Staging run: | xcodebuild \ -workspace Sample.xcworkspace \ -project iPad-Staging/iPad-Staging.xcodeproj \ -scheme iPad-Staging \ -configuration Debug \ build
ステップ14: 動作確認
- 本番
- ステージング
- ローカル
- UIテスト
の各プロジェクトが
- xcworkspaceから問題なくビルドできる
- それぞれの xcodeproj で適切な環境につながる
- Firebaseが正しく切り替わる
- アイコンが正しく表示される
- スナップショットテストも動く
- ハードウェアとの接続が問題なくできる
ことを最終確認して完了です。
- 具体的な作業手順
- コマンド例
- 注意点
まで含めて、移行作業をできるだけ詳細に説明しました。
「移行のリアル」感が伝われば幸いです。
今後の展望
今回の
- xcworkspace+複数xcodeproj構成
- Swift Package中心のモジュール設計
への移行は、単に現行のiPadアプリの問題を解決するだけでなく
将来的なiPhone展開を視野に入れた土台作り という意味でも非常に大きな意味がありました。
現状まだ開発できていない iPhone ver を見据えた際にも、同じxcworkspaceにプロジェクトを追加するだけで
- 共通モジュール
- 共通CI/CD
を活かしながら開発できます。
一方で課題もあります。
- iPadとiPhoneでUI設計の差分が大きくなる
- モジュール依存の最適化がさらに必要になる
などの課題に向けては
- SharedUIの継続的な整理
- QA自動化の強化
が重要になると考えています。
さいごに
今回の取り組みでは
- xcworkspaceの導入
- 複数xcodeprojによる環境分割
- Swift Package中心のモジュール構成
という構造改革を行い、もともと 従来の単一xcodeprojに依存した仕組みから一歩踏み出しました。
その移行における 苦労したポイント についても共有できたかと思います。
- pbxprojの大量置換
- xcconfigの共通化
- CI/CDのパラメータ整理
とくに、
- Build Settings
- ターゲット依存
- Info.plist の参照
は慎重に確認しないとビルドが通らなくなる箇所が多く、地道に手で修正していった部分もたくさんあります。
少しでも何かの参考になりましたら幸いです。
STORES レジ ではアプリを開発・運用するにあたって、品質の維持と向上への取り組みを行っています。
少しでも面白そうと感じていただけましたら、ぜひカジュアルに連絡をいただけますと泣いて喜びます。
また STORES では STORES レジ 以外のプロダクトでもエンジニアを絶賛募集中です。
ぜひ採用サイトにも遊びに来てください。