記事

iOS APP 版本號管理|ルール解説と比較で最適解を導く

iOSアプリ開発者向けに、バージョン番号のルールと比較方法を明確化。複雑な判定を簡素化し、正確なバージョン管理でリリースミスを防止します。効率的な運用を実現し、開発品質を向上させる具体的な解決策を紹介。

iOS APP 版本號管理|ルール解説と比較で最適解を導く

本記事は AI による翻訳をもとに作成されています。表現が不自然な箇所がありましたら、ぜひコメントでお知らせください。

記事一覧


iOS APP バージョン番号について

バージョン番号の規則と判定比較の解決策

Photo by [James Yarema](https://unsplash.com/@jamesyarema?utm_source=unsplash&utm_medium=referral&utm_content=creditCopyText){:target="_blank"}

Photo by James Yarema

前書き

すべての iOS アプリ開発者が必ず遭遇する2つの数字、Version Number と Build Number;最近ちょうどバージョン番号に関する要件があり、バージョン番号を使ってユーザーにアプリの評価をお願いする判定を行う必要がありました。それを機にバージョン番号について調べてみました。記事の最後には私のバージョン番号判定の完全解決策も掲載しています。

[XCode ヘルプ](https://help.apple.com/xcode/mac/current/#/devba7f53ad4){:target="_blank"}

XCode Help

セマンティックバージョン x.y.z

まず「セマンティックバージョニング」という規格を紹介します。これはソフトウェアの依存関係や管理の問題を解決するためのもので、私たちがよく使うCocoapodsにも関係します。例えば、私がMoya 4.0を使用しているとします。Moya 4.0はAlamofire 2.0.0に依存しています。もしAlamofireが更新された場合、それは新機能の追加かもしれませんし、問題の修正かもしれません。または互換性のない大幅な変更かもしれません。このとき、バージョン番号に共通のルールがなければ、どのバージョンが互換性があり、更新可能なのか分からず混乱してしまいます。

セマンティックバージョンは3つの部分で構成されます: x.y.z

  • x: メジャーバージョン (major):互換性のないAPI変更を行った場合

  • y: マイナーバージョン (minor):後方互換性のある機能追加を行った場合

  • z: パッチ番号 (patch):下位互換の問題修正を行った場合

共通ルール:

  • 非負の整数でなければなりません

  • ゼロパディングしない

  • 0.y.z は開発初期段階を示し、正式版のバージョン番号には使用すべきではありません。

  • 数値で増加

比較方法:

まずメジャーバージョンを比較し、メジャーバージョンが同じ場合はマイナーバージョンを比較し、マイナーバージョンも同じ場合はパッチバージョンを比較します。

例:1.0.0 < 2.0.0 < 2.1.0 < 2.1.1

また、修正番号の後に「プレリリース情報(例: 1.0.1-alpha)」や「ビルドメタデータ(例: 1.0.0-alpha+001)」を追加することも可能ですが、iOSアプリのバージョン番号はこれらの形式でApp Storeにアップロードすることができません。詳細は「語意化版本」をご参照ください。

✅:1.0.1、1.0.0、5.6.7
❌:01.5.6、a1.2.3、2.005.6

実際の使用

iOSアプリのバージョン管理に実際に使用する場合、リリース版のバージョンを示すだけであり、他のアプリやソフトウェアとの依存関係はないため、実際の運用では各チームが独自に定義しています。以下はあくまで個人的な考えです:

  • x: メジャーバージョン (major):大幅な更新時(複数ページのUI刷新、主要機能のリリース)

  • y: マイナーバージョン (minor):既存機能の最適化や強化時(大きな機能の下の小機能追加)

  • z: パッチバージョン (patch):現在のバージョンのバグを修正するとき

一般的に緊急修正(Hot Fix)の場合のみ修正番号を変更し、通常は0のままにします。新しいバージョンをリリースする際には0に戻すことができます。

例:初版リリース(1.0.0) -> 初版の機能強化(1.1.0) -> 問題発見による修正(1.1.1) -> 再度問題発見(1.1.2) -> 初版機能のさらなる強化(1.2.0) -> 大幅リニューアル(2.0.0) -> 問題発見による修正(2.0.1) … 以下同様

バージョン番号とビルド番号の違い

Version Number (APP バージョン番号)

  • App Store、外部識別用

  • プロパティリストキー: CFBundleShortVersionString

  • 内容は数字と「.」のみで構成してください

  • 公式でもセマンティックバージョニングの x.y.z 形式の使用が推奨されています

  • 2020121701、2.0、2.0.0.1 のいずれも可
    (以下にApp Store上のアプリバージョン番号の命名方法の一覧を示します)

  • 18文字以内でお願いします

  • フォーマットが合わなくてもビルド&実行は可能ですが、App Storeへのアップロードはできません

  • 上方向への増加のみ可能で、重複や減少は不可

一般的にセマンティックバージョニングの x.y.z または x.y を使用します。

ビルド番号

  • 内部開発プロセスや段階の識別に使用し、ユーザーには公開されません。

  • App Storeへのアップロード識別に使用(同じビルド番号では再アップロード不可)

  • プロパティリストキー: CFBundleVersion

  • 内容は数字と「.」のみで構成されていなければなりません

  • 公式でもセマンティックバージョニングの x.y.z 形式の使用が推奨されています

  • 1、2020121701、2.0、2.0.0.1 どれでも可

  • 18文字以内でお願いします

  • フォーマットが合っていないと、ビルド&実行はできるがApp Storeへのアップロードはできません。

  • 同じAPPのバージョン番号は重複できませんが、異なるAPPのバージョン番号は重複可能です。
    例: 1.0.0 build: 1.0.0、1.1.0 build: 1.0.0 ✅

一般的には日付や番号(各新バージョンは0から開始)を使い、CIやfastlaneと連携してビルド番号を自動でインクリメントします。

ランキング上のアプリのバージョン番号の形式を少し集計しました。上の図の通りです。

一般的には x.y.z を主に使います。

バージョン番号の比較と判定方法

時々バージョンを使って判定する必要があります。例えば、x.y.z バージョン未満なら強制更新を促す、特定のバージョンと等しい場合に評価の招待を行う、といった場合です。このような時に、2つのバージョン文字列を比較する機能が必要になります。

簡単な方法

1
2
3
4
5
let version = "1.0.0"
print(version.compare("1.0.0", options: .numeric) == .orderedSame) // true 1.0.0 = 1.0.0
print(version.compare("1.22.0", options: .numeric) == .orderedAscending) // true 1.0.0 < 1.22.0
print(version.compare("0.0.9", options: .numeric) == .orderedDescending) // true 1.0.0 > 0.0.9
print(version.compare("2", options: .numeric) == .orderedAscending) // true 1.0.0 < 2

String Extensionも書けます:

1
2
3
4
5
extension String {
    func versionCompare(_ otherVersion: String) -> ComparisonResult {
        return self.compare(otherVersion, options: .numeric) // 数値オプションで比較する
    }
}

⚠️しかし、形式が異なる場合に同じかどうかを判断すると誤りが生じる可能性があります:

1
2
let version = "1.0.0"
version.compare("1", options: .numeric) //.orderedDescending

実際には 1 == 1.0.0 ですが、この方法で判定すると .orderedDescending になります。詳しくはこちらの記事のゼロ補完してから判定する方法を参照してください。通常はAPPのバージョン形式を決めたら変えずに、x.y.z をずっと使い続け、x.y.z と x.y を混在させないようにしましょう。

複雑な方法

既存のライブラリをそのまま使用可能: mrackwitz/Version 以下は自作の実装です。

複雑な方法では、セマンティックバージョニング x.y.z をフォーマット規則として使用し、独自に正規表現で文字列を解析し比較演算子を実装しています。基本的な =/>/≥/</≤ に加え、~> 演算子(Cocoapodsのバージョン指定方法と同様)も実装しており、静的入力にも対応しています。

~> 演算子の定義は:

このバージョン以上で、かつ(前の階層のバージョン番号+1)未満

1
2
3
4
EX:
~> 1.2.1: (1.2.1 <= バージョン < 1.3) 1.2.3,1.2.4...
~> 1.2: (1.2 <= バージョン < 2) 1.3,1.4,1.5,1.3.2,1.4.1...
~> 1: (1 <= バージョン < 2) 1.1.2,1.2.3,1.5.9,1.9.0...
  1. まずはVersionオブジェクトを定義する必要があります:
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
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
@objcMembers
class Version: NSObject {
    private(set) var major: Int
    private(set) var minor: Int
    private(set) var patch: Int

    override var description: String {
        return "\(self.major),\(self.minor),\(self.patch)"
    }

    init(_ major: Int, _ minor: Int, _ patch: Int) {
        self.major = major
        self.minor = minor
        self.patch = patch
    }

    init(_ string: String) throws {
        let result = try Version.parse(string: string)
        self.major = result.version.major
        self.minor = result.version.minor
        self.patch = result.version.patch
    }

    static func parse(string: String) throws -> VersionParseResult {
        let regex = "^(?:(>=\\|>\\|<=\\|<\\|~>\\|=\\|!=){1}\\s*)?(0\\|[1-9]\\d*)\\.(0\\|[1-9]\\d*)\\.(0\\|[1-9]\\d*)$"
        let result = string.groupInMatches(regex)

        if result.count == 4 {
            // 演算子で始まる場合...
            let versionOperator = VersionOperator(string: result[0])
            guard versionOperator != .unSupported else {
                throw VersionUnSupported()
            }
            let major = Int(result[1]) ?? 0
            let minor = Int(result[2]) ?? 0
            let patch = Int(result[3]) ?? 0
            return VersionParseResult(versionOperator, Version(major, minor, patch))
        } else if result.count == 3 {
            // 演算子指定なしの場合...
            let major = Int(result[0]) ?? 0
            let minor = Int(result[1]) ?? 0
            let patch = Int(result[2]) ?? 0
            return VersionParseResult(.unSpecified, Version(major, minor, patch))
        } else {
            throw VersionUnSupported()
        }
    }
}

// 対応するオブジェクト
@objc class VersionUnSupported: NSObject, Error { }

@objc enum VersionOperator: Int {
    case equal
    case notEqual
    case higherThan
    case lowerThan
    case lowerThanOrEqual
    case higherThanOrEqual
    case optimistic

    case unSpecified
    case unSupported

    init(string: String) {
        switch string {
        case ">":
            self = .higherThan
        case "<":
            self = .lowerThan
        case "<=":
            self = .lowerThanOrEqual
        case ">=":
            self = .higherThanOrEqual
        case "~>":
            self = .optimistic
        case "=":
            self = .equal
        case "!=":
            self = .notEqual
        default:
            self = .unSupported
        }
    }
}

@objcMembers
class VersionParseResult: NSObject {
    var versionOperator: VersionOperator
    var version: Version
    init(_ versionOperator: VersionOperator, _ version: Version) {
        self.versionOperator = versionOperator
        self.version = version
    }
}

Version は major, minor, patch を格納するものであり、解析方法は static として外部から呼び出しやすくしています。1.0.0≥1.0.1 のような形式を渡せるため、文字列解析や設定ファイルの解析に便利です。

1
2
Input: 1.0.0 => Output: .unSpecified, Version(1.0.0)
Input: ≥ 1.0.1 => Output: .higherThanOrEqual, Version(1.0.0)

Regex は「 語意化バージョン文書 」で提供されているRegexを参考に修正したものです:

1
^(0\\|[1-9]\d*)\.(0\\|[1-9]\d*)\.(0\\|[1-9]\d*)(?:-((?:0\\|[1-9]\d*\\|\d*[a-zA-Z-][0-9a-zA-Z-]*)(?:\.(?:0\\|[1-9]\d*\\|\d*[a-zA-Z-][0-9a-zA-Z-]*))*))?(?:\+([0-9a-zA-Z-]+(?:\.[0-9a-zA-Z-]+)*))?$

プロジェクトがObjective-Cと混在しているため、OCでも使用できるようにすべて@objcMembersで宣言し、OC互換の書き方を妥協して採用しています。

(実際には VersionOperator は enum: String を使い、Result は tuple/struct を使うこともできます)

もし実装するオブジェクトが NSObject を継承している場合、Comparable/Equatable の == を実装するときは必ず != も実装してください。元の NSObject の != 演算子は期待通りに動作しません。

2. Comparable メソッドの実装:

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
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
extension Version: Comparable {
    static func < (lhs: Version, rhs: Version) -> Bool {
        if lhs.major < rhs.major {
            return true
        } else if lhs.major == rhs.major {
            if lhs.minor < rhs.minor {
                return true
            } else if lhs.minor == rhs.minor {
                if lhs.patch < rhs.patch {
                    return true
                }
            }
        }

        return false
    }

    static func == (lhs: Version, rhs: Version) -> Bool {
        return lhs.major == rhs.major && lhs.minor == rhs.minor && lhs.patch == rhs.patch
    }

    static func != (lhs: Version, rhs: Version) -> Bool {
        return !(lhs == rhs)
    }

    static func ~> (lhs: Version, rhs: Version) -> Bool {
        let start = Version(lhs.major, lhs.minor, lhs.patch)
        let end = Version(lhs.major, lhs.minor, lhs.patch)

        if end.patch >= 0 {
            end.minor += 1
            end.patch = 0
        } else if end.minor > 0 {
            end.major += 1
            end.minor = 0
        } else {
            end.major += 1
        }
        return start <= rhs && rhs < end
    }

    func compareWith(_ version: Version, operator: VersionOperator) -> Bool {
        switch `operator` {
        case .equal, .unSpecified:
            return self == version
        case .notEqual:
            return self != version
        case .higherThan:
            return self > version
        case .lowerThan:
            return self < version
        case .lowerThanOrEqual:
            return self <= version
        case .higherThanOrEqual:
            return self >= version
        case .optimistic:
            return self ~> version
        case .unSupported:
            return false
        }
    }
}

実際には前述の判定ロジックを実装し、最後に compareWith メソッドを用意して、外部から解析結果を直接渡して最終判定を得られるようにしています。

使用例:

1
2
3
4
5
6
7
8
let shouldAskUserFeedbackVersion = ">= 2.0.0"
let currentVersion = "3.0.0"
do {
  let result = try Version.parse(shouldAskUserFeedbackVersion)
  result.version.comparWith(currentVersion, result.operator) // true
} catch {
  print("バージョン文字列の解析エラーです!")
}

または…

1
Version(1,0,0) >= Version(0,0,9) //true...

>/≥/</≤/=/!=/~> 演算子をサポートしています。

次のステップ

テストケース…

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
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
import XCTest

class VersionTests: XCTestCase {
    func testHigher() throws {
        let version = Version(3, 12, 1)
        XCTAssertEqual(version > Version(2, 100, 120), true)
        XCTAssertEqual(version > Version(3, 12, 0), true)
        XCTAssertEqual(version > Version(3, 10, 0), true)
        XCTAssertEqual(version >= Version(3, 12, 1), true)

        XCTAssertEqual(version > Version(3, 12, 1), false)
        XCTAssertEqual(version > Version(3, 12, 2), false)
        XCTAssertEqual(version > Version(4, 0, 0), false)
        XCTAssertEqual(version > Version(3, 13, 1), false)
    }

    func testLower() throws {
        let version = Version(3, 12, 1)
        XCTAssertEqual(version < Version(2, 100, 120), false)
        XCTAssertEqual(version < Version(3, 12, 0), false)
        XCTAssertEqual(version < Version(3, 10, 0), false)
        XCTAssertEqual(version <= Version(3, 12, 1), true)

        XCTAssertEqual(version < Version(3, 12, 1), false)
        XCTAssertEqual(version < Version(3, 12, 2), true)
        XCTAssertEqual(version < Version(4, 0, 0), true)
        XCTAssertEqual(version < Version(3, 13, 1), true)
    }

    func testEqual() throws {
        let version = Version(3, 12, 1)
        XCTAssertEqual(version == Version(3, 12, 1), true)
        XCTAssertEqual(version == Version(3, 12, 21), false)
        XCTAssertEqual(version != Version(3, 12, 1), false)
        XCTAssertEqual(version != Version(3, 12, 2), true)
    }

    func testOptimistic() throws {
        let version = Version(3, 12, 1)
        XCTAssertEqual(version ~> Version(3, 12, 1), true) //3.12.1 <= $0 < 3.13.0
        XCTAssertEqual(version ~> Version(3, 12, 9), true) //3.12.1 <= $0 < 3.13.0
        XCTAssertEqual(version ~> Version(3, 13, 0), false) //3.12.1 <= $0 < 3.13.0
        XCTAssertEqual(version ~> Version(3, 11, 1), false) //3.12.1 <= $0 < 3.13.0
        XCTAssertEqual(version ~> Version(3, 13, 1), false) //3.12.1 <= $0 < 3.13.0
        XCTAssertEqual(version ~> Version(2, 13, 0), false) //3.12.1 <= $0 < 3.13.0
        XCTAssertEqual(version ~> Version(3, 11, 100), false) //3.12.1 <= $0 < 3.13.0
    }

    func testVersionParse() throws {
        let unSpecifiedVersion = try? Version.parse(string: "1.2.3")
        XCTAssertNotNil(unSpecifiedVersion)
        XCTAssertEqual(unSpecifiedVersion!.version == Version(1, 2, 3), true)
        XCTAssertEqual(unSpecifiedVersion!.versionOperator, .unSpecified)

        let optimisticVersion = try? Version.parse(string: "~> 1.2.3")
        XCTAssertNotNil(optimisticVersion)
        XCTAssertEqual(optimisticVersion!.version == Version(1, 2, 3), true)
        XCTAssertEqual(optimisticVersion!.versionOperator, .optimistic)

        let higherThanVersion = try? Version.parse(string: "> 1.2.3")
        XCTAssertNotNil(higherThanVersion)
        XCTAssertEqual(higherThanVersion!.version == Version(1, 2, 3), true)
        XCTAssertEqual(higherThanVersion!.versionOperator, .higherThan)

        XCTAssertThrowsError(try Version.parse(string: "!! 1.2.3")) { error in
            XCTAssertEqual(error is VersionUnSupported, true)
        }
    }
}

現在、Versionをさらに最適化し、パフォーマンステストを調整し、パッケージを整理してから、自分のcocoapodsのプロセスを一度実行する予定です。

ただし、現在はすでに非常に完成度の高い Version がPodプロジェクトで利用されているため、わざわざ再作成する必要はなく、単にビルドプロセスを整理したいだけですXD。

既存のライブラリに対して ~> の実装をPRとして提出することもあるかもしれません。

参考資料:

ご質問やご意見がありましたら、こちらからご連絡ください

Post は Medium から ZMediumToMarkdown によって変換されました。


🍺 Buy me a beer on PayPal

👉👉👉 Follow Me On Medium! (1,053+ Followers) 👈👈👈

本記事は Medium にて初公開されました(こちらからオリジナル版を確認)。ZMediumToMarkdown による自動変換・同期技術を使用しています。

Improve this page on Github.

本記事は著者により CC BY 4.0 に基づき公開されています。

© ZhgChgLi. All rights reserved.
閲覧数: 802,415+, 最終更新日時: 2026-01-15 11:14:58 +08:00

本サイトは Chirpy テーマを使用し、Jekyll 上で構築されています。
Medium の記事は ZMediumToMarkdown により変換されています。