スタディサプリ Product Team Blog

株式会社リクルートが開発するスタディサプリのプロダクトチームのブログです

Xcode ProjectにローカルのSwift Packageを追加する2つの方法の違い

背景

まず、Swift Package、Xcode Project、およびXcode ProjectにSwift Packageを追加する方法について説明します。なお、説明は全てXcode 15.4の挙動に基づいています。

Swift PackageはSwiftのソースコードや関連するリソースファイルを1つにまとめる仕組みです。同じくSwiftを使うiOSアプリ開発と親和性が高く、iOSアプリ開発に用いられるXcodeという統合開発環境からよく参照されます。XcodeではソフトウェアをProjectという単位に分けて管理することが可能なのですが、このXcode Projectにローカルに存在するSwift Packageを自身の依存として追加する方法には2つあります。

1つ目の方法:Add Package Dependencies...する方法

1つ目は、Add Package Dependencies...する方法です。この方法の導線は自分が調べた限り以下の3つがあります。言い換えると、この方法はその3つの導線からSwift Packageを追加する方法です。なお、いずれの導線を使ってもその後に表示される画面は同一です。

  • XcodeのメニューからAdd Package Dependencies...を選択

  • XcodeのNavigator/Projectから右クリックでAdd Package Dependencies...を選択

  • XcodeのNavigator/ProjectからProjectの設定を開き、Package Dependenciesタブに表示されている+ボタンをクリックする

+ボタンはホバーするとAdd Package Dependenciesとアノテーションされます。

2つ目の方法: Fileとして追加する方法

2つ目は、Fileとして追加する方法です。この方法は以下の手順を踏む必要があります。

  • XcodeのNavigator/Projectから右クリックでAdd Files to "MyProject"を選択する

  • 追加したいSwift Packageを選択

  • Navigator/ProjectからGeneralタブを開き、Frameworks,Libraries, and Embedded Contentに先ほど追加したSwift Packageを追加する

自分が携わっているProjectでは2つの方法が使われており、違いが気になりました。

目的

いずれの方法もSwift Packageをインポートして利用するという点では同じ結果を得られますが、追加する方法が異なるためどちらを使うか迷ってしまいます。それぞれの違いを知って迷わないようにする事、その結果を共有する事がこの記事の目的です。背景までで察しがつく方も多いと思うので、初心者向けの記事になるかもしれません。

実験

それぞれの方法を使ってSwift PackageをXcode Projectに追加してみます。そして、それぞれの方法におけるXcodeの表示とXcode Projectに含まれるproject.pbxprojの差分をまとめます。なお、project.pbxprojはXMLでXcode Projectの設定を記述したファイルです。

1つ目の方法

まず、Xcodeの表示は以下のとおりです。Navigator/ProjectにPackage Dependenciesという欄が現れて、そこに追加したSwift Packageが表示されています。

次に、project.pbxprojの差分は以下のとおりです。FrameworksBuildPhaseにSwift Packageが追加されていて、ビルド対象に含まれている事がわかります。また、XCLocalSwiftPackageReference section やXCSwiftPackageProductDependency section が追加されています。

diff --git a/MyProject.xcodeproj/project.pbxproj b/MyProject.xcodeproj/project.pbxproj
index 7c40cac..6586b55 100644
--- a/MyProject.xcodeproj/project.pbxproj
+++ b/MyProject.xcodeproj/project.pbxproj
@@ -14,6 +14,7 @@
        F694CF102D083C9200C2DA8F /* MyProjectTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = F694CF0F2D083C9200C2DA8F /* MyProjectTests.swift */; };
        F694CF1A2D083C9200C2DA8F /* MyProjectUITests.swift in Sources */ = {isa = PBXBuildFile; fileRef = F694CF192D083C9200C2DA8F /* MyProjectUITests.swift */; };
        F694CF1C2D083C9200C2DA8F /* MyProjectUITestsLaunchTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = F694CF1B2D083C9200C2DA8F /* MyProjectUITestsLaunchTests.swift */; };
+       F694CF312D0850C400C2DA8F /* MyPackage in Frameworks */ = {isa = PBXBuildFile; productRef = F694CF302D0850C400C2DA8F /* MyPackage */; };
 /* End PBXBuildFile section */
 
 /* Begin PBXContainerItemProxy section */
@@ -51,6 +52,7 @@
            isa = PBXFrameworksBuildPhase;
            buildActionMask = 2147483647;
            files = (
+               F694CF312D0850C400C2DA8F /* MyPackage in Frameworks */,
            );
            runOnlyForDeploymentPostprocessing = 0;
        };
