Swift Package Manager

Swift Package Manager(以下、SPM)で作成したライブラリをデモアプリに組み込んでテストしたいと思うときがある。

が、SPMはGitのレポジトリから取り込む形にしか対応しているように見えない。実はローカルで使う方法もあるのだが、今回はその方法を紹介する。

Salmon Statsライブラリ

Swiftで使えるSalmon Statsライブラリ (opens new window)をSPMで開発したのだが、でもアプリがついていないのでいまいちわかりにくい感じになっている。

ちなみに現在対応しているのは次の機能。

  • シフト統計の取得
    • グローバルのみ取得
  • シフト記録の取得
    • 赤イクラ記録
    • 金イクラ記録
  • ユーザメタデータの取得
    • バイト回数とかのデータ
  • ユーザのリザルトの取得
    • 最大200件まで取得

というわけで、まずはGitHubからSalmon Statsライブラリを取得する。

git clone git@github.com:tkgstrator/SalmonStats.git
Cloning into 'SalmonStats'...
remote: Enumerating objects: 73, done.
remote: Counting objects: 100% (73/73), done.
remote: Compressing objects: 100% (54/54), done.
remote: Total 73 (delta 20), reused 52 (delta 10), pack-reused 0
Receiving objects: 100% (73/73), 15.29 KiB | 5.10 MiB/s, done.
Resolving deltas: 100% (20/20), done.
1
2
3
4
5
6
7
8

cloneしたらSalmonStatsディレクトリ以下にXcodeで新プロジェクトを作成する。

新プロジェクトを作成

XcodeからSalmonStatsDemoという新プロジェクトを作成する。

この段階ではまだローカルパッケージが読み込まれていない。

次にこのデモアプリのプロジェクトに対してSalmonStatsのディレクトリをそのままドラッグアンドドロップしてライブラリを追加する。

注意点としては「コピーする」は選択しなくて良いというところです。

ちゃんとできると上の画像のようにSalmonStatsのライブラリを読み込んでくれます。

ちなみに、プロジェクトを作成するときにIDETemplateMacros.plistを作成しておくと便利です。詳しくはカピ通信 (opens new window)さんが解説されています。

最後に、デモアプリに対してライブラリを追加します。

デモアプリ形式の便利なところ

SPMで読み込んだ場合にはライブラリのソースコードを変更できないという問題がありますが、このようにローカルでライブラリを読み込んだ場合にはライブラリのコードを変えながらデモアプリでチェックできます。

GitHubなどにコミットする必要もなく、手間が省けるというわけです。

既存のバグ

CombineExpectationsをライブラリに追加したままローカルやSPMで別のアプリに追加するとクラッシュします。

import PackageDescription
let package = Package(
    name: "SalmonStats",
    platforms:  [
        .iOS(.v13), .macOS(.v10_15)
    ],
    products: [
        .library(
            name: "SalmonStats",
            targets: ["SalmonStats"]),
    ],
    dependencies: [
        .package(url: "https://github.com/Alamofire/Alamofire.git", from: "5.4.2"),
        .package(url: "https://github.com/groue/CombineExpectations.git", from: "0.7.0")
    ],
    targets: [
        .target(
            name: "SalmonStats",
            dependencies: ["Alamofire"]),
        .testTarget(
            name: "SalmonStatsTests",
            dependencies: ["SalmonStats", "CombineExpectations"]),
    ]
)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25

どうもCombineExpectationsはtestTargetにしか追加してはダメなようだった。

サンプルコード

例えば以下のようなコードを書けばSalmon Statsから10万番目のリザルトを取得できる。

イニシャライザで宣言するとビューを呼び出す前にアクセスしてしまうのでonAppearで宣言するのが適切かもしれない。

// onAppearで呼び出す場合
import SwiftUI
import SalmonStats
import Combine
struct ContentView: View {
    @State private var task: AnyCancellable?
    var body: some View {
        Text("Hello, world!")
            .padding()
            .onAppear {
                task = SalmonStats.shared.getResult(resultId: 1000000)
                    .sink(receiveCompletion: { completion in
                        switch completion {
                        case .finished:
                            print("FINISHED")
                        case .failure(let error):
                            print("ERROR", error)
                        }
                    }, receiveValue: { response in
                        dump(response)
                    })
            }
    }
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
// イニシャライザで呼び出す場合
import SwiftUI
import SalmonStats
import Combine
struct ContentView: View {
    private var task: AnyCancellable?
    init() {
        task = SalmonStats.shared.getResult(resultId: 100000)
            .sink(receiveCompletion: { completion in
                switch completion {
                case .finished:
                    print("FINISHED")
                case .failure(let error):
                    print("ERROR", error)
                }
            }, receiveValue: { response in
                dump(response)
            })
    }
    var body: some View {
        Text("Hello, world!")
            .padding()
    }
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26

ただ、SwiftUIはstruct型なのでクロージャの中でselfを書き換えることができない。これはmutatingでも同じことである。詳しくはここのGitHub Gist (opens new window)を読めばいいことがあるかもしれない。

よって、イニシャライザを使った場合は受け取ったデータであるresopnseを処理することができない。なので普通にonAppearでいいような気がしてきました。

デモアプリ

import SwiftUI
import SalmonStats
import Combine
struct ContentView: View {
    @State private var result: Response.ResultCoop?
    @State private var task: AnyCancellable?
    var body: some View {
        List {
            HStack {
                Text("BOSS APPEARANCES")
                Spacer()
                Text("\(result?.bossAppearanceCount ?? 0)")
            }
        }
        .onAppear() {
            getResultFromSalmonStats()
        }
    }
    func getResultFromSalmonStats() {
        task = SalmonStats.shared.getResult(resultId: 100000)
                    .sink(receiveCompletion: { completion in
                        switch completion {
                        case .finished:
                            print("FINISHED")
                        case .failure(let error):
                            print("ERROR", error)
                        }
                    }, receiveValue: { response in
                        result = response
                        dump(response)
                    })
    }
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37

ビルドすると以下のように表示できる。

Combineは非同期処理なのでデータの読み込みが終わるまでは0と表示されているが、読み込みが完了すると正しい63という値に修正される。

ところでonAppearってダサいなあって思っているので前みたいにviewDidLoadとかで呼び出せるようになってくれないかなあと。