😽

SPM活用でモジュール分割したときのディレクトリ構成を考える

に公開

対象読者

SPM(Swift Package Manager)をモジュール分割に活用する使い方があります。
この記事は実際にSPM使ったモジュール分割しようとして、ディレクトリ構成どうしたらいいんだこれとなった人向けに書かれています。
以下の知識は前提とさせてください。

  • SPMの基礎
  • モジュール分割
  • SPMを使ったモジュール分割

SPMを使ったモジュール分割に関しては、d_dateさんの発信があるので、知りたい方はそっち見てください。

https://speakerdeck.com/d_date/swift-package-centered-project-build-and-practice

https://www.notion.so/date/Swift-PM-Build-Configuration-4f14ceac795a4338a5a44748adfeaa40

ルートディレクトリ

この記事ではルートディレクトリという言葉を使います。

ProjectName/
├── ProjectName/
└── ProjectName.xcodeproj

このようなiOSアプリの標準的なプロジェクトがあった場合、

ProjectName/ <- ルートディレクトリ
├── ProjectName/
└── ProjectName.xcodeproj

これのことをルートと言っています。

ディレクトリ構成をどうするか

SPMはルートディレクトリに単一のPackage.swift(マニフェストファイル)置くもよし、ルート・サブディレクトリに複数のPackage.swiftを置いても良し、となっており、柔軟な構成が組めます。
ただしリポジトリを外部に公開する場合、ルートディレクトリのPackage.swiftに公開設定を記述する必要があります。

外部に公開しないリポジトリであっても、「公開しようと思えばできる」状態にしておくことはメリットがあって、
組織内の別アプリから特定のモジュールを使わせたい!ということができます。
これはこれで大変なんですけど、という記事を前に書きました

この制約があるので、Package.swiftはルートディレクトリに置きたいです。
理想はこの構成です。

ProjectName/
├── ProjectName/
├── ProjectName.xcodeproj
├── Package.swift
├── Sources/
└── Tests/

ただこの構成にしようとすると、ローカルパッケージを上手くプロジェクト内に入れることができません。
(※記事の流れの都合上、ここではできないと書かせていただきますが、ガチャガチャ試してたらできる方法があったので、それは最後に書きます)

パッケージをプロジェクト内に入れようとすると……

  • Package.swiftのドラッグアンドドロップはソースファイルとして処理される
  • リポジトリのルートディレクトリのドラッグアンドドロップは最初パッケージ認識されるが、Xcodeを閉じて、開くとフォルダリファレンスになってしまう
    (※2021年時点でXcodeの挙動変わってますが、上手くいかないのは一緒。後述します)

ということで、xcodeprojファイル(プロジェクトファイル)とPackage.swiftを両方ルートに置くことはできません。

ディレクト構成の戦略

SPMによるモジュール分割(本人曰くHyper-modularization)のお手本としてisowordsがあります。

2021年7月にisowordsはディレクトリ構成を変更しています。
この議論を追いました。

https://github.com/pointfreeco/isowords/pull/122

詳細は直接プルリクエストを見ていただくとして、選択肢が3つあったことがわかります。

  1. ルートディレクトリにxcodeprojファイルを置く。Package.swiftをサブディレクトリに置く
  2. ルートディレクトリにPackage.swiftを置く。xcodeprojファイルをサブディレクトリに置く
  3. ワークスペース(xcworkspace)をつくって適切に配置する

結局このプルリクでは、3→2に変更が入りました。
それぞれのメリデメですが、

  1. メリット: xcodeprojファイルがルートにあってわかりやすい / デメリット: パッケージの公開ができない
  2. メリット: パッケージの公開ができる / デメリット: xcodeprojファイルがルートになくてわかりづらい
  3. メリット: プロジェクトとパッケージを適切に扱える / デメリット: ワークスペースが扱いづらい

isowordsが2を選択したのは、やはりルートにPackage.swiftがあった方が、公開まで考えると良い構成で、SPMのメリットを十分に享受できるという判断なんだと思われます。
SPMを使ったモジュール分割をしている例をいくつか探してみましたが、この方式がスタンダードになっている印象でした。

ルートディレクトリのドラッグアンドドロップができなくなっている問題

以上が議論のまとめでした。
というわけでルートにPackage.swift置いて、プロジェクトファイルを一個下の階層に下げる構成を試していたのですが、問題が起こりました。

Xcode 16.4で操作すると、ルートディレクトリのドラッグアンドドロップができませんでした。

https://www.youtube.com/watch?v=TAD5jAqaH-A

isowordsのプルリクや、Twitterのフォロワーからも、ドラッグアンドドロップでプロジェクト内にパッケージを追加できた模様です。
今のXcodeで新規に設定しようとすると、Package Dependenciesの欄に表示するしかありません。

検証リポジトリ isowords

別に左の状態でも編集はできるんで、問題ないっちゃないんですけど、
将来的に外部パッケージが増えたときに、内部パッケージが混じってるのはわかりづらいので、パッケージ内に表示したいです。

またこのXcodeの変更がいつからなのか、意図的なものなのかは不明です。
経緯知ってる方いたらコメント欲しいです。

直接パスを編集する

諦めるしかないかなと思っていたところ、@devdazyさんがアドバイスをくれて、
以下の手順でやると、プロジェクト内にパッケージが入れられました。

  • サブディレクトリにパッケージを新規作成する
  • そのパッケージのディレクトリをXcodeにドラッグアンドドロップ
    • これは問題なくプロジェクト内パッケージとして認識されます
  • pbxprojファイルを手編集してパスをルートに書き換える
    • (念のため)xcodeprojを右クリックして「パッケージ内容を表示」でアクセスできます
- A671F2E52DF59681001E451B /* AppFeature */ = {isa = PBXFileReference; lastKnownFileType = wrapper; name = AppFeature; path = ../AppFeature; sourceTree = SOURCE_ROOT; };
+ A671F2E52DF59681001E451B /* AppFeature */ = {isa = PBXFileReference; lastKnownFileType = wrapper; name = AppFeature; path = ../; sourceTree = SOURCE_ROOT; };

ディレクトリ名とパッケージ名は一致していなくても動きました。
できればあまりやりたくない方法ではあるんですけど、今だとこれしか実現方法がないみたいです。

https://github.com/0si43/GitHubUsers/pull/15

対応した詳細は上記のプルリクを見ていただければと思います。

直接パス編集するのであればルートに全部置けた(追記)

「手でパスを編集するのなら、xcodeprojファイルがルートにあっても共存できるのでは?」
と、この記事を書きながらふと思いました。
どうせ何か問題起きるんだろうなと思って、試してみたのですが、なんとできました

- A671F2E52DF59681001E451B /* AppFeature */ = {isa = PBXFileReference; lastKnownFileType = wrapper; name = AppFeature; path = ../; sourceTree = SOURCE_ROOT; };
+ A671F2E52DF59681001E451B /* AppFeature */ = {isa = PBXFileReference; lastKnownFileType = wrapper; name = AppFeature; path = .; sourceTree = SOURCE_ROOT; };

プロジェクトファイルの場所がルートになったので、パスもカレントディレクリに変更しています。

Finder Xcode

ディレクトリ構成とXcode上の表示です。
これで問題ないのであればこれでいきたいですね。

https://github.com/0si43/GitHubUsers/pull/18

対応したプルリクはこちらです。

更に実験(追記)

この構成ができるなら、更に標準のディレクト構成に近くできないかなと思い、
プロジェクトファイルの階層下げるにあたってリネームしていたMain(isowordsだとAppに相当)も、名前をプロジェクト名と一緒にしたら、よりわかりやすいなと思いました。

Xcode上のリネームで、Main -> GitHubUsersにするのを試しましたが、上手くいきませんでした。

パッケージ名が出てなくておかしいんですけど、ビルドにあたっては No such module 'PackageDescription' でコンパイルエラーが出ます。
これはメインディレクトリ(リネーム後はGitHubUsers/)内のPackage.swiftに対して出ているので、これを消したら一応ビルドはできました。

パッケージ名が表示されない問題は解決できませんでした。
パッケージ名とプロジェクト名を今別のものにしていたので、一緒にするも試しましたが変わらず。

プロジェクトファイル & Swift.package両方をルートに置く戦略を取ったとしても、ここは別名にしておくのが無難なようです。

まとめ

以上書いてきたことを改めてまとめると、ディレクトリ構成は4つ選択肢があります。
オススメ順に並べると、

  1. ルートディレクトリにxcodeprojファイルもPackage.swiftも置く
  2. ルートディレクトリにPackage.swiftを置く。xcodeprojファイルをサブディレクトリに置く
  3. ルートディレクトリにxcodeprojファイルを置く。Package.swiftをサブディレクトリに置く
  4. ワークスペース(xcworkspace)をつくって適切に配置する

1が今のところ最も見やすい構成(ディレクト&Xcode上両方)だと思っています。
ただ操作が危険なので、Appleの推奨ではないと思われます。
デファクトスタンダードが2だと思うんですが、今のXcodeだとドラッグアンドドロップできなくなっているので注意が必要です。
3が正直一番設定作業は楽でした。
4は試してません。

(了)

Discussion