Tomo.Log()


SPMの1つのパッケージに複数のプロダクト・モジュールを含めたい!

[11/30, 2024]

こんにちはトモです。

みなさん、Swift Package Manager(SPM)使ってますか?最近ちょっとずつ理解が進んで、ちょっぴり楽しくなってきました。そんな中1つのパッケージの中に複数のプロダクトを含めたいという欲求が出てきました。

やりたい事

Package Aをdependenciesに入れたらアプリ側で

import X
import Y

のように複数のimportができるようにしたい感じです。例えるならばfirebase-ios-sdkのリポジトリに「FirebaseCrashlytics」や「FirebaseAnalytics」が含まれている状態ですかね。(Firebaseはとても巨大で複雑なので厳密には違う可能性もあります...)

ここで疑問:各種nameの意味

またそれを実現するにあたり、このPackageのnameと.libraryのnameと.targetのnameのnameってどこで使われる名前なの?という疑問が湧きました。(そもそもPackage.swiftの書き方も雰囲気で書いているのですが。。。😅)

let package = Package(
    name: "Name1", //<-これ
    products: [
        .library(name: "Name2", targets: []), //<-これ
    ],
    targets: [
        .target(name: "Name3", dependencies: []), //<-これ
    ]
)

なぜならば、これを理解しないとXとYの指定をどこにすればいいかわからなかったからです。

よくあるSPMのパッケージのつくりかたはName1〜3まで全部同じ名前になっていますよね?なんならパッケージのフォルダの名前も同じです。 なので自分は正直、思考停止で全部同じ名前を指定している状態でした。

調べてみた

今回試したのはBaseViewKitというフォルダの中にパッケージを作っています。

BaseViewKit/
    Package.swift
    Sources/

またローカルなSPMのパッケージという前提でやっていますが、リモートでも概ね同じだと思います。

フォルダー名は

フォルダー名は

File > Add Package Dependencies > Add Local...

の読み込み時に表示されます。

この投稿をInstagramで見る

Tomolog(@tomologram)がシェアした投稿

この画像の「Choose Package Products for BaseViewKit」の部分はName1が表示されると思っていました。

Name1は

Xcode上のPackageのListなどに表示されます。

この投稿をInstagramで見る

Tomolog(@tomologram)がシェアした投稿

Add PackageのRecently Usedにもフォルダー名が出ています。(なぜ?)リモートのパッケージの場合はちゃんとName1が表示される・・・?

Name2は

Frameworks, Libraries, and Embedded Contentの中などに表示されます。

この投稿をInstagramで見る

Tomolog(@tomologram)がシェアした投稿

なるほどまさにライブラリ名だったんですね。

余談ですが、ローカルのパッケージを追加したり削除したり名前を編集したり繰り返している際に、古いライブラリ名がここに残ってしまうことがありました。
すると、ビルド時にそんな名前のライブラリ見つからないよと怒られます。最初エラーの原因がわからずプロジェクトをクリーンしたり、DerivedDataを削除したりしましたが解決しませんでした。
よくよくみたらここに古い名前が残っていることに気づき、選択して「ー」ボタンを押して手動で削除したら解決しました。

ローカルのパッケージを削除する際に一緒に消えてくれないのはバグなのか?仕様なのか・・・?

Name3がimportの名前

そしてName3が大本命のimport XXXXに相当する名前でした。

この投稿をInstagramで見る

Tomolog(@tomologram)がシェアした投稿

なのでtargetを複数用意してName3の部分を理想の名前にすれば良さそうです。

つまりtargetsを複数作って好きな名前をつけるとよい

最終的には以下のようになりました。(最小限の記載です)

let package = Package(
    name: "BaseViewKit",
    products: [
        .library(name: "BaseViewKit", targets: [
        	    //Name3:targetsの名前を指定
            "BaseViewV1",
            "BaseViewV2",
        ]),
    ],
    targets: [
        .target(name: "BaseViewV1", dependencies: []),
        .target(name: "BaseViewV2", dependencies: [], path:"Sources/V2"),
    ]
)

Name3(.target)の部分を2つ作り、importしたいnameにします。

こうすることでアプリ側では

import BaseViewV1
import BaseViewV2

と呼べるようになります。

フォルダ構成とpathの指定について

また余談ですがtargetのpathを指定しない場合はsourceのフォルダ名がnameと同じ必要があるようです。

targets: [
    .target(name: "BaseViewV1", dependencies: []), <- 指定なし
    .target(name: "BaseViewV2", dependencies: [], path:"Sources/V2"), <-指定あり
]

なので、「BaseViewV2」の方ではあえて指定する形もサンプルとして載せてみました。

BaseViewKit/
    Package.swift
    Sources/
        BaseViewV1/ <- pathの指定なし = nameと同じ名前
        V2/ <- pathの指定ありなので好きな名前でOK

これで1つのパッケージに複数のモジュール?を含めることができるようになりました。

機能ごとに分けてもいいですし、自分はBaseViewの破壊的変更があった際にBaseView2として切り出したいニーズがあったのでそう言った使い方もできるのかなーと思っています。

また今回調べてみて

products: [ .library() ]

ここを複数用意したい場合はどういうようとの時なんだろう?🤔という新しい疑問も湧いてきました。

ひとまずやりたいことができたので、めでたしめでたし🎉