【Swift】アプリのバージョンの取得と比較を試してみた

2022.08.20

アプリのバージョンの取得と比較を試してみたので記事にしておこうと思います。

環境

  • Xcode 13.3

アプリバージョン取得

アプリバージョン取得は、Bundle.main.objectからCFBundleShortVersionStringキーを使用することで取得出来ます。

let currentAppVersionString = Bundle.main.object(forInfoDictionaryKey: "CFBundleShortVersionString") as? String

このバージョンは、TARGETS > General > Identity > Version の値を取得します。

アプリバージョンの比較

文字列で比較する

文字列でもcompareメソッドや比較演算子を使用して比較することが出来ます。

ただ、10.0.0のようなメジャーバージョンが二桁のケースと3.2.1を比較した場合に想定していた結果になりませんでした。

let requiredVersion = "10.0.0"
let currentVersion = "3.2.1"

if requiredVersion.compare(currentVersion) == .orderedDescending {
    print("requiredVersionの方が大きい")
} else {
    print("currentVersionの方が大きい")
}
// 出力 currentVersionの方が大きい

if requiredVersion > currentVersion  {
    print("requiredVersionの方が大きい")
} else {
    print("currentVersionの方が大きい")
}
// 出力 currentVersionの方が大きい

いずれも3.2.1の方が10.0.0より大きいという結果になってしまいました。

AppVersionで比較する

文字列比較では想定した結果にならなかった為、AppVersionという構造体を作成してアプリバージョンを比較出来るようにします。

前提条件

  • 渡されるバージョンはセマンティックバージョニングのものであること
  • 上記条件を満たさない場合はnilを返す

今回はこの条件にして1.0のようなセマンティックバージョニングでは無いものを許容しないようにしました。ですが、その時々の仕様に合わせて変更していただけたらと思います。

struct AppVersion

struct AppVersion {

    let versionString: String
    let majorVersion: Int
    let minorVersion: Int
    let patchVersion: Int

    init?(_ version: String) {
        let versionNumbers = version.components(separatedBy: ".").compactMap { Int($0) }
        if versionNumbers.count == 3 {
            versionString = version
            majorVersion = versionNumbers[0]
            minorVersion = versionNumbers[1]
            patchVersion = versionNumbers[2]
        } else {
            print("Does not meet the conditions of Semantic Versioning. App Version: \(version)")
            return nil
        }
    }
}

プロパティ

  • versionString: String
    • アプリバージョン文字列
  • majorVersion: Int
    • メジャーバージョン
  • minorVersion: Int
    • マイナーバージョン
  • patchVersion: Int
    • パッチバージョン

init

init時にString型のアプリバージョンを受け取り、そのバージョンを.区切りをして、その区切った値をInt型に変更しています。

セマンティックバージョニングで、メジャー、マイナー、パッチバージョンがある想定なので、.区切りで配列にした値の要素数が3では無い場合、今回はnilを返しています。

要素数が3つある場合は、それぞれの要素をそれぞれのバージョン番号の定数に渡しています。

比較する処理を実装

AppVersion型で比較演算子を用いて比較出来るようにする為、Comparebleに準拠させます。

ComparebleEquatableに準拠している為、==のメソッドも作成する必要があります。

// MARK: - AppVersion Comparable Functions
extension AppVersion: Comparable {

    // 左辺と右辺は等しい
    static func == (lhs: AppVersion, rhs: AppVersion) -> Bool {

        if lhs.majorVersion == rhs.majorVersion,
           lhs.minorVersion == rhs.minorVersion,
           lhs.patchVersion == rhs.patchVersion {
            return true
        }

        return false
    }

    // 左辺は右辺より小さい
    static func < (lhs: AppVersion, rhs: AppVersion) -> Bool {

        if lhs.majorVersion != rhs.majorVersion {
            return lhs.majorVersion < rhs.majorVersion
        }

        if lhs.minorVersion != rhs.minorVersion {
            return lhs.minorVersion < rhs.minorVersion
        }

        if lhs.patchVersion != rhs.patchVersion {
            return lhs.patchVersion < rhs.patchVersion
        }

        return false
    }

    // 左辺は右辺より大きい
    static func > (lhs: AppVersion, rhs: AppVersion) -> Bool {

        if lhs.majorVersion != rhs.majorVersion {
            return lhs.majorVersion > rhs.majorVersion
        }

        if lhs.minorVersion != rhs.minorVersion {
            return lhs.minorVersion > rhs.minorVersion
        }

        if lhs.patchVersion != rhs.patchVersion {
            return lhs.patchVersion > rhs.patchVersion
        }

        return false
    }

    // 左辺は右辺以下
    static func <= (lhs: AppVersion, rhs: AppVersion) -> Bool {

        if lhs == rhs {
            return true
        }

        return lhs < rhs
    }

    // 左辺は右辺以上
    static func >= (lhs: AppVersion, rhs: AppVersion) -> Bool {

        if lhs == rhs {
            return true
        }

        return lhs > rhs
    }
}

左辺と右辺は等しい

メジャーバージョンからパッチバージョンまでの全てのバージョン番号が一致する場合は、trueを返しています。そうではない場合はfalseを返しています。

static func == (lhs: AppVersion, rhs: AppVersion) -> Bool {

    if lhs.majorVersion == rhs.majorVersion,
       lhs.minorVersion == rhs.minorVersion,
       lhs.patchVersion == rhs.patchVersion {
        return true
    }

    return false
}

左辺は右辺より大きい

まずはメジャーバージョン同士で比較を行い、等しい場合は、マイナーバージョンとパッチバージョンの比較に移っていきます。バージョンの比較で等しく無い場合の時だけそのバージョン同士で左辺が右辺より大きいかどうかの判定をしています。

左辺は右辺より小さいかどうかの判定の場合も処理内容はほぼ同じで、バージョンの比較でイコールでは無い場合の時だけそのバージョン同士で左辺は右辺より小さいかどうかの判定をしています。

static func > (lhs: AppVersion, rhs: AppVersion) -> Bool {

    if lhs.majorVersion != rhs.majorVersion {
        return lhs.majorVersion > rhs.majorVersion
    }

    if lhs.minorVersion != rhs.minorVersion {
        return lhs.minorVersion > rhs.minorVersion
    }

    if lhs.patchVersion != rhs.patchVersion {
        return lhs.patchVersion > rhs.patchVersion
    }

    return false
}

左辺は右辺以下、左辺は右辺以上

等しいかどうかの判定と、より大きい、またはより小さいの判定を組み合わせて比較結果を出します。

static func <= (lhs: AppVersion, rhs: AppVersion) -> Bool {

    if lhs == rhs {
        return true
    }

    return lhs < rhs
}

static func >= (lhs: AppVersion, rhs: AppVersion) -> Bool {

    if lhs == rhs {
        return true
    }

    return lhs > rhs
}

比較結果

10.0.03.2.1を比較した時に無事にrequiredVersion(10.0.0)の方が大きいという結果を得ることが出来ました。

guard let requiredVersion = AppVersion("10.0.0"),
      let currentVersion = AppVersion("3.2.1") else { return }

if requiredVersion > currentVersion  {
    print("requiredVersionの方が大きい")
} else {
    print("currentVersionの方が大きい")
}

// 出力 requiredVersionの方が大きい

おわりに

文字列でも比較は出来るが、想定外の結果になるパターンがあることが分かりました。比較をするクラスや構造体を作成するには、Comparableに準拠させましょう!

lhsrhsってなんだよ!と思ったら、left hand side(左辺)とright hand side(右辺)の略語でした。

この記事が誰かの助けになればと思います。

参考