[iOSアプリ開発] バージョン番号を比較する仕組みを作る

5 min読了の目安(約5000字TECH技術記事

こんにちは。
ZennではiOSアプリ開発の普段自分が使っているちょっとしたTipsなどを書いていければと思っております。

今回は 「バージョン番号の比較」 をやってみたいと思います。

「バージョン番号」とは1.0.0とか13.2など、主にアプリのバージョン表記に使われる番号のことを指します。

番号といっても途中にドットが入りますし、それは小数点とも違います。なので文字列として扱うことになります。

アプリのバージョン番号の比較ができることによって、たとえば強制アップデート機能や、バージョンごとに挙動を変えるなどの実装に役に立つかと思います。

バージョン番号

まずは、ご存じの方も多いとは思いますが、バージョン番号の考え方を簡単に振り返りましょう。

バージョン番号とは少なくともiOSアプリでは「リリース単位に振られる番号である」という解釈で間違いはないでしょう。
バージョン番号は左からドット区切りの数字に意味があります。その振り方はアプリの提供元によって定義は異なることも多々ありますが、おおよそは以下のようなルールで振られていると思います。

メジャーバージョン

一番左の数字です。
アプリ(プロダクト)の根本から変更する場合、または現在までのUIから大きく変わる場合に変更がなされます。

マイナーバージョン

左から二番目の数字です。
アプリに大きな機能追加や変更があった場合に変更がなされます。

リビジョン(リビジョンナンバー)

左から三番目の数字です。
アプリに軽微または大きめの不具合修正(バグフィックス)、小さな機能追加や変更があった場合に変更がなされます。
iOSアプリでは付けられないケースもあります。

ビルド(ビルドナンバー)

左から四番目の数字ですが、iOSアプリでは付けられないケースのほうが多いと思います。
大きなシステムであれば修正パッチごとに変更がなされます。

原則としては1ずつインクリメントされていきますが、リビジョンやビルドは都合によりスキップされるケースもあります。また、1.9.0の次は2.0.0にしなければならないといった10進数でマイナーからメジャーにあげるルールはありません。

基本は統一されているはずだが

同一のアプリでバージョン番号の形式が異なるということはあまり考えにくいですが、
他のサービスなどを使用するなどして別のフォーマットが混在してくる可能性はなくもありません。(避けたいところですが)

ver1.0.0ver1.0はどちらが大きなバージョン番号だろうという判定は、人間の目にはすぐわかってもプログラムとしては少し判定までのプロセスが必要になってきます。
フォーマットが同じであれば文字列比較でもある程度吸収できますが、思わぬバグを生み出すことも考えられます。今回はその確実性を持たせるためにVersionという構造体を作っていきます。

Version構造体

最初の定義はこうです。

import Foundation

struct Version {
    
    let versionNumber: String
    
    init(_ versionNumber: String) {
        self.versionNumber = versionNumber
    }
}

この場合、イニシャライザは定義しなくてもいいのですが、インスタンスを作るごとにversionNumber:と書くのもアレなので、ラベル省略できるように定義しています。

使い方

let version = Version("1.0.0")

Comparableに準拠

次にVersion構造体をComparableに準拠させます。
そしてComparableが備えなければならない <演算子に加え、Comparableが準拠するEquatableのための==演算子の定義も書きます。

extension Version: Comparable {
    
    static func == (lhs: Version, rhs: Version) -> Bool {
        return false
    }
    
    static func < (lhs: Version, rhs: Version) -> Bool {
        return false
    }
}

一旦ここは仮実装にして、この中身は後で書きます。

比較処理

前述の比較演算子の処理を完成させていきましょう。

2つのVersion構造体が持っているバージョン番号文字列同士を比較するために、まずはいくつかの細かいメソッドを定義していきます。

1つめは、ドットによって文字列を分割して、それぞれをInt型に変換して配列化するメソッドです。

    private static func splitByDot(_ versionNumber: String) -> [Int] {
        return versionNumber.split(separator: ".").map { string -> Int in
            return Int(string) ?? 0
        }
    }

"1.0.0"であれば、[1, 0, 0]"2.1"であれば、[2, 1]になる感じです。

2つめは、指定した要素数になるまでIntの配列を0で埋めるメソッドです。

    private static func filled(_ target: [Int], count: Int) -> [Int] {
        return (0..<count).map { i -> Int in
            (i < target.count) ? target[i] : 0
        }
    }

count:3にして[1]という配列を渡せば[1, 0, 0]に、[2, 1]を渡せば[2, 1, 0]してくれます。

これらを使って、2つのVersion構造体を比較するメソッドを実装します。

    private static func compare(lhs: Version, rhs: Version) -> ComparisonResult {
        var lhsComponents = splitByDot(lhs.versionNumber)
        var rhsComponents = splitByDot(rhs.versionNumber)
        
        let count = max(lhsComponents.count, rhsComponents.count)
        lhsComponents = filled(lhsComponents, count: count)
        rhsComponents = filled(rhsComponents, count: count)
        
        for i in 0..<count {
            let lhsComponent = lhsComponents[i]
            let rhsComponent = rhsComponents[i]
            
            if lhsComponent < rhsComponent {
                return .orderedDescending
            }
            if lhsComponent > rhsComponent {
                return .orderedAscending
            }
        }
        return .orderedSame
    }

配列の要素数を揃えた上で、1つ1つの数値を比較して、どちらが大きいか(または同じか)を判定します。左から(先頭から)一つでも差異があれば判定できるはずです。

ここまでできたら、先程のComparableEquatableで仮実装していたところを本実装していきます。

extension Version: Comparable {
    
    static func == (lhs: Version, rhs: Version) -> Bool {
-       return false
+       return compare(lhs: lhs, rhs: rhs) == .orderedSame
    }
    
    static func < (lhs: Version, rhs: Version) -> Bool {
-       return false
+       return compare(lhs: lhs, rhs: rhs) == .orderedDescending
    }
}

使い方

let ver1 = Version("1")
let ver2 = Version("1.0")
let ver3 = Version("2.0.1")

if ver1 == ver2 {
    print("同じバージョンです")
}
if ver2 < ver3 {
    print("ver3のほうが大きなバージョンです")
}

たとえばver1ver2は文字列比較するとfalseになると思いますが、このように同値として判定してくれます。

現在のアプリバージョン

下図のアプリターゲットの設定のversionをこのVersion構造体で取れるようにしておくと便利かなと思います。

info.plistからバージョン番号を取得することができるので、このように実装します。

extension Version {
    
    static var bundleShortVersion: Version {
        let versionNumber = Bundle.main.object(forInfoDictionaryKey: "CFBundleShortVersionString") as? String ?? ""
        return Version(versionNumber)
    }
}

使い方

冒頭でも書いたとおり、強制アップデート機能などでこのようにシンプルに書けると思います。

let forceUpdateVersion = Version("2.0.0")
if Version.bundleShortVersion < forceUpdateVersion {
    // 強制アップデートを行う
}

まとめ

  • バージョン番号同士の比較は、フォーマットが揃っていることが前提
  • 文字列比較だと思わぬバグを生むかも
  • フォーマットを揃えて安心に比較するために構造体を用意した
  • 現在のアプリバージョンをinfo.plistから取れる仕組みを作っておくと便利

というわけで、ではまた。