LoginSignup
16
5

More than 1 year has passed since last update.

個人開発アプリに Swift Package Manager を導入してみた

Last updated at Posted at 2022-12-17

この記事はand factory.inc Advent Calendar 2022 18日目の記事です。
昨日は @y-okudera さんの Flutterで連続性のある複雑なアニメーションを実装する でした。

はじめに

今回は個人開発しているアプリに Swift Package Manager (以降SPMと記載) を導入した際の記事になります。
これまでは XcodeGen + Carthage を用いており、DataStore層, Domain層, Presentation層の3層を Embedded Framework 化したプロジェクト構成でした。
これらを全て SPM で置き換える事が出来るのでは?と思い移植を試みました。

開発環境

  • MacBook Pro(14インチ、2021) Apple M1 Max
  • Xcode14.1(14B47b)
  • Swift version 5.7.1

XcodeGen, Embedded Framework からの脱却

まずは XcodeGen と Embedded Framework 化をやめて、SPM を用いてマルチモジュール管理するよう対応してみました。

project.yml の削除

Diff
-name: RoyaleApp
-
-options:
-  bundleIdPrefix: nakandakari.toru.RoyaleApp
-  deploymentTarget:
-    iOS: 13.0
-  developmentLanguage: en
-  xcodeVersion: "14.0"
-
-settings:
-  base:
-    MARKETING_VERSION: 1.2.0
-    CURRENT_PROJECT_VERSION: 1
-    DEVELOPMENT_TEAM: 8CBGKNYH9U
-    DEBUG_INFORMATION_FORMAT: "dwarf-with-dsym"
-    CODE_SIGN_STYLE: Manual
-    SWIFT_VERSION: 5.0
...略

.gitignore の更新

Diff
XcodeProj を管理するようにする
-# xcodegen
-*.xcodeproj
-*.xcworkspace

Package の追加

  1. Targets から Embedded Framework を削除する
    スクリーンショット 2022-12-13 19.54.32.png

  2. Packageを置く任意のディレクトリ(Packages)を作成し、File->New->Packages から Package を作成
    Packages作成①
    Packages作成②
    Packages作成③

  3. Sources 以下に元の DataStore 層のソースコードを移動させる
    Sources以下にソースコードを移動

  4. Package.swift にパッケージ構成を書く
    スクリーンショット 2022-12-13 20.14.12.png

あとは他の Embedded Framework も同様の作業をして置き換え作業していけば OK :ok_hand:

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

import PackageDescription

let package = Package(
    name: "DataStore",
    platforms: [
        .iOS(.v14)
    ],
    products: [
        // Products define the executables and libraries a package produces, and make them visible to other packages.
        .library(
            name: "DataStore",
            targets: ["DataStore"]
        )
    ],
    dependencies: [
        // Dependencies declare other packages that this package depends on.
        // .package(url: /* package url */, from: "1.0.0"),
        .package(url: "https://github.com/Alamofire/Alamofire", from: "5.6.2"),
        .package(url: "https://github.com/realm/realm-swift", from: "10.30.0")
    ],
    targets: [
        // Targets are the basic building blocks of a package. A target can define a module or a test suite.
        // Targets can depend on other targets in this package, and on products in packages this package depends on.
        .target(
            name: "DataStore",
            dependencies: [
                .product(name: "Alamofire", package: "Alamofire"),
                .product(name: "Realm", package: "realm-swift"),
                .product(name: "RealmSwift", package: "realm-swift")
            ]),
        .testTarget(
            name: "DataStoreTests",
            dependencies: ["DataStore"])
    ]
)
Domain/Package.swift
// swift-tools-version: 5.7
// The swift-tools-version declares the minimum version of Swift required to build this package.

import PackageDescription

let package = Package(
    name: "Domain",
    platforms: [
        .iOS(.v14)
    ],
    products: [
        // Products define the executables and libraries a package produces, and make them visible to other packages.
        .library(
            name: "Domain",
            targets: ["Domain"])
    ],
    dependencies: [
        // Dependencies declare other packages that this package depends on.
        // .package(url: /* package url */, from: "1.0.0"),
        .package(name: "DataStore", path: "../DataStore")
    ],
    targets: [
        // Targets are the basic building blocks of a package. A target can define a module or a test suite.
        // Targets can depend on other targets in this package, and on products in packages this package depends on.
        .target(
            name: "Domain",
            dependencies: [
                .product(name: "DataStore", package: "DataStore")
            ]),
        .testTarget(
            name: "DomainTests",
            dependencies: ["Domain"])
    ]
)
Presentation/Package.swift
// swift-tools-version: 5.7
// The swift-tools-version declares the minimum version of Swift required to build this package.

import PackageDescription

let package = Package(
    name: "Presentation",
    platforms: [
        .iOS(.v14)
    ],
    products: [
        // Products define the executables and libraries a package produces, and make them visible to other packages.
        .library(
            name: "Presentation",
            targets: ["Presentation"])
    ],
    dependencies: [
        // Dependencies declare other packages that this package depends on.
        .package(name: "Analytics", path: "../Analytics"),
        .package(name: "Domain", path: "../Domain"),
        .package(url: "https://github.com/danielgindi/Charts", from: "4.1.0"),
        .package(url: "https://github.com/marcosgriselli/SwipeableTabBarController", from: "3.4.2"),
        .package(url: "https://github.com/onevcat/Kingfisher", from: "7.3.2"),
        .package(url: "https://github.com/googleads/swift-package-manager-google-mobile-ads", from: "9.11.0")
    ],
    targets: [
        // Targets are the basic building blocks of a package. A target can define a module or a test suite.
        // Targets can depend on other targets in this package, and on products in packages this package depends on.
        .target(
            name: "Presentation",
            dependencies: [
                .product(name: "Analytics", package: "Analytics"),
                .product(name: "Domain", package: "Domain"),
                .product(name: "Charts", package: "Charts"),
                .product(name: "Kingfisher", package: "Kingfisher"),
                .product(name: "SwipeableTabBarController", package: "SwipeableTabBarController"),
                .product(name: "GoogleMobileAds", package: "swift-package-manager-google-mobile-ads")
            ]),
        .testTarget(
            name: "PresentationTests",
            dependencies: ["Presentation"])
    ]
)

最終的なプロジェクト構成はコチラ
スクリーンショット 2022-12-13 20.36.48.png

Carthage からの脱却

Package.swift にてライブラリの依存関係を記載しているので基本的には以下ファイル郡を削除するだけで OK :ok_hand:

  • Cartfile
  • Cartfile.resolved
  • Carthage/Checkouts/

あとは Makefile を修正したり Carthage 専用のスクリプトを削除したりしました。

ハマったポイント

Sources 直下のディレクトリは1つのディレクトリ構成にする

以下のようなディレクトリ構成にすると Cannot find XXX in scope のエラーが出てしまいソースコードを読み込んでくれません。

スクリーンショット 2022-12-17 9.55.42.png

原因としては Package の Sources 以下はディレクトリごとに別ターゲット扱いになるためです。
これを解決する方法として2種類の方法があります。

①Package.swiftにそれぞれのターゲットの依存関係を記載する

Package.swift
...
targets: [
        // Targets are the basic building blocks of a package. A target can define a module or a test suite.
        // Targets can depend on other targets in this package, and on products in packages this package depends on.
        .target(
            name: "Test",
            dependencies: ["Test2"]), // Test1 のターゲットで Test2 のターゲットを扱うために追加
        // Test2をターゲットとして認識させるために追加
        .target(
            name: "Test2",
            dependencies: []),
        .testTarget(
            name: "TestTests",
            dependencies: ["Test"]),
    ]

こうすることで Test1 側で Test2 をインポート出来るようになるので無事エラーが解消されます

Test.swift
import Test2 // Test2 がインポートできる

public struct Test {
    public private(set) var text = "Hello, World!"

    public init() {
        let test2 = Test2()
        print("$$$ test2.number = \(test2.hogeNumber)")
        print("$$$ test2.string = \(test2.fugaString)")
    }
}

②Sources 直下を1つのディレクトリとして置く

スクリーンショット 2022-12-17 10.09.07.png

上記のように Sources 直下は Test ディレクトリだけにしてその Test ディレクトリ以下に Test2 ディレクトリを配置する。
このディレクトリ構成にすると Package.swift などを変更せずに Test2 の内容を読み込むことができます。

個人的には①より②の解決方法が良いと思っていて、理由としては①のように依存関係を書くならそもそも Package 自体分けるべきなのかなと思ったからです。(Testパッケージ内ではなくTest2のパッケージを作ってそこの Sources 以下に Test2 のコードを配置して Test の Package.swift に Test2 を依存するように書く)

ただ正直まだこの辺の理解をちゃんと出来ている訳ではないので詳しい方がいましたらコメントでご指摘頂けると幸いです。

Realm の依存関係の記述ミス

Realm は1つのパッケージで RealmRealmSwift の2つのプロダクトを含んでいるためこの2つの依存関係を書かないと上手く読み込んでくれません。

Realm/Package.swift
...
let package = Package(
    name: "Realm",
    platforms: [
        .macOS(.v10_10),
        .iOS(.v11),
        .tvOS(.v9),
        .watchOS(.v2)
    ],
    products: [
        .library(
            name: "Realm",
            targets: ["Realm"]), // Realm
        .library(
            name: "RealmSwift",
            targets: ["Realm", "RealmSwift"]), // RealmSwift
    ],
...

そのため以下のように依存関係を記述しないといけないです。

DataStore/Package.swift
    ...
    targets: [
        // Targets are the basic building blocks of a package. A target can define a module or a test suite.
        // Targets can depend on other targets in this package, and on products in packages this package depends on.
        .target(
            name: "DataStore",
            dependencies: [
                .product(name: "Alamofire", package: "Alamofire"),
                .product(name: "Realm", package: "realm-swift"),     // Realm
                .product(name: "RealmSwift", package: "realm-swift") // ReamSwift
            ]),
        .testTarget(
            name: "DataStoreTests",
            dependencies: ["DataStore"])
    ]
    ...

起動時に storyboard や xib 周りでクラッシュ

ビルドエラーを解消し、ようやくシミュレータで動作確認しようとすると起動時に以下のクラッシュが発生

Terminating app due to uncaught exception 'NSInvalidArgumentException', reason: 'Could not find a storyboard named 'RootViewController' in bundle NSBundle

スクリーンショット 2022-12-17 10.46.05.png

Embedded Framework の時は storyboard や xib の Inherit Module From Target にチェックを付ける必要があったのですが、パッケージ管理にした場合は不要となるので全てのチェックを外す必要があります。(なぜ不要になるかはイマイチ理解出来ていない...)

スクリーンショット 2022-12-17 11.49.27.png

そこまで数は多くなかったものの1つずつ手動で設定を外していくのは面倒だったので以下のコマンドで一括でチェックを外すよう対応

# storyboard や xib の
$ git grep -l 'customModuleProvider="target"' | xargs sed -i '' -e 's/customModuleProvider="target"//g'

おわりに

SPMが登場してから大分日は経ちますが、まだまだキャッチアップ出来ていない部分も多々あるので今後も引き続き活用して理解を深めていければと思っています。

最後まで読んでいただきありがとうございます。
明日の Advent Calendar の記事もお楽しみに:santa:

16
5
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
16
5