バージョン取得ユーティリティの実装例
iOSアプリの現在のバージョンを取得するコードは以下のように実装できます。
Bundle.main.infoDictionary!["CFBundleShortVersionString"] as! String // いわゆる「アプリバージョン」
Bundle.main.infoDictionary!["CFBundleVersion"] as! String // いわゆる「ビルドバージョン」
これらをまとめたユーティリティは例えば以下のように実装できます。
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]
}
}
AppVersion.appVersion() // ex: 3.4.0
AppVersion.buildVersion() // ex: 2100
フレームワークでユニットテストを実行すると意図しない結果になる
最初はこれでよかったのですが、後からこのユーティリティをフレームワークに移動し、ユニットテストを書こうとしたら問題が出てきました。
ここで、フレームワークのアプリバージョンが3.4.0
、ビルドバージョンが2100
だとします。
※ユニットテストはQuick/Nimbleで書いてます。
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))
}
}
}
}
上記のテストは失敗します。
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)
一方、Bundle(for: AppVersionSpec.self)
をprintしてみると、以下のパスが出力されました。
NSBundle </Users/takehilo/Library/Developer/Xcode/DerivedData/Sample-gfdtgwqbgmitwoacobrheufigmgd/Build/Products/Debug-iphonesimulator/CommonExtensionTests.xctest> (loaded)
どうやら先ほどの1.3.1
というのはXcodeのバージョンを指しているようです。フレームワークのユニットテストを実装したときのBundle.mainはフレームワークではなく特別に用意されたバンドルになるようですね。
そこで、バージョンの取得対象となるバンドルを指定できるように、AppVersionを改良しました。
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]
}
}
プロダクションコードで使用する際は基本的にアプリ本体のバージョンを取得したいケースがほとんどなので、デフォルトでBundle.mainになるようにしています。
以下のように、引数に特に何も指定せずに実行するとアプリ本体のバージョンを取得できます。
AppVersion.appVersion()
一方、ユニットテストを実行する際は、以下のようにすることでユニットテストターゲットのバージョンを取得させることが可能です。
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))
}
}
}
}
AppVersionSpec.selfを指定することで、ユニットテストターゲットのバージョンを取得できます。
私の環境では、フレームワークのバージョンは更新していきますが、ユニットテストターゲットのバージョンは更新しません。なのであえてユニットテストターゲットのバージョンでテストすることで、バージョン更新のたびにユニットテストを修正するという手間がかからないようにしています。