Ended up having to convert my Strings to NSStrings:
if storeVersion.compare(currentVersion, options: NSStringCompareOptions.NumericSearch) == NSComparisonResult.OrderedDescending {
println("store version is newer")
}
Swift 3
let currentVersion = "3.0.1"
let storeVersion = "3.0.2"
if storeVersion.compare(currentVersion, options: .numeric) == .orderedDescending {
print("store version is newer")
}
Sometimes, the storeVersion's length is not equal to the currentVersion's. e.g. maybe storeVersion is 3.0.0, however, you fixed a bug and named it 3.0.0.1.
func ascendingOrSameVersion(minorVersion smallerVersion:String, largerVersion:String)->Bool{
var result = true //default value is equal
let smaller = split(smallerVersion){ $0 == "." }
let larger = split(largerVersion){ $0 == "." }
let maxLength = max(smaller.count, larger.count)
for var i:Int = 0; i < maxLength; i++ {
var s = i < smaller.count ? smaller[i] : "0"
var l = i < larger.count ? larger[i] : "0"
if s != l {
result = s < l
break
}
}
return result
}
You don't need to cast it as NSString. String object in Swift 3 is just powerful enough to compare versions like below.
let version = "1.0.0"
let targetVersion = "0.5.0"
version.compare(targetVersion, options: .numeric) == .orderedSame // false
version.compare(targetVersion, options: .numeric) == .orderedAscending // false
version.compare(targetVersion, options: .numeric) == .orderedDescending // true
But sample above does not cover versions with extra zeros.(Ex: "1.0.0" & "1.0")
So, I've made all kinds of these extension methods in String to handle version comparison using Swift. It does consider extra zeros I said, quite simple and will work as you expected.
class func compareVersion(_ ver: String, to toVer: String) -> ComparisonResult {
var ary0 = ver.components(separatedBy: ".").map({ return Int($0) ?? 0 })
var ary1 = toVer.components(separatedBy: ".").map({ return Int($0) ?? 0 })
while ary0.count < 3 {
ary0.append(0)
}
while ary1.count < 3 {
ary1.append(0)
}
let des0 = ary0[0...2].description
let des1 = ary1[0...2].description
return des0.compare(des1, options: .numeric)
}
and test:
func test_compare_version() {
XCTAssertEqual(compareVersion("1.0.0", to: "1.0.0"), .orderedSame)
XCTAssertEqual(compareVersion("1.0.0", to: "1.0."), .orderedSame)
XCTAssertEqual(compareVersion("1.0.0", to: "1.0"), .orderedSame)
XCTAssertEqual(compareVersion("1.0.0", to: "1."), .orderedSame)
XCTAssertEqual(compareVersion("1.0.0", to: "1"), .orderedSame)
XCTAssertEqual(compareVersion("1.0.0", to: "1.0.1"), .orderedAscending)
XCTAssertEqual(compareVersion("1.0.0", to: "1.1."), .orderedAscending)
XCTAssertEqual(compareVersion("1.0.0", to: "1.1"), .orderedAscending)
XCTAssertEqual(compareVersion("1.0.0", to: "2."), .orderedAscending)
XCTAssertEqual(compareVersion("1.0.0", to: "2"), .orderedAscending)
XCTAssertEqual(compareVersion("1.0.0", to: "1.0.0"), .orderedSame)
XCTAssertEqual(compareVersion("1.0.", to: "1.0.0"), .orderedSame)
XCTAssertEqual(compareVersion("1.0", to: "1.0.0"), .orderedSame)
XCTAssertEqual(compareVersion("1.", to: "1.0.0"), .orderedSame)
XCTAssertEqual(compareVersion("1", to: "1.0.0"), .orderedSame)
XCTAssertEqual(compareVersion("1.0.1", to: "1.0.0"), .orderedDescending)
XCTAssertEqual(compareVersion("1.1.", to: "1.0.0"), .orderedDescending)
XCTAssertEqual(compareVersion("1.1", to: "1.0.0"), .orderedDescending)
XCTAssertEqual(compareVersion("2.", to: "1.0.0"), .orderedDescending)
XCTAssertEqual(compareVersion("2", to: "1.0.0"), .orderedDescending)
XCTAssertEqual(compareVersion("1.0.0", to: "0.9.9"), .orderedDescending)
XCTAssertEqual(compareVersion("1.0.0", to: "0.9."), .orderedDescending)
XCTAssertEqual(compareVersion("1.0.0", to: "0.9"), .orderedDescending)
XCTAssertEqual(compareVersion("1.0.0", to: "0."), .orderedDescending)
XCTAssertEqual(compareVersion("1.0.0", to: "0"), .orderedDescending)
XCTAssertEqual(compareVersion("", to: "0"), .orderedSame)
XCTAssertEqual(compareVersion("0", to: ""), .orderedSame)
XCTAssertEqual(compareVersion("", to: "1"), .orderedAscending)
XCTAssertEqual(compareVersion("1", to: ""), .orderedDescending)
XCTAssertEqual(compareVersion("1.0.0", to: "1.0.0.9"), .orderedSame)
XCTAssertEqual(compareVersion("1.0.0.9", to: "1.0.0"), .orderedSame)
}
let current = "1.3"
let appStore = "1.2.9"
let versionCompare = current.compare(appStore, options: .numeric)
if versionCompare == .orderedSame {
print("same version")
} else if versionCompare == .orderedAscending {
// will execute the code here
print("ask user to update")
} else if versionCompare == .orderedDescending {
// execute if current > appStore
print("don't expect happen...")
}
But, unfortunately, it doesn't work when the version is like 1.0.1.2 (I know, it shouldn't exist, but things happens).
I changed your extension and improved the tests to cover (I believe) all cases.
Also, I created an extension that removes all the unnecessary characters from the version string, sometimes it can be v1.0.1.2.
You can check all the code in the following gists:
extension String {
func versionCompare(_ otherVersion: String) -> ComparisonResult {
let versionDelimiter = "."
var versionComponents = self.components(separatedBy: versionDelimiter) // <1>
var otherVersionComponents = otherVersion.components(separatedBy: versionDelimiter)
let zeroDiff = versionComponents.count - otherVersionComponents.count // <2>
if zeroDiff == 0 { // <3>
// Same format, compare normally
return self.compare(otherVersion, options: .literal)
} else {
let zeros = Array(repeating: "0", count: abs(zeroDiff)) // <4>
if zeroDiff > 0 {
otherVersionComponents.append(contentsOf: zeros) // <5>
} else {
versionComponents.append(contentsOf: zeros)
}
return versionComponents.joined(separator: versionDelimiter)
.compare(otherVersionComponents.joined(separator: versionDelimiter), options: .literal) // <6>
}
}
}
//USAGE
let previousVersionNumber = "1.102.130"
let newAnpStoreVersion = "1.2" // <- Higher
switch previousVersionNumber.versionCompare(newAnpStoreVersion) {
case .orderedAscending:
case .orderedSame:
case .orderedDescending:
}
Once you remove the dots from the version strings, it's a simple Int comparison.
Assumption/Caveat: Version String is always the same number of digits (this fails, if for instance, there is version 1.10.0 since it will be converted to 1110 and version 2.1.0 will be compared as 210). It's not a huge leap to do a comparison of major first, then minor, then patch or incorporate other strategies mentioned in other answers and combine.
// inner json (there are *many* more parameters, but this is the only one we're concerned with)
struct AppUpdateData: Codable {
let version: String
}
// outer json
struct AppVersionResults: Codable {
let results: [AppUpdateData]
}
struct AppVersionComparison {
let currentVersion: Int
let latestVersion: Int
var needsUpdate: Bool {
currentVersion < latestVersion
}
}
// this isn't really doing much for error handling, could just return an optional
func needsUpdate() async throws -> AppVersionComparison {
let bundleId = "com.my.bundleId"
let url = URL(string: "https://itunes.apple.com/lookup?bundleId=\(bundleId)")!
let (data, _) = try await URLSession.shared.data(from: url)
let noResultsError: NSError = NSError(domain: #function, code: 999, userInfo: [NSLocalizedDescriptionKey: "No results"])
do {
let decoder = JSONDecoder()
let decoded = try decoder.decode(AppVersionResults.self, from: data)
guard decoded.results.count > 0 else { throw noResultsError }
var latestVersionString = decoded.results[0].version
latestVersionString.removeAll { $0 == "." }
guard let latestVersion = Int(latestVersionString),
var currentVersionString = Bundle.main.infoDictionary?["CFBundleShortVersionString"] as? String
else { throw noResultsError }
currentVersionString.removeAll { $0 == "." }
guard let currentVersion = Int(currentVersionString) else {
throw noResultsError
}
return .init(currentVersion: currentVersion, latestVersion: latestVersion)
}
}