Home iOS APP Version Numbers Explained
Post
Cancel

iOS APP Version Numbers Explained

iOS APP Version Numbers Explained

Version number rules and comparison solutions

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

Photo by James Yarema

Introduction

All iOS APP developers will encounter two numbers, Version Number and Build Number; recently, I had a requirement related to version numbers, to prompt users to rate the APP, and I took the opportunity to explore version numbers; at the end of the article, I will also provide my comprehensive solution for version number comparison.

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

XCode Help

Semantic Versioning x.y.z

First, let’s introduce the “ Semantic Versioning “ specification, which mainly addresses software dependency and management issues, such as the commonly used Cocoapods; suppose I use Moya 4.0 today, Moya 4.0 uses and depends on Alamofire 2.0.0. If Alamofire is updated, it could be a new feature, a bug fix, or a complete overhaul (incompatible with the old version); without a common consensus on version numbers, it would be chaotic because you wouldn’t know which version is compatible and updatable.

Semantic versioning consists of three parts: x.y.z

  • x: Major version (major): When you make incompatible API changes
  • y: Minor version (minor): When you add functionality in a backward-compatible manner
  • z: Patch version (patch): When you make backward-compatible bug fixes

General rules:

  • Must be non-negative integers
  • No leading zeros
  • 0.y.z indicates the initial development phase and should not be used for official version numbers
  • Increment numerically

Comparison method:

First compare the major version, if the major version is equal, then compare the minor version, if the minor version is equal, then compare the patch version.

ex: 1.0.0 < 2.0.0 < 2.1.0 < 2.1.1

Additionally, you can add “pre-release version information (ex: 1.0.1-alpha)” or “build metadata (ex: 1.0.0-alpha+001)” after the patch version, but iOS APP version numbers do not allow these formats to be uploaded to the App Store, so they will not be elaborated here. For details, refer to “ Semantic Versioning “.

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

Practical Use

Regarding practical use in iOS APP version control, since we only use it as a marker for the release APP version and there are no dependency issues with other APPs or software; the actual usage definition is up to each team. The following is just my personal opinion:

  • x: Major version (major): For significant updates (multiple page interface overhauls, major feature launches)
  • y: Minor version (minor): For optimizing and enhancing existing features (adding small features under a major feature)
  • z: Patch version (patch): For fixing bugs in the current version

Generally, the revision number is only changed for emergency fixes (Hot Fix), and under normal circumstances, it remains 0. If a new version is released, it can be reset to 0.

EX: First version release (1.0.0) -> Strengthen the first version’s features (1.1.0) -> Found an issue to fix (1.1.1) -> Found another issue (1.1.2) -> Continue to strengthen the first version’s features (1.2.0) -> Major update (2.0.0) -> Found an issue to fix (2.0.1) … and so on

Version Number vs. Build Number

Version Number (APP Version Number)

  • Used for App Store and external identification
  • Property List Key: CFBundleShortVersionString
  • Content can only consist of numbers and “.”
  • Officially recommended to use semantic versioning x.y.z format
  • 2020121701, 2.0, 2.0.0.1 are all acceptable (A summary of App Store app version naming conventions will be provided below)
  • Cannot exceed 18 characters
  • If the format is incorrect, you can build & run but cannot package and upload to the App Store
  • Can only increment, cannot repeat, cannot decrement

It is generally customary to use semantic versioning x.y.z or x.y.

Build Number

  • Used for internal development process and stage identification, not disclosed to users
  • Used for identification when packaging and uploading to the App Store (the same build number cannot be packaged and uploaded repeatedly)
  • Property List Key: CFBundleVersion
  • Content can only consist of numbers and “.”
  • Officially recommended to use semantic versioning x.y.z format
  • 1, 2020121701, 2.0, 2.0.0.1 are all acceptable
  • Cannot exceed 18 characters
  • If the format is incorrect, you can build & run but cannot package and upload to the App Store
  • Cannot repeat under the same APP version number, but can repeat under different APP version numbers ex: 1.0.0 build: 1.0.0, 1.1.0 build: 1.0.0 ✅

