STORES Product Blog

こだわりを持ったお商売を支える「STORES」のテクノロジー部門のメンバーによるブログです。

multi-xcodeproj + xcworkspace 構成移行の実践

multi-xcodeproj + xcworkspace 構成移行の実践

はじめに

こんにちは、@marcy731 です。
STORES レジ のモバイルチームのマネージャー兼iOSエンジニアをしています。

stores.fun

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

としてしか扱えないという仕様があります。

maiyama4.hatenablog.com

具体的に何が起きるのか?

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中心のプロジェクト構成とその実践 に詳しく書いてありますで、まだみたことのない方がいましたら参照してください。

speakerdeck.com

構成の全体像

新しい構成のフォルダ構成イメージは以下のようになります。

├── 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

にリネームしました。

手順としては

  1. Swift Packageの Package.swift で iPadUI に変更
.target(name: "iPadUI", ...)
  1. ディレクトリも以下に変更
- Sources/UI/
+ Sources/iPadUI/
  1. 既存の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

さらに AppDelegateResources などのアプリ本体のコードも、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 レジ 以外のプロダクトでもエンジニアを絶賛募集中です。
ぜひ採用サイトにも遊びに来てください。

jobs.st.inc