iOSアプリバージョンを取得するユーティリティ

# バージョン取得ユーティリティの実装例

iOSアプリの現在のバージョンを取得するコードは以下のように実装できます。

```swift
Bundle.main.infoDictionary!["CFBundleShortVersionString"] as! String  // いわゆる「アプリバージョン」
Bundle.main.infoDictionary!["CFBundleVersion"] as! String  // いわゆる「ビルドバージョン」
```

<br>

これらをまとめたユーティリティは例えば以下のように実装できます。

```swift
public enum AppVersion {
    public static func appVersion() -> String {
        info(key: "CFBundleShortVersionString") as! String
    }

    public static func buildVersion() -> String {
        info(key: "CFBundleShortVersionString") as! String
    }

    private static func info(key: String) -> Any? {
        Bundle.main.infoDictionary![key]
    }
}
```

```swift
AppVersion.appVersion()  // ex: 3.4.0
AppVersion.buildVersion()  // ex: 2100
```

# フレームワークでユニットテストを実行すると意図しない結果になる
最初はこれでよかったのですが、後からこのユーティリティをフレームワークに移動し、ユニットテストを書こうとしたら問題が出てきました。

ここで、フレームワークのアプリバージョンが`3.4.0`、ビルドバージョンが`2100`だとします。

※ユニットテストはQuick/Nimbleで書いてます。

```swift
class AppVersionSpec: QuickSpec {
    override func spec() {
        describe("appVersion()") {
            it("アプリバージョンを返すこと") {
                expect(AppVersion.appVersion()).to(equal("3.4.0"))
            }
        }

        describe("buildVersion()") {
            it("ビルドバージョンを返すこと") {
                expect(AppVersion.buildVersion()).to(equal(2100))
            }
        }
    }
}
```

<br>

上記のテストは失敗します。

`3.4.0`を期待しているところでは`1.3.1`（環境に依ります）、`2100`を期待しているところでは`20076`（環境に依ります）になるのです。

# 任意のバンドルを指定できるように改良する
どうやら、AppVersionの実装でメインバンドル（`Bundle.main`）を参照しているのが原因のようです。

ユニットテストの実装内で`Bundle.main`をprintしてみると、以下のパスが出力されました。

```
NSBundle </Applications/Xcode-13.3.1.app/Contents/Developer/Platforms/iPhoneSimulator.platform/Developer/Library/Xcode/Agents> (loaded)
```

<br>

一方、`Bundle(for: AppVersionSpec.self)`をprintしてみると、以下のパスが出力されました。

```
NSBundle </Users/takehilo/Library/Developer/Xcode/DerivedData/Sample-gfdtgwqbgmitwoacobrheufigmgd/Build/Products/Debug-iphonesimulator/CommonExtensionTests.xctest> (loaded)
```

<br>

どうやら先ほどの`1.3.1`というのはXcodeのバージョンを指しているようです。フレームワークのユニットテストを実装したときのBundle.mainはフレームワークではなく特別に用意されたバンドルになるようですね。

<br>
<br>

そこで、バージョンの取得対象となるバンドルを指定できるように、AppVersionを改良しました。

```swift
public enum AppVersion {
    public static func appVersion(of bundleClass: AnyClass? = nil) -> String {
        info(of: bundleClass, key: "CFBundleShortVersionString") as! String
    }

    public static func buildVersion(of bundleClass: AnyClass? = nil) -> String {
        info(of: bundleClass, key: "CFBundleVersion") as! String
    }

    private static func info(of bundleClass: AnyClass? = nil, key: String) -> Any? {
        let bundle = bundleClass.map { Bundle(for: $0) } ?? Bundle.main
        return bundle.infoDictionary![key]
    }
}
```

<br>

プロダクションコードで使用する際は基本的にアプリ本体のバージョンを取得したいケースがほとんどなので、デフォルトでBundle.mainになるようにしています。

以下のように、引数に特に何も指定せずに実行するとアプリ本体のバージョンを取得できます。

```swift
AppVersion.appVersion()
```

<br>

一方、ユニットテストを実行する際は、以下のようにすることでユニットテストターゲットのバージョンを取得させることが可能です。

```swift
class AppVersionSpec: QuickSpec {
    override func spec() {
        describe("appVersion(of:)") {
            it("アプリバージョンを返すこと") {
                expect(AppVersion.appVersion(of: AppVersionSpec.self)).to(equal("1.0.0"))
            }
        }

        describe("buildVersion(of:)") {
            it("ビルドバージョンを返すこと") {
                expect(AppVersion.buildVersion(of: AppVersionSpec.self)).to(equal(1))
            }
        }
    }
}
```

<br>

AppVersionSpec.selfを指定することで、ユニットテストターゲットのバージョンを取得できます。

私の環境では、フレームワークのバージョンは更新していきますが、ユニットテストターゲットのバージョンは更新しません。なのであえてユニットテストターゲットのバージョンでテストすることで、バージョン更新のたびにユニットテストを修正するという手間がかからないようにしています。