🛠️

ローカルPackageを持つXcode Projectのディレクトリ構成

2024/06/01に公開5

Swift Package Managerが登場してからSwift Packageを使ったマルチモジュール構成が流行っていますが、そんなローカルPackageを持つXcode Projectのディレクトリ構成はどのようにするのが一番いいのか考えましょう。

パターン1

Xcode ProjectのあるディレクトリとローカルPackageを並列に配置するパターン

RepositoryRoot
├── AppProject
│   ├── App
│   ├── App.xcodeproj
│   └── AppUITests
└── LocalPackage
    ├── Package.swift
    ├── Plugins
    ├── Sources
    └── Tests

パターン2

ルートをXcode Projectのあるディレクトリにし、その中にローカルPackageを配置するパターン

RepositoryRoot(≒AppProject)
├── App
├── App.xcodeproj
├── AppUITests
└── LocalPackage
    ├── Package.swift
    ├── Plugins
    ├── Sources
    └── Tests

パターン3

ルートをローカルPackageのディレクトリにし、その中にXcode Projectのあるディレクトリを配置するパターン

RepositoryRoot(≒LocalPackage)
├── AppProject
│   ├── App
│   ├── App.xcodeproj
│   ├── AppUITests
│   └── Package.swift
├── Package.swift
├── Plugins
├── Sources
└── Tests

考察

  • パターン1はアプリ本体とローカルパッケージの責務分離をディレクトリで示せるので、開発者の理解に優しい
  • Xcode ProjectのBuild Phases > Run Scriptのワークスペースを基準に考えるとパターン2が最適解
    • ワークスペース=ルートディレクトリになるので、相対パスで親ディレクトリに遡ってパスを辿る必要が発生しない
    • SwiftLintやSwiftFormatなどのツールを適用する際に、ルートディレクトリ配下の.swiftファイルを対象にするだけで全てカバーできる(.swiftlint.yml.swiftformatなどのコンフィグファイルの配置場所もルートディレクトリ一箇所にまとまる)
  • パターン3の場合、Xcode ProjectのあるディレクトリをローカルPackageの管轄外として扱うために、ダミーのPackage.swiftファイルを配置しないといけない

Discussion

kntkymtkntkymt

僕は完全に一緒ではないですが、パターン1のような形式でやっています
というのもRelease/Debug/Mockごとにxcodeprojを作って環境変数やDIする値を管理しており
ルートでxcworkspaceを使っているので、その下にプロジェクトとPackageが並ぶ形になっています。

参考: https://speakerdeck.com/d_date/swift-package-centered-project-build-and-practice?slide=91

- RepositoryRoot
    - App.xcworkspace
    - App
        -  ReleaseProject
            - Release
            - Release.xcodeproj
            - ReleaseUITests
        -  DebugProject
            - Debug
            - Debug.xcodeproj
            - DebugUITests
        - LocalPackage
            - Package.swift
            -  Plugins
            -  Sources
            -  Tests
KyomeKyome

は〜なるほど、弊社チームにもxcworkspace使っているプロジェクトあります。
ただkntkさんのケースの場合xcconfigファイルを使う方法で事足りませんか?
ReleaseUITestsとDebugUITestsで異なることをやっていたりするんでしょうか?

kntkymtkntkymt

ただkntkさんのケースの場合xcconfigファイルを使う方法で事足りませんか?

環境変数などについてはこの例で済むと思います。

自分は各xcodeprojでDIコンテナを定義することで
ReleaseとMockでコンパイルするソースを完全に分離するという設計にしていて
(Releaseでは実際のRepositoryのみに依存し、MockではMockのRepositoryにのみ依存する、これによってコンパイル時間の削減・アプリバイナリサイズの削減などができます)

依存するフレームワークとコンパイルする.swiftファイルがxcodeprojごとに変わるのでxconfigでは対応できないという認識でいます。

Release/Env.swift

import RepositoryProtocol
import Repository

struct ReleaseEnv: Env {
    let searchRepository: SearchRepositoryProtocol = SearchRepository(...)
}

Mock/Env.swift

import RepositoryProtocol
import RepositoryMock

struct MockEnv: Env {
     let searchRepository: SearchRepositoryProtocol = SearchRepositoryMock(...)
}
KyomeKyome

ReleaseとMockでコンパイルするソースを完全に分離するという設計にしていて

Release用とMock用の実装本体はLocalPackageの中にあって、xcodeprojでそれを選択的にDIコンテナで使うというのであっていますか?

kntkymtkntkymt

Release用とMock用の実装本体はLocalPackageの中にあって、xcodeprojでそれを選択的にDIコンテナで使うというのであっていますか?

そうです。

RepositoryProtocol: protocol
Repository: Release用の実装本体
RepositoryMock: Mock用の実装本体
のようにLocalPacakge内にtarget分割しています。