@@ -143,6 +145,9 @@
            dependencies = (
            );
            name = MyProject;
+           packageProductDependencies = (
+               F694CF302D0850C400C2DA8F /* MyPackage */,
+           );
            productName = MyProject;
            productReference = F694CEFB2D083C9000C2DA8F /* MyProject.app */;
            productType = "com.apple.product-type.application";
@@ -215,6 +220,9 @@
                Base,
            );
            mainGroup = F694CEF22D083C9000C2DA8F;
+           packageReferences = (
+               F694CF2F2D0850C400C2DA8F /* XCLocalSwiftPackageReference "MyPackage" */,
+           );
            productRefGroup = F694CEFC2D083C9000C2DA8F /* Products */;
            projectDirPath = "";
            projectRoot = "";
@@ -582,6 +590,20 @@
            defaultConfigurationName = Release;
        };
 /* End XCConfigurationList section */
+
+/* Begin XCLocalSwiftPackageReference section */
+       F694CF2F2D0850C400C2DA8F /* XCLocalSwiftPackageReference "MyPackage" */ = {
+           isa = XCLocalSwiftPackageReference;
+           relativePath = MyPackage;
+       };
+/* End XCLocalSwiftPackageReference section */
+
+/* Begin XCSwiftPackageProductDependency section */
+       F694CF302D0850C400C2DA8F /* MyPackage */ = {
+           isa = XCSwiftPackageProductDependency;
+           productName = MyPackage;
+       };
+/* End XCSwiftPackageProductDependency section */
    };
    rootObject = F694CEF32D083C9000C2DA8F /* Project object */;
 }

2つ目の方法

まず、Xcodeの表示は以下のとおりです。Navigator/Projectに他のファイルと同様にSwift Packageが表示されています。Swift PackageのアイコンがSwift Packageを表す📦のようなアイコンになっていることから、Xcodeに「お前、Swift Packageなんやな。」とSwift Package認定されている事がわかります。一介のディレクトリとは一線を画しているわけです。

次に、project.pbxprojの差分は以下のとおりです。PBXGroupに対する変更が入っている事から、Xcode Projectが管理しているディレクトリ構造であるグループに変更が入っている事がわかります。PBXResourcesBuildPhaseにも含まれており、リソースとしてコピーもされます。また、XCLocalSwiftPackageReference sectionがなくXCSwiftPackageProductDependency section のみが追加されている事がわかります。

diff --git a/MyProject.xcodeproj/project.pbxproj b/MyProject.xcodeproj/project.pbxproj
index 7c40cac..76abb9d 100644
--- a/MyProject.xcodeproj/project.pbxproj
+++ b/MyProject.xcodeproj/project.pbxproj
@@ -14,6 +14,8 @@
        F694CF102D083C9200C2DA8F /* MyProjectTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = F694CF0F2D083C9200C2DA8F /* MyProjectTests.swift */; };
        F694CF1A2D083C9200C2DA8F /* MyProjectUITests.swift in Sources */ = {isa = PBXBuildFile; fileRef = F694CF192D083C9200C2DA8F /* MyProjectUITests.swift */; };
        F694CF1C2D083C9200C2DA8F /* MyProjectUITestsLaunchTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = F694CF1B2D083C9200C2DA8F /* MyProjectUITestsLaunchTests.swift */; };
+       F694CF332D08523A00C2DA8F /* MyPackage in Resources */ = {isa = PBXBuildFile; fileRef = F694CF322D08523A00C2DA8F /* MyPackage */; };
+       F694CF362D08524400C2DA8F /* MyPackage in Frameworks */ = {isa = PBXBuildFile; productRef = F694CF352D08524400C2DA8F /* MyPackage */; };
 /* End PBXBuildFile section */
 
 /* Begin PBXContainerItemProxy section */
@@ -44,6 +46,7 @@
        F694CF152D083C9200C2DA8F /* MyProjectUITests.xctest */ = {isa = PBXFileReference; explicitFileType = wrapper.cfbundle; includeInIndex = 0; path = MyProjectUITests.xctest; sourceTree = BUILT_PRODUCTS_DIR; };
        F694CF192D083C9200C2DA8F /* MyProjectUITests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MyProjectUITests.swift; sourceTree = "<group>"; };
        F694CF1B2D083C9200C2DA8F /* MyProjectUITestsLaunchTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MyProjectUITestsLaunchTests.swift; sourceTree = "<group>"; };
+       F694CF322D08523A00C2DA8F /* MyPackage */ = {isa = PBXFileReference; lastKnownFileType = wrapper; path = MyPackage; sourceTree = "<group>"; };
 /* End PBXFileReference section */
 
 /* Begin PBXFrameworksBuildPhase section */
@@ -51,6 +54,7 @@
            isa = PBXFrameworksBuildPhase;
            buildActionMask = 2147483647;
            files = (
+               F694CF362D08524400C2DA8F /* MyPackage in Frameworks */,
            );
            runOnlyForDeploymentPostprocessing = 0;
        };
@@ -74,10 +78,12 @@
        F694CEF22D083C9000C2DA8F = {
            isa = PBXGroup;
            children = (
+               F694CF322D08523A00C2DA8F /* MyPackage */,
                F694CEFD2D083C9000C2DA8F /* MyProject */,
                F694CF0E2D083C9200C2DA8F /* MyProjectTests */,
                F694CF182D083C9200C2DA8F /* MyProjectUITests */,
                F694CEFC2D083C9000C2DA8F /* Products */,
+               F694CF342D08524400C2DA8F /* Frameworks */,
            );
            sourceTree = "<group>";
        };
@@ -127,6 +133,13 @@
            path = MyProjectUITests;
            sourceTree = "<group>";
        };
+       F694CF342D08524400C2DA8F /* Frameworks */ = {
+           isa = PBXGroup;
+           children = (
+           );
+           name = Frameworks;
+           sourceTree = "<group>";
+       };
 /* End PBXGroup section */
 
 /* Begin PBXNativeTarget section */
@@ -143,6 +156,9 @@
            dependencies = (
            );
            name = MyProject;
+           packageProductDependencies = (
+               F694CF352D08524400C2DA8F /* MyPackage */,
+           );
            productName = MyProject;
            productReference = F694CEFB2D083C9000C2DA8F /* MyProject.app */;
            productType = "com.apple.product-type.application";
@@ -231,6 +247,7 @@
            isa = PBXResourcesBuildPhase;
            buildActionMask = 2147483647;
            files = (
+               F694CF332D08523A00C2DA8F /* MyPackage in Resources */,
                F694CF062D083C9200C2DA8F /* Preview Assets.xcassets in Resources */,
                F694CF032D083C9200C2DA8F /* Assets.xcassets in Resources */,
            );
@@ -582,6 +599,13 @@
            defaultConfigurationName = Release;
        };
 /* End XCConfigurationList section */
+
+/* Begin XCSwiftPackageProductDependency section */
+       F694CF352D08524400C2DA8F /* MyPackage */ = {
+           isa = XCSwiftPackageProductDependency;
+           productName = MyPackage;
+       };
+/* End XCSwiftPackageProductDependency section */
    };
    rootObject = F694CEF32D083C9000C2DA8F /* Project object */;
 }

わかった事

まず、2つめの方法ではSwift Packageのディレクトリがグループに含まれます。そのためSwift Packageを追加した際にデフォルトで、Copy Bundle ResourcesにSwift Packageが含まれます。そのため、何も対応を行わないとSwift Packageがアプリに含まれます。以下がビルドしたアプリの中身です。Swift Packageのディレクトリがアプリ内にコピーされている事がわかります。回避するにはCopy Bundle Resourcesから取り除くだけで良いです。

また、2つ目の方法ではXCLocalSwiftPackageReference sectionがありません。また、1つ目の方法はNavigatorにlocalという表記が出てくるのに対して2つ目では表記されません。XCLocalSwiftPackageReference sectionはそういった表記の分岐処理に用いられているのかもしれません。

最後に2つ目の方法でのみXcode ProjectのテストプランにSwift PackageのTargetを追加できました。下にそれぞれの方法において、テストプランにテストターゲットを追加する画面を画像で記載します。この差分はSwift Packageがグループに含まれている影響かもしれません。1つ目の方法ではテストプランに登録できない事がSwift Forumにて言及されていました*1が、2つ目の方法でなぜうまく動作するかの情報は見つけられませんでした...。しかし、PBXFileReference sectionとPBXGroup sectionの差分を取り消すと1つ目の方法と同様にテストターゲットに追加できなくなったので、グループとしてリファレンスしているかが関係ありそうに感じました。

1つ目の方法においてMyPackageTestが選択肢に現れていない様子
2つ目の方法においてMyPackageTestが選択肢に現れている様子

結論

  • 基本1つ目の方法でOK
  • 1つ目の方法でシュッとできなさそうな事があれば(e.g. テストプランに追加)、2つ目を検討する
  • ただし、2つ目の方法を使う場合はCopy Bundle ResourcesからSwift Packageを取り除く