It is generally customary to use dates, numbers (starting from 0 for each new version), and use CI/fastlane to automatically increment the build number during packaging.

A brief survey of the version number formats of apps on the leaderboard, as shown in the image above.

Generally, x.y.z is still the main format.

Version Number Comparison and Judgment

Sometimes we need to use version numbers for judgment, for example: force update if below x.y.z version, prompt for rating if equal to a certain version. In such cases, we need a function to compare two version strings.

Simple Method

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

You can also write a String Extension:

1
2
3
4
5
extension String {
    func versionCompare(_ otherVersion: String) -> ComparisonResult {
        return self.compare(otherVersion, options: .numeric)
    }
}

⚠️ However, note that if the formats are different, the judgment will be incorrect:

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

In reality, we know 1 == 1.0.0, but using this method will result in .orderedDescending; you can refer to this article for padding with 0 before comparing; under normal circumstances, once we decide on an APP version format, it should not change. If using x.y.z, stick with x.y.z, do not switch between x.y.z and x.y.

Complex Method

Can directly use the existing wheel: mrackwitz/Version Below is the recreation of the wheel.

The complex method here follows the semantic versioning x.y.z as the format specification, using Regex for string parsing and implementing comparison operators by ourselves. In addition to the basic =/>/≥/< /≤, we also implemented the ~> operator (same as the Cocoapods version specification method) and support static input.

The definition of the ~> operator is:

Greater than or equal to this version but less than this version’s (previous level version number +1)

1
2
3
4
EX:
~> 1.2.1: (1.2.1 <= version < 1.3) 1.2.3,1.2.4...
~> 1.2: (1.2 <= version < 2) 1.3,1.4,1.5,1.3.2,1.4.1...
~> 1: (1 <= version < 2) 1.1.2,1.2.3,1.5.9,1.9.0...
  1. First, we need to define the Version object:
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 {
            //start with operator...
            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 {
            //unSpecified operator...
            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()
        }
    }
}

//Supported Objects
@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
    }
}

You can see that Version is a storage for major, minor, and patch, and the parsing method is written as static for external calls. It can accept formats like 1.0.0 or ≥1.0.1, making it convenient for string parsing and configuration file parsing.

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

The Regex is modified based on the Regex provided in the “Semantic Versioning Specification”:

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-]+)*))?$

*Considering the project is mixed with Objective-C, it should be usable in OC as well, so everything is declared as @objcMembers, and compromises are made to use OC-compatible syntax.

(Actually, you can directly use VersionOperator with enum: String, and Result with tuple/struct)

*If the implemented object is derived from NSObject, remember to implement != when implementing Comparable/Equatable ==, as the original NSObject’s != operation will not yield the expected result.

2. Implement Comparable methods:

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
        }
    }
}

It is actually implementing the judgment logic described earlier, and finally opening a compareWith method for easy external input of the parsing results to get the final judgment.

Usage Example:

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("version string parse error!")
}

Or…

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

Supports >/≥/</≤/=/!=/~> operators.

Next Step

Test cases…

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)
        }
    }
}

Currently planning to further optimize Version, adjust performance tests, organize packaging, and then run through the process of creating my own cocoapods.

However, there is already a very complete Version handling Pod project, so there is no need to reinvent the wheel. I just want to streamline the creation process XD.

Maybe I will also submit a PR for the existing wheel to implement ~>.

References:

If you have any questions or comments, feel free to contact me.

===

本文中文版本

===

This article was first published in Traditional Chinese on Medium ➡️ View Here



This post is licensed under CC BY 4.0 by the author.

Apple Watch Original Stainless Steel Milanese Loop Unboxing

AVPlayer Real-time Cache Implementation