📦

既存の Xcode プロジェクトを SwiftPM でマルチモジュール化する最初のステップ

2022/02/05に公開

本記事では Swift Package Manager(SwiftPM) を用いて、既存の Xcode プロジェクトをマルチモジュール化する最初のステップについて説明します。

「最初のステップ」であるため、SwiftPM で本格的なプロジェクトのマルチモジュール化を行う際に起きるかもしれない問題の解決策などについては説明しません🙏

本記事では以下を理解できるようになることを目指しています。

  • SwiftPM を用いたマルチモジュール化の始め方
  • SwiftPM を用いたマルチモジュール化の基本的な流れ

以降では非常にシンプルなプロジェクトを参考に、SwiftPM を用いてマルチモジュール化を行う方法について説明します。

マルチモジュール化していくプロジェクト

まずマルチモジュール化していくプロジェクトについて触れておきます。
例としてはシンプルすぎますが、以下のような作りたての SwiftUI プロジェクトを例にします。(マルチモジュール化すべきかどうかは置いておきます)

.
├── Sample
│   ├── Assets.xcassets
│   ├── ContentView.swift
│   ├── Preview Content
│   └── SampleApp.swift
└── Sample.xcodeproj
    ├── project.pbxproj
    ├── project.xcworkspace
    └── xcuserdata

Xcode 上の Project Navigator で示すと以下のようになっています。

Sample プロジェクトを SwiftPM でマルチモジュール化する

では実際に少しずつマルチモジュール化を行っていきます。

App ディレクトリに全てのファイルをまとめる(任意)

今から説明する手順はやってもやらなくても良いのですが、後の作業が楽になるため実施しておきます。

まず前準備として、ルートディレクトリに存在しているファイルを全て任意のディレクトリにまとめてしまいます。
そのために、ルートディレクトリで以下のコマンドを叩きます。

$ mkdir App
$ ls
App Sample Sample.xcodeproj
$ mv Sample Sample.xcodeproj App
$ ls App
Sample Sample.xcodeproj

これで App ディレクトリに全てのファイルがまとまりました。

ルートディレクトリで swift package init を叩く

次にルートディレクトリで swift package init を叩きます。

$ ls
App
$ swift package init
Creating library package: Sample
Creating Package.swift
Creating README.md
Creating .gitignore
Creating Sources/
Creating Sources/Sample/Sample.swift
Creating Tests/
Creating Tests/SampleTests/
Creating Tests/SampleTests/SampleTests.swift
$ ls
App Package.swift README.md Sources Tests

これによりいくつかのファイルやディレクトリが作成されました。

Xcode プロジェクトを整理する

ここまで来たら既存のプロジェクトファイルを開き、整理していきます。

$ open App/Sample.xcodeproj

開くと、一番最初の状態と何も変わっていない状態のプロジェクトが表示されると思います。

ここから先ほど swift package init で生成されたファイル等を利用しつつ、プロジェクト内を整理します。手順についても軽く先に示しておきます。

  • Xcode プロジェクトに Package.swift が格納されているディレクトリをドラッグ&ドロップする
  • Project Navigator で表示されて欲しくないディレクトリに空の Package.swift ファイルを追加する

それぞれについて説明していきます。

Xcode プロジェクトに Package.swift が格納されているディレクトリをドラッグ&ドロップする

SwiftPM を用いたマルチモジュール化を行っていくために、プロジェクトに Package.swift が格納されているディレクトリ(今回の例では Sample)をドラッグ&ドロップしてみましょう。

以下がその操作イメージになります。

gif を見ていただけるとわかりますが、Xcode プロジェクトに Package.swift が入ったディレクトリをドラッグ&ドロップするだけで Xcode プロジェクトが Swift Package として認識してくれていることがわかります。(手元で Xcode 13.0 で試しましたが認識してくれませんでした。Xcode 13.2 では動作することが確認できました。またうまくいかない場合は Xcode を再起動するとうまくいく場合がありました。)

Project Navigator で表示されて欲しくないディレクトリに空の Package.swift ファイルを追加する

これで Xcode プロジェクトに Swift Package を導入することができましたが、現状のままだと少し問題があります。
Project Navigator 上でディレクトリ構成を見てみると、App ディレクトリが丸ごと見えてしまっているという問題が発生しています。

この問題を解決するためには Project Navigator 内で見せたくないディレクトリ直下に、空の Package.swift を追加する必要があります。
空の Package.swift を該当のディレクトリに追加することによって、そのディレクトリを Xcode に独自の Swift Package であると解釈させることができ、結果として Project Navigator に表示されないようにすることができます。

では早速 Xcode 上の Package 内に表示されてしまっている App ディレクトリに、空の Package.swift を追加しましょう。

Package.swift
import PackageDescription

let package = Package(
  name: "",
  products: [],
  dependencies: [],
  targets: []
)

そして Xcode を再起動します。
すると、Project Navigator 上から無事 Package 内の App ディレクトリが消えていることがわかります。

マルチモジュール化する

ここまででマルチモジュール化を行うための準備は全て整いました。
後はマルチモジュール化の作業を実際に行うのみです。

現状 swift package init によって作成されたディレクトリ構成は以下のようになっています。

一方、Package.swift は以下のようになっています。

Package.swift
// swift-tools-version:5.5
// The swift-tools-version declares the minimum version of Swift required to build this package.

import PackageDescription

let package = Package(
    // package 名
    name: "Sample",
    // .library という形式でモジュールを追加していく
    products: [
        .library(
            name: "Sample",
            targets: ["Sample"]),
    ],
    // ライブラリなどの依存関係を定義する
    dependencies: [
    ],
    // target や test 用の target を追加していく
    targets: [
        .target(
            name: "Sample",
            dependencies: []),
        .testTarget(
            name: "SampleTests",
            dependencies: ["Sample"]),
    ]
)

コメントで軽く説明は書きましたが、説明のために一旦 Sources, Tests ディレクトリにあるファイルの全てを削除し、Package.swift もそれに合わせて修正します。

Package.swift
// swift-tools-version:5.5
// The swift-tools-version declares the minimum version of Swift required to build this package.

import PackageDescription

let package = Package(
    name: "Sample",
    products: [
    ],
    dependencies: [
    ],
    targets: [
    ]
)

現在モジュール化すべきファイルはほとんどない Sample プロジェクトですが、モジュール化の一連の流れを説明するために ContentView.swift をモジュール化してみます。

まず Sources ディレクトリに AppFeature ディレクトリを作成します。

Sources に新たなモジュールが追加されたので、Package.swift ファイルを以下のように修正します。

Package.swift
// swift-tools-version:5.5
// The swift-tools-version declares the minimum version of Swift required to build this package.

import PackageDescription

let package = Package(
    name: "Sample",
    platforms: [.iOS(.v13)], // SwiftUI を利用しているため追加
    products: [
        .library(name: "AppFeature", targets: ["AppFeature"])
    ],
    dependencies: [
    ],
    targets: [
        .target(name: "AppFeature")
    ]
)

次に ContentView.swift をモジュール化するために、作成した AppFeature ディレクトリに移動します。

モジュールに追加したファイルに対して、モジュール外からアクセスするためには public な修飾子を付与する必要があるため、ContentView.swift を以下のように修正します。

import SwiftUI

public struct ContentView: View {
    // 外部からインスタンス化するために public initializer も追加
    public init() {}

    public var body: some View {
        Text("Hello, world!")
            .padding()
    }
}

後はメインアプリターゲットに存在している SampleApp.swift から AppFeature モジュールをimport できるようにすれば無事モジュール化が完了します。

メインアプリターゲットから import できるようにするために、まずメインアプリターゲットの「Frameworks, Libraries, and Embedded Content」から「AppFeature」を追加する必要があります。

これでメインアプリターゲット内のコードから AppFeature を import することができるようになるため、後は必要な部分で import するだけになります。

SampleApp.swift
// これを追加
import AppFeature
import SwiftUI

@main
struct SampleApp: App {
    var body: some Scene {
        WindowGroup {
            ContentView()
        }
    }
}

ここまでで無事ビルドが成功すると思います👏

おわりに

最初に説明させて頂いた通り、本記事では SwiftPM を用いたマルチモジュール化の非常に基本的な部分についてのみしか説明していません。
そのため、大きなプロジェクトに速攻適用できるようなものではないですが、SwiftPM を用いたマルチモジュール化の基本的な流れを理解する助けにはなるかなと思っております。
本記事が今後 SwiftPM を用いてマルチモジュール化を行う方の助けになれば幸いです。

参考

Discussion