SwiftPackageManager

Apple 謹製のライブラリ管理ツールなのだが、CocoaPods や Carthage で開発したライブラリをそのまま移植しようとするとバグることがある。

というのも、どうも外部ファイルが正しく読み込めていないようで XIB や NIB などのファイルを読み込ませようとするとクラッシュする。

既存の問題

現在のところ、以下のバグが存在しているようだ。

  • IBDesignable が効かない
  • XIB や NIB をそのまま利用するとクラッシュする
    • 対応策はあるが

今回は XIB や NIB については扱わず、JSON ファイルをローカルでライブラリに追加したい場合を考える。

もちろん、最初から JSON を Swift のデータ構造に変換してから追加すればこのような記事は要らないのだが、いちいち変換するコードも書きたくないのである。

今回考えるのは以下のような JSON である。全部書くと長くなるので最初の一つのオブジェクトだけ書いたが、これが延々と数百個配列に入ったものだと考えてもらえば良い。

[
  {
    "end_time": 1500696000,
    "rare_weapon": 20000,
    "stage_id": 5000,
    "start_time": 1500616800,
    "weapon_list": [10, 5010, 1010, 2010]
  }
]
1
2
3
4
5
6
7
8
9

賢明なうちの読者の皆様ならわかるだろうが、これは Codable を使って自動エンコードできる。

// 受け取るJSON配列一つ一つの構造
public struct CoopShift: Codable {
    var startTime: Int
    var endTime: Int
    var stageId: Int
    var rareWeapon: Int
    var weaponList: [Int]
}
1
2
3
4
5
6
7
8

つまり、こういう構造になっているというのがすぐに分かるわけである。

最後に JSONDecoder のkeyDecodingStrategyでスネークケースからの自動変換設定をつけて読み込ませればいいというわけだ。

let decoder: JSONDecoder = {
    let decoder = JSONDecoder()
    decoder.keyDecodingStrategy = .convertFromSnakeCase
    return decoder
}()
1
2
3
4
5

となれば問題となるのはローカル JSON を Data 型として取得するところだけである。

が、これを解決するのに結構時間がかかった。

Package.swift の編集

今回は読み込ませたいファイルをcoop.jsonとし、該当ファイルをパッケージのソースコードディレクトリ内のResourceディレクトリ内に配置した。

// swift-tools-version:5.3
// The swift-tools-version declares the minimum version of Swift required to build this package.
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"],
            resources: [.copy("Resources/coop.json")] // 追加
            ),
        .testTarget(
            name: "SalmonStatsTests",
            dependencies: ["SalmonStats", "CombineExpectations"],
            resources: [.copy("Resources/coop.json")]  // 追加
            )
    ]
)
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

注意点としてはcopyコマンドを使わないといけないという点。詳しくはApple のドキュメント (opens new window)を読めば書いてある。

JSON ファイル自体は多分どこでもいいんだろうけれど、Resourcesにおいておくのが無難ではないかと思っている。

ローカルファイル読み込み

// NG
guard let json = Bundle.main.url(forResource: "coop", withExtension: "json") else { return }
// NG
guard let json = Bundle.main.path(forResource: "coop", ofType: "json") else { return }
// OK
guard let json = Bundle.module.url(forResource: "coop", withExtension: "json") else { return }
1
2
3
4
5
6
7
8

いろいろ調べるとBundle.main.pathBundle.main.urlを使うように書いてあるがこれは Swift Package Manager では全く動かないのでいくら使ってもダメ。

というか、そもそもBundle.mainはライブラリで使うのは推奨されていないようだ。

Swift Package Manager の場合は必ずBundle.moduleで読み込ませること。そうしないと Swift Package Manager では常に nil が返ってきてファイル読み込みに失敗してしまう。

使い方

これで正しいのかはわからないのだが、ライブラリで実際に使うところまで実装してみた。

// 継承できないようにfinalでs値減する
public final class SalmonStats {
    // Singletonで宣言
    public static let shared = SalmonStats()
    private var task: AnyCancellable?
    // 一回だけ呼び出して再利用するのでstaticで呼び出す
    static var shift: [CoopShift] {
        get {
            guard let json = Bundle.module.url(forResource: "coop", withExtension: "json") else { return [] }
            guard let data = try? Data(contentsOf: json) else { return [] }
            let decoder: JSONDecoder = {
                let decoder = JSONDecoder()
                decoder.keyDecodingStrategy = .convertFromSnakeCase
                return decoder
            }()
            guard let shift = try? decoder.decode([CoopShift].self, from: data) else { return [] }
            return shift
        }
    }
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22

読み込みに失敗したら空っぽの配列を返すようにした。

まあ、実際にはライブラリ内で失敗することは想定されないので強制アンラップしてしまっても良いかもしれない。

ライブラリを改修する

Salmon Stats の API はリザルトを一件ずつ取得した場合には全部のデータが正しく入っているのだが、複数件同時取得の API を叩くと何故か startTime と playTime の二つしかスケジュール情報が入っていないという大問題(バグ?)がある。

このため、ステージ ID などもいちいちアプリ側でとってこなければいけないという仕様になっている。

これはライブラリ側で解決すべき問題だと考えているので、Salmon Stats ライブラリでは自動補完できるようにするのである。

完成したもの

いろいろあったが、無事にSalmon Statsライブラリ (opens new window)を完成させることができた。

詳しくはREADMEに書いてあるのだが、以下のAPIを叩いてそのレスポンスを整形した上で返してくれる。

内容 エンドポイント パラメータ
リザルト一件取得 results -
リザルト複数件取得 player/{nsaid}/results raw, count, page
シフト記録取得 schedules/{schedule_id} -
シフト統計取得 players/{nsaid}/schedules/{schedule_id} -
ユーザデータ取得 players/metadata ids
ユーザデータ概要複数取得 players/metadata ids
ユーザ検索 players/search name

ユーザデータ複数取得にいつの間にかAPIが対応していたのだが、この記事を書くまで気づかなかったのでライブラリ側でまだ対応できていない。

ただ、ユーザデータも複数件取得した場合にはいくつかのデータが抜け落ちた状態でレスポンスが返ってくる。

あと、絶対必要だと思っていたのだが普通に名前検索機能も忘れていた。数日中にアップデートする予定である。