diff --git a/.gitignore b/.gitignore index f067d67..8ee381c 100644 --- a/.gitignore +++ b/.gitignore @@ -31,3 +31,4 @@ Carthage # `pod install` in .travis.yml # # Pods/ +Example/Pods diff --git a/Example/Podfile.lock b/Example/Podfile.lock new file mode 100644 index 0000000..0acf4fb --- /dev/null +++ b/Example/Podfile.lock @@ -0,0 +1,20 @@ +PODS: + - Nimble (3.0.0) + - Quick (0.8.0) + - VersionsTracker (0.1.0) + +DEPENDENCIES: + - Nimble (= 3.0.0) + - Quick (~> 0.8.0) + - VersionsTracker (from `../`) + +EXTERNAL SOURCES: + VersionsTracker: + :path: "../" + +SPEC CHECKSUMS: + Nimble: 4c353d43735b38b545cbb4cb91504588eb5de926 + Quick: 563d0f6ec5f72e394645adb377708639b7dd38ab + VersionsTracker: 43a34b9b112bb711095e4816a7b9ce902ece1b17 + +COCOAPODS: 0.39.0 diff --git a/Example/Tests/Tests.swift b/Example/Tests/Tests.swift deleted file mode 100644 index b654ae1..0000000 --- a/Example/Tests/Tests.swift +++ /dev/null @@ -1,50 +0,0 @@ -// https://github.com/Quick/Quick - -import Quick -import Nimble -import VersionsTracker - -class TableOfContentsSpec: QuickSpec { - override func spec() { - describe("these will fail") { - - it("can do maths") { - expect(1) == 2 - } - - it("can read") { - expect("number") == "string" - } - - it("will eventually fail") { - expect("time").toEventually( equal("done") ) - } - - context("these will pass") { - - it("can do maths") { - expect(23) == 23 - } - - it("can read") { - expect("๐Ÿฎ") == "๐Ÿฎ" - } - - it("will eventually pass") { - var time = "passing" - - dispatch_async(dispatch_get_main_queue()) { - time = "done" - } - - waitUntil { done in - NSThread.sleepForTimeInterval(0.5) - expect(time) == "done" - - done() - } - } - } - } - } -} diff --git a/Example/Tests/VersionTests.swift b/Example/Tests/VersionTests.swift new file mode 100644 index 0000000..9c597e7 --- /dev/null +++ b/Example/Tests/VersionTests.swift @@ -0,0 +1,221 @@ +// +// VersionTests.swift +// VersionTracker +// +// Copyright (c) 2016 Martin Stemmle +// +// Permission is hereby granted, free of charge, to any person obtaining a copy +// of this software and associated documentation files (the "Software"), to deal +// in the Software without restriction, including without limitation the rights +// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +// copies of the Software, and to permit persons to whom the Software is +// furnished to do so, subject to the following conditions: +// +// The above copyright notice and this permission notice shall be included in +// all copies or substantial portions of the Software. +// +// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN +// THE SOFTWARE. + +// https://github.com/Quick/Quick + +import Quick +import Nimble +@testable import VersionsTracker + +class VersionTests: QuickSpec { + override func spec() { + describe("Version") { + + describe("currentVersion") { + let infoDict = NSBundle.mainBundle().infoDictionary! + it("versionString is set to CFBundleShortVersionString from info.plist") { + expect(Version.currentAppVersion.versionString).to(equal(infoDict["CFBundleShortVersionString"] as? String)) + } + it("buildString is set to kCFBundleVersionKey from info.plist") { + expect(Version.currentAppVersion.buildString).to(equal(infoDict[kCFBundleVersionKey as String] as? String)) + } + } + + describe("can compare") { + describe("with another Version") { + it("can handle smaller or equal") { + let version = Version("1.2.3") + // positive tests + expect(version).to(beLessThanOrEqualTo(Version("1.2.3"))) + expect(version).to(beLessThan(Version("1.2.3.1"))) + expect(version).to(beLessThan(Version("1.2.3.1.2.3"))) + expect(version).to(beLessThan(Version("1.2.3.1111"))) + expect(version).to(beLessThan(Version("1.2.3.4.5.6.7.8.9"))) + expect(version).to(beLessThan(Version("1.2.33"))) + expect(version).to(beLessThan(Version("1.2.4"))) + expect(version).to(beLessThan(Version("1.22"))) + expect(version).to(beLessThan(Version("1.22.0"))) + expect(version).to(beLessThan(Version("1.222.0"))) + expect(version).to(beLessThan(Version("1.3"))) + expect(version).to(beLessThan(Version("1.3.0"))) + expect(version).to(beLessThan(Version("1.3.1"))) + expect(version).to(beLessThan(Version("1.3.3"))) + expect(version).to(beLessThan(Version("2.0"))) + expect(version).to(beLessThan(Version("2.0.0"))) + expect(version).to(beLessThan(Version("2.2"))) + expect(version).to(beLessThan(Version("2.2.2"))) + // negative tests + expect(version).toNot(beLessThan(Version("1.0"))) + expect(version).toNot(beLessThan(Version("1.1"))) + expect(version).toNot(beLessThan(Version("1.2"))) + expect(version).toNot(beLessThan(Version("1.2.1"))) + expect(version).toNot(beLessThan(Version("1.2.1.2"))) + expect(version).toNot(beLessThan(Version("1.2.1.2.1"))) + expect(version).toNot(beLessThan(Version("1.2.1.222.1"))) + } + + it("treats same semantic versions as equal") { + let version = Version("2.0.0") + expect(version).to(equal(Version("2.0"))) + expect(version).to(equal(Version("2.0.0"))) + expect(version).to(equal(Version("2.0.0.0"))) + expect(version).to(equal(Version("2.00.00.0.0"))) + } + + it("treats diffenrent semantic versions not equal") { + let version = Version("2.0.0") + expect(version).toNot(equal(Version("2.1"))) + expect(version).toNot(equal(Version("2.0.1"))) + expect(version).toNot(equal(Version("2.0.0.1"))) + expect(version).toNot(equal(Version("2.00.00.0.1"))) + } + + it("treats same semantic versions but different builds as not equal") { + let version = Version("2.0.0", buildString: "B0815") + expect(version).to(equal(Version("2.0", buildString: "B0815"))) + expect(version).to(equal(Version("2.0.0", buildString: "B0815"))) + expect(version).to(equal(Version("2.0.0.0", buildString: "B0815"))) + expect(version).toNot(equal(Version("2.00.00.0.0", buildString: "B4711"))) + expect(version).toNot(equal(Version("2.0", buildString: "B4711"))) + expect(version).toNot(equal(Version("2.0.0", buildString: "B4711"))) + expect(version).toNot(equal(Version("2.0.0.0", buildString: "B4711"))) + expect(version).toNot(equal(Version("2.00.00.0.0", buildString: "B4711"))) + } + + it("does not care about installDate when semantic versions and builds are equal") { + let version = Version("2.0.0", buildString: "B0815", installDate: NSDate()) + let laterDate = NSDate(timeIntervalSinceNow: 30) + let earlierDate = NSDate(timeIntervalSinceNow: -30) + expect(version).to(equal(Version("2.0", buildString: "B0815", installDate: earlierDate))) + expect(version).to(equal(Version("2.0.0", buildString: "B0815", installDate: earlierDate))) + expect(version).to(equal(Version("2.0.0.0", buildString: "B0815", installDate: earlierDate))) + expect(version).to(equal(Version("2.00.00.0.0", buildString: "B0815", installDate: earlierDate))) + expect(version).to(equal(Version("2.0", buildString: "B0815", installDate: laterDate))) + expect(version).to(equal(Version("2.0.0", buildString: "B0815", installDate: laterDate))) + expect(version).to(equal(Version("2.0.0.0", buildString: "B0815", installDate: laterDate))) + expect(version).to(equal(Version("2.00.00.0.0", buildString: "B0815", installDate: laterDate))) + } + } + + + describe("with another Strings") { + it("can handle smaller or equal") { + let version = Version("1.2.3") + // positive tests + expect(version).to(beLessThanOrEqualTo("1.2.3")) + expect(version).to(beLessThan("1.2.3.1")) + expect(version).to(beLessThan("1.2.3.1.2.3")) + expect(version).to(beLessThan("1.2.3.1111")) + expect(version).to(beLessThan("1.2.3.4.5.6.7.8.9")) + expect(version).to(beLessThan("1.2.33")) + expect(version).to(beLessThan("1.2.4")) + expect(version).to(beLessThan("1.22")) + expect(version).to(beLessThan("1.22.0")) + expect(version).to(beLessThan("1.222.0")) + expect(version).to(beLessThan("1.3")) + expect(version).to(beLessThan("1.3.0")) + expect(version).to(beLessThan("1.3.1")) + expect(version).to(beLessThan("1.3.3")) + expect(version).to(beLessThan("2.0")) + expect(version).to(beLessThan("2.0.0")) + expect(version).to(beLessThan("2.2")) + expect(version).to(beLessThan("2.2.2")) + // negative tests + expect(version).toNot(beLessThan("1.0")) + expect(version).toNot(beLessThan("1.1")) + expect(version).toNot(beLessThan("1.2")) + expect(version).toNot(beLessThan("1.2.1")) + expect(version).toNot(beLessThan("1.2.1.2")) + expect(version).toNot(beLessThan("1.2.1.2.1")) + expect(version).toNot(beLessThan("1.2.1.222.1")) + } + + it("treats same semantic versions as equal") { + let version = Version("2.0.0") + expect(version).to(equal("2.0")) + expect(version).to(equal("2.0.0")) + expect(version).to(equal("2.0.0.0")) + expect(version).to(equal("2.00.00.0.0")) + } + + it("treats diffenrent semantic versions not equal") { + let version = Version("2.0.0") + expect(version).toNot(equal("2.1")) + expect(version).toNot(equal("2.0.1")) + expect(version).toNot(equal("2.0.0.1")) + expect(version).toNot(equal("2.00.00.0.1")) + } + } + } + + it("can comepare app version strings being smaller or equal") { + expect(Version("1.4.5") <= "1.5.5") == true + expect(Version("1.4.5") <= "1.5.5") == true + expect(Version("1.4.5") <= "1.55.0") == true + expect(Version("1.4.5") <= "1.555.0") == true + expect(Version("1.4.5") <= "1.4.5.4") == true + expect(Version("1.4.5") <= "1.4.5.3.4.6") == true + expect(Version("1.4.5") <= "2.0.0") == true + + expect(Version("1.5.5") <= "1.4.5") == false + expect(Version("1.55.0") <= "1.4.5") == false + expect(Version("1.555.0") <= "1.4.5") == false + expect(Version("1.4.5.4") <= "1.4.5") == false + expect(Version("1.4.5.3.4.6.") <= "1.4.5") == false + expect(Version("2.0.0") <= "1.4.5") == false + } + + + describe("can determine version changes") { + it("detects clean installs") { + expect(Version.changeStateForFromVersion(nil, toVersion: Version("1.0", buildString: "19", installDate: NSDate()))).to(equal(Version.ChangeState.Installed)) + } + it("detects same version as not changed") { + let prevVersion = Version("1.0", buildString: "19", installDate: NSDate()) + let curVersion = Version("1.0", buildString: "19", installDate: NSDate()) + expect(Version.changeStateForFromVersion(prevVersion, toVersion: curVersion)).to(equal(Version.ChangeState.NotChanged)) + } + it("detects updates") { + let prevVersion = Version("1.0", buildString: "19", installDate: NSDate()) + let curVersion = Version("1.0", buildString: "20", installDate: NSDate()) + expect(Version.changeStateForFromVersion(prevVersion, toVersion: curVersion)).to(equal(Version.ChangeState.Update(previousVersion: prevVersion))) + } + it("detects upgrades") { + let prevVersion = Version("1.0", buildString: "19", installDate: NSDate()) + let curVersion = Version("1.1", buildString: "20", installDate: NSDate()) + expect(Version.changeStateForFromVersion(prevVersion, toVersion: curVersion)).to(equal(Version.ChangeState.Upgraded(previousVersion: prevVersion))) + } + it("detects downgrades") { + let prevVersion = Version("1.1", buildString: "19", installDate: NSDate()) + let curVersion = Version("1.0", buildString: "19", installDate: NSDate()) + expect(Version.changeStateForFromVersion(prevVersion, toVersion: curVersion)).to(equal(Version.ChangeState.Downgraded(previousVersion: prevVersion))) + } + } + + } + + } +} + + diff --git a/Example/Tests/VersionTrackerTests.swift b/Example/Tests/VersionTrackerTests.swift new file mode 100644 index 0000000..931dba4 --- /dev/null +++ b/Example/Tests/VersionTrackerTests.swift @@ -0,0 +1,78 @@ +// +// VersionTrackerTests.swift +// VersionsTracker +// +// Copyright (c) 2016 Martin Stemmle +// +// Permission is hereby granted, free of charge, to any person obtaining a copy +// of this software and associated documentation files (the "Software"), to deal +// in the Software without restriction, including without limitation the rights +// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +// copies of the Software, and to permit persons to whom the Software is +// furnished to do so, subject to the following conditions: +// +// The above copyright notice and this permission notice shall be included in +// all copies or substantial portions of the Software. +// +// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN +// THE SOFTWARE. + +// https://github.com/Quick/Quick + +import Quick +import Nimble +@testable import VersionsTracker + +class VersionTrackerTests: QuickSpec { + override func spec() { + describe("AppVersionTracker") { + context("used with custom NSUserDefaults") { + let scope = "appVersion" + let userDefaults = NSUserDefaults(suiteName: "AppVersionTrackerTests")! + userDefaults.resetInScope(scope) + + let versions = [ + Version("1.0", buildString: "1"), // install + Version("1.0", buildString: "2"), // update + Version("1.1", buildString: "3"), // upgrade + Version("1.0", buildString: "2") // downgrade + ] + + beforeEach { + VersionTracker.resetUpdateVersionHistoryOnceToken() + } + + it("detects the very first app launch as Installed state") { + let versionTracker = VersionTracker(currentVersion: versions[0], inScope: scope, userDefaults: userDefaults) + expect(versionTracker.changeState).to(equal(Version.ChangeState.Installed)); + } + + it("it will notice NotChanged for the second launch") { + let versionTracker = VersionTracker(currentVersion: versions[0], inScope: scope, userDefaults: userDefaults) + expect(versionTracker.changeState).to(equal(Version.ChangeState.NotChanged)); + } + + it("it will notice build Updates") { + let versionTracker = VersionTracker(currentVersion: versions[1], inScope: scope, userDefaults: userDefaults) + expect(versionTracker.changeState).to(equal(Version.ChangeState.Update(previousVersion: Version("1.0", buildString: "1")))); + } + + it("it will notice markting version upgrades") { + let versionTracker = VersionTracker(currentVersion: versions[2], inScope: scope, userDefaults: userDefaults) + expect(versionTracker.changeState).to(equal(Version.ChangeState.Upgraded(previousVersion: Version("1.0", buildString: "2")))); + } + + it("it will notice version downgrades") { + let versionTracker = VersionTracker(currentVersion: versions[3], inScope: scope, userDefaults: userDefaults) + expect(versionTracker.changeState).to(equal(Version.ChangeState.Downgraded(previousVersion: Version("1.1", buildString: "3")))); + } + + } + } + } +} \ No newline at end of file diff --git a/Example/VersionsTracker.xcodeproj/project.pbxproj b/Example/VersionsTracker.xcodeproj/project.pbxproj index a4a0836..c0e7425 100644 --- a/Example/VersionsTracker.xcodeproj/project.pbxproj +++ b/Example/VersionsTracker.xcodeproj/project.pbxproj @@ -12,7 +12,8 @@ 607FACDB1AFB9204008FA782 /* Main.storyboard in Resources */ = {isa = PBXBuildFile; fileRef = 607FACD91AFB9204008FA782 /* Main.storyboard */; }; 607FACDD1AFB9204008FA782 /* Images.xcassets in Resources */ = {isa = PBXBuildFile; fileRef = 607FACDC1AFB9204008FA782 /* Images.xcassets */; }; 607FACE01AFB9204008FA782 /* LaunchScreen.xib in Resources */ = {isa = PBXBuildFile; fileRef = 607FACDE1AFB9204008FA782 /* LaunchScreen.xib */; }; - 607FACEC1AFB9204008FA782 /* Tests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 607FACEB1AFB9204008FA782 /* Tests.swift */; }; + 78CADB251C67ACC50002A51E /* VersionTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 78CADB1D1C67A3840002A51E /* VersionTests.swift */; }; + 78CADB281C67AE430002A51E /* VersionTrackerTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 78CADB261C67AD1C0002A51E /* VersionTrackerTests.swift */; }; 9E4EB5E5A42F2405FF44BF56 /* Pods_VersionsTracker_Tests.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = C9687EE749DBFD898A2EFA34 /* Pods_VersionsTracker_Tests.framework */; }; FDD54E09DF7023795BB30A48 /* Pods_VersionsTracker_Example.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = D1F13660186FCBDE4AD866A9 /* Pods_VersionsTracker_Example.framework */; }; /* End PBXBuildFile section */ @@ -40,7 +41,8 @@ 607FACDF1AFB9204008FA782 /* Base */ = {isa = PBXFileReference; lastKnownFileType = file.xib; name = Base; path = Base.lproj/LaunchScreen.xib; sourceTree = ""; }; 607FACE51AFB9204008FA782 /* VersionsTracker_Tests.xctest */ = {isa = PBXFileReference; explicitFileType = wrapper.cfbundle; includeInIndex = 0; path = VersionsTracker_Tests.xctest; sourceTree = BUILT_PRODUCTS_DIR; }; 607FACEA1AFB9204008FA782 /* Info.plist */ = {isa = PBXFileReference; lastKnownFileType = text.plist.xml; path = Info.plist; sourceTree = ""; }; - 607FACEB1AFB9204008FA782 /* Tests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Tests.swift; sourceTree = ""; }; + 78CADB1D1C67A3840002A51E /* VersionTests.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = VersionTests.swift; sourceTree = ""; }; + 78CADB261C67AD1C0002A51E /* VersionTrackerTests.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = VersionTrackerTests.swift; sourceTree = ""; }; B85D1A37B004EF88A7EDCB01 /* Pods-VersionsTracker_Example.debug.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-VersionsTracker_Example.debug.xcconfig"; path = "Pods/Target Support Files/Pods-VersionsTracker_Example/Pods-VersionsTracker_Example.debug.xcconfig"; sourceTree = ""; }; C9687EE749DBFD898A2EFA34 /* Pods_VersionsTracker_Tests.framework */ = {isa = PBXFileReference; explicitFileType = wrapper.framework; includeInIndex = 0; path = Pods_VersionsTracker_Tests.framework; sourceTree = BUILT_PRODUCTS_DIR; }; D1F13660186FCBDE4AD866A9 /* Pods_VersionsTracker_Example.framework */ = {isa = PBXFileReference; explicitFileType = wrapper.framework; includeInIndex = 0; path = Pods_VersionsTracker_Example.framework; sourceTree = BUILT_PRODUCTS_DIR; }; @@ -126,7 +128,8 @@ 607FACE81AFB9204008FA782 /* Tests */ = { isa = PBXGroup; children = ( - 607FACEB1AFB9204008FA782 /* Tests.swift */, + 78CADB1D1C67A3840002A51E /* VersionTests.swift */, + 78CADB261C67AD1C0002A51E /* VersionTrackerTests.swift */, 607FACE91AFB9204008FA782 /* Supporting Files */, ); path = Tests; @@ -368,7 +371,8 @@ isa = PBXSourcesBuildPhase; buildActionMask = 2147483647; files = ( - 607FACEC1AFB9204008FA782 /* Tests.swift in Sources */, + 78CADB251C67ACC50002A51E /* VersionTests.swift in Sources */, + 78CADB281C67AE430002A51E /* VersionTrackerTests.swift in Sources */, ); runOnlyForDeploymentPostprocessing = 0; }; diff --git a/Example/VersionsTracker.xcworkspace/contents.xcworkspacedata b/Example/VersionsTracker.xcworkspace/contents.xcworkspacedata new file mode 100644 index 0000000..ae38229 --- /dev/null +++ b/Example/VersionsTracker.xcworkspace/contents.xcworkspacedata @@ -0,0 +1,10 @@ + + + + + + + diff --git a/Example/VersionsTracker/AppDelegate.swift b/Example/VersionsTracker/AppDelegate.swift index b03dbb0..4758ff0 100644 --- a/Example/VersionsTracker/AppDelegate.swift +++ b/Example/VersionsTracker/AppDelegate.swift @@ -2,11 +2,30 @@ // AppDelegate.swift // VersionsTracker // -// Created by Martin Stemmle on 02/07/2016. -// Copyright (c) 2016 Martin Stemmle. All rights reserved. +// Copyright (c) 2016 Martin Stemmle // +// Permission is hereby granted, free of charge, to any person obtaining a copy +// of this software and associated documentation files (the "Software"), to deal +// in the Software without restriction, including without limitation the rights +// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +// copies of the Software, and to permit persons to whom the Software is +// furnished to do so, subject to the following conditions: +// +// The above copyright notice and this permission notice shall be included in +// all copies or substantial portions of the Software. +// +// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN +// THE SOFTWARE. import UIKit +import VersionsTracker + +let iDontMindSingletons = false @UIApplicationMain class AppDelegate: UIResponder, UIApplicationDelegate { @@ -16,6 +35,27 @@ class AppDelegate: UIResponder, UIApplicationDelegate { func application(application: UIApplication, didFinishLaunchingWithOptions launchOptions: [NSObject: AnyObject]?) -> Bool { // Override point for customization after application launch. + + guard NSClassFromString("XCTest") == nil else { + // skip AppVersionTracker setup to unit tests take care of it + return true + } + + print("โ˜€๏ธ W E L C O M E ๐Ÿค—") + + if iDontMindSingletons { + // initialize AppVersionTracker once + VersionsTracker.initialize(trackAppVersion: true, trackOSVersion: true) + // access the AppVersionTracker.sharedInstance anywhere you need + printVersionInfo(VersionsTracker.sharedInstance.osVersion, headline: "OS VERSION") + printVersionInfo(VersionsTracker.sharedInstance.appVersion, headline: "APP VERSION (via AppDelegate)") + } + else { + // make sure to update the version history once once during you app's life time + VersionsTracker.updateVersionHistories(trackAppVersion: true, trackOSVersion: true) + // see ViewController.viewDidLoad() for usage of tracked version + } + return true } @@ -41,6 +81,42 @@ class AppDelegate: UIResponder, UIApplicationDelegate { // Called when the application is about to terminate. Save data if appropriate. See also applicationDidEnterBackground:. } +} + +// MARK: - Helpers +func printVersionInfo(versionTracker: VersionTracker, headline: String) { + print("") + print("") + print(headline) + print([String](count: headline.characters.count, repeatedValue: "-").joinWithSeparator("")) + print("") + printVersionChange(versionTracker) + print("โŒš๏ธCurrent version is from \(versionTracker.currentVersion.installDate)") + print(" previous version is from \((versionTracker.previousVersion?.installDate ?? "- oh, there is no")!)") + printVersionHistory(versionTracker) } +func printVersionChange(versionTracker: VersionTracker) { + switch versionTracker.changeState { + case .Installed: + print("๐Ÿ†• Congratulations, the app is launched for the very first time") + case .NotChanged: + print("๐Ÿ”„ Welcome back, nothing as changed since the last time") + case .Update(let previousVersion): + print("๐Ÿ†™ The app was updated making small changes: \(previousVersion) -> \(versionTracker.currentVersion)") + case .Upgraded(let previousVersion): + print("โฌ†๏ธ Cool, its a new version: \(previousVersion) -> \(versionTracker.currentVersion)") + case .Downgraded(let previousVersion): + print("โฌ‡๏ธ Oohu, looks like something is wrong with the current version to make you come back here: \(previousVersion) -> \(versionTracker.currentVersion)") + } +} + +func printVersionHistory(versionTracker: VersionTracker) { + let clocks = ["๐Ÿ•", "๐Ÿ•‘", "๐Ÿ•’", "๐Ÿ•“", "๐Ÿ•”", "๐Ÿ••", "๐Ÿ•–", "๐Ÿ•—", "๐Ÿ•˜", "๐Ÿ•™", "๐Ÿ•š", "๐Ÿ•›"] + print("") + print("Version history:") + for (index, version) in versionTracker.versionHistory.enumerate() { + print("\(clocks[index % clocks.count]) \(version.installDate) \(version)") + } +} diff --git a/Example/VersionsTracker/Base.lproj/Main.storyboard b/Example/VersionsTracker/Base.lproj/Main.storyboard index 52ea29e..4e91241 100644 --- a/Example/VersionsTracker/Base.lproj/Main.storyboard +++ b/Example/VersionsTracker/Base.lproj/Main.storyboard @@ -1,13 +1,16 @@ - + - + + + + - + @@ -15,7 +18,28 @@ + + + + + + + + + + + diff --git a/Example/VersionsTracker/ViewController.swift b/Example/VersionsTracker/ViewController.swift index a5a2f2f..202e942 100644 --- a/Example/VersionsTracker/ViewController.swift +++ b/Example/VersionsTracker/ViewController.swift @@ -2,22 +2,38 @@ // ViewController.swift // VersionsTracker // -// Created by Martin Stemmle on 02/07/2016. -// Copyright (c) 2016 Martin Stemmle. All rights reserved. +// Copyright (c) 2016 Martin Stemmle // +// Permission is hereby granted, free of charge, to any person obtaining a copy +// of this software and associated documentation files (the "Software"), to deal +// in the Software without restriction, including without limitation the rights +// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +// copies of the Software, and to permit persons to whom the Software is +// furnished to do so, subject to the following conditions: +// +// The above copyright notice and this permission notice shall be included in +// all copies or substantial portions of the Software. +// +// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN +// THE SOFTWARE. import UIKit +import VersionsTracker class ViewController: UIViewController { override func viewDidLoad() { super.viewDidLoad() - // Do any additional setup after loading the view, typically from a nib. - } - override func didReceiveMemoryWarning() { - super.didReceiveMemoryWarning() - // Dispose of any resources that can be recreated. + if !iDontMindSingletons { + let versionsTracker = VersionsTracker() + printVersionInfo(versionsTracker.appVersion, headline: "APP VERSION (via ViewController)") + } } } diff --git a/Pod/Classes/ReplaceMe.swift b/Pod/Classes/ReplaceMe.swift deleted file mode 100644 index e69de29..0000000 diff --git a/Pod/Classes/Version.swift b/Pod/Classes/Version.swift new file mode 100644 index 0000000..f8f2b36 --- /dev/null +++ b/Pod/Classes/Version.swift @@ -0,0 +1,211 @@ +// Version.swift +// +// Copyright (c) 2016 Martin Stemmle +// +// Permission is hereby granted, free of charge, to any person obtaining a copy +// of this software and associated documentation files (the "Software"), to deal +// in the Software without restriction, including without limitation the rights +// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +// copies of the Software, and to permit persons to whom the Software is +// furnished to do so, subject to the following conditions: +// +// The above copyright notice and this permission notice shall be included in +// all copies or substantial portions of the Software. +// +// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN +// THE SOFTWARE. + + +import Foundation + +private func parseVersion(lhs: Version, rhs: Version) -> Zip2Sequence<[Int], [Int]> { + + let lhs = lhs.versionString.characters.split(".").map { (String($0) as NSString).integerValue } + let rhs = rhs.versionString.characters.split(".").map { (String($0) as NSString).integerValue } + let count = max(lhs.count, rhs.count) + return zip( + lhs + Array(count: count - lhs.count, repeatedValue: 0), + rhs + Array(count: count - rhs.count, repeatedValue: 0)) +} + +public func == (lhs: Version, rhs: Version) -> Bool { + + var result: Bool = true + for (l, r) in parseVersion(lhs, rhs: rhs) { + + if l != r { + result = false + } + } + + if result == true { + result = lhs.buildString == rhs.buildString + } + + return result +} + +public func < (lhs: Version, rhs: Version) -> Bool { + + for (l, r) in parseVersion(lhs, rhs: rhs) { + if l < r { + return true + } else if l > r { + return false + } + } + return false +} + +public class Version: StringLiteralConvertible, Comparable { + + internal class var currentAppVersion: Version { + guard let infoDict = NSBundle.mainBundle().infoDictionary else { + fatalError() + } + return Version(infoDict["CFBundleShortVersionString"] as! String, buildString: infoDict[kCFBundleVersionKey as String] as? String, installDate: nil) + } + + internal class var currentOSVersion : Version { + let systemVersion = NSProcessInfo.processInfo().operatingSystemVersion + let systemVersionString = [systemVersion.majorVersion, systemVersion.minorVersion, systemVersion.patchVersion].map({String($0)}).joinWithSeparator(".") + let systemVersionStringScanner = NSScanner(string: NSProcessInfo.processInfo().operatingSystemVersionString) + var build: NSString? + systemVersionStringScanner.scanUpToString("(Build", intoString: nil) + systemVersionStringScanner.scanUpToString(" ", intoString: nil) + systemVersionStringScanner.scanUpToString(")", intoString: &build) + return Version(systemVersionString, buildString: build as? String) + } + + public let versionString: String + public let buildString: String + internal(set) public var installDate: NSDate + + public init(_ versionString: String, buildString build: String? = nil, installDate date: NSDate? = nil) { + self.versionString = versionString + self.buildString = build ?? "" + self.installDate = date ?? NSDate() + } + + // MARK: NSUserDefaults serialization + + private static let versionStringKey = "versionString" + private static let buildStringKey = "buildString" + private static let installDateKey = "installDate" + + internal convenience init(dict: NSDictionary) { + self.init(dict[Version.versionStringKey] as! String, + buildString: dict[Version.buildStringKey] as? String, + installDate: dict[Version.installDateKey] as? NSDate) + } + + internal static func versionFromDictionary(dict: NSDictionary?) -> Version? { + if let dictionary = dict { + return Version(dict: dictionary) + } + return nil + } + + internal var asDictionary: NSDictionary { + get { + return [ + Version.versionStringKey : self.versionString, + Version.buildStringKey : self.buildString, + Version.installDateKey : self.installDate + ] + } + } + + + // MARK: StringLiteralConvertible + + public required init(stringLiteral value: String) { + self.versionString = value + self.buildString = "" + self.installDate = NSDate() + } + + public required init(unicodeScalarLiteral value: String) { + self.versionString = value + self.buildString = "" + self.installDate = NSDate() + } + + public required init(extendedGraphemeClusterLiteral value: String) { + self.versionString = value + self.buildString = "" + self.installDate = NSDate() + } +} + +extension Version: CustomStringConvertible { + public var description: String { + return "\(self.versionString) (\(self.buildString))" + } +} + +extension Version { + + /** + The app version state indicates version changes since the last launch of the app. + - Installed: clean install, very first launch + - NotChanged: version not changed + - Update: build string changed, but marketing version stayed the same + - Upgraded: marketing version increased + - Downgraded: markting version decreased + */ + public enum ChangeState { + case Installed + case NotChanged + case Update(previousVersion: Version) + case Upgraded(previousVersion: Version) + case Downgraded(previousVersion: Version) + } + + /** + Determines the change state from one version to another. + */ + internal static func changeStateForFromVersion(olderVersion: Version?, toVersion newerVersion: Version) -> ChangeState { + guard let olderVersion = olderVersion else { + return .Installed + } + + if olderVersion < newerVersion { + return .Upgraded(previousVersion: olderVersion) + } + else if olderVersion > newerVersion { + return .Downgraded(previousVersion: olderVersion) + } + else if olderVersion != newerVersion { + return .Update(previousVersion: olderVersion) + } + return .NotChanged + } + +} + + +extension Version.ChangeState: Equatable { +} + +public func ==(lhs: Version.ChangeState, rhs: Version.ChangeState) -> Bool { + switch (lhs, rhs) { + case (.Installed, .Installed): + return true + case (.NotChanged, .NotChanged): + return true + case (let .Update(previousVersionLHS), let .Update(previousVersionRHS)): + return previousVersionLHS == previousVersionRHS + case (let .Upgraded(previousVersionLHS), let .Upgraded(previousVersionRHS)): + return previousVersionLHS == previousVersionRHS + case (let .Downgraded(previousVersionLHS), let .Downgraded(previousVersionRHS)): + return previousVersionLHS == previousVersionRHS + default: + return false + } +} diff --git a/Pod/Classes/VersionTracker.swift b/Pod/Classes/VersionTracker.swift new file mode 100644 index 0000000..a39a7d3 --- /dev/null +++ b/Pod/Classes/VersionTracker.swift @@ -0,0 +1,258 @@ +// VersionTracker.swift +// +// Copyright (c) 2016 Martin Stemmle +// +// Permission is hereby granted, free of charge, to any person obtaining a copy +// of this software and associated documentation files (the "Software"), to deal +// in the Software without restriction, including without limitation the rights +// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +// copies of the Software, and to permit persons to whom the Software is +// furnished to do so, subject to the following conditions: +// +// The above copyright notice and this permission notice shall be included in +// all copies or substantial portions of the Software. +// +// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN +// THE SOFTWARE. + + +import Foundation + +public class VersionTracker { + + /** + Sorted array of all versions which the user has had installed. New versions are appended at the end of the array. The last element is the current version. + + *Unless using the Singleton, the version history is lazy loaded from NSUserDefaults. If you only need to access the `currentVersion` + or the `previousVersion` use the corresponding properties to save loading the entire history.* + */ + private(set) public lazy var versionHistory: [Version] = self.userDefaults.versionsInScope(self.userDefaultsScope) + + /** + The previous version or `nil` if the user did not updated the app yet. + + *The absence of the previous version does not mean the app is running for the very first time. Therefor check if the* `state` *is set to* `Installed`. + + - returns: The previousVersion version or `nil`. + */ + private(set) public lazy var previousVersion: Version? = { + if self._previousVersion == self.currentVersion { + return nil + } + return self._previousVersion + }() + + /** + The previous version as stored in NSUserDefaults. Might be equal to the currentVersion in which case `previousVersion` should return `nil`. Hence the need for a 2nd property. + */ + private(set) lazy var _previousVersion: Version? = self.userDefaults.previousVersionForKey(self.userDefaultsScope) + + /** + - returns: The current version. + */ + private(set) public lazy var currentVersion: Version = self.userDefaults.lastLaunchedVersionForkey(self.userDefaultsScope)! + + + /** + The app version state indicates version changes since the last launch of the app. + */ + private(set) public lazy var changeState: Version.ChangeState = Version.changeStateForFromVersion(self._previousVersion, toVersion: self.currentVersion) + + + /** + The user defaults to store the version history in. + */ + private let userDefaults: NSUserDefaults + + /** + A string to build keys for storing version history of a particular verion to track. + */ + private let userDefaultsScope : String + + + /** + Initializes and returns a newly allocated `VersionTracker` instance. + When `VersionTracker.updateVersionHistory()` was called before, all properties will be lazy loaded to keep the memory footprint low. + + - parameter currentVersion: The current version. + - parameter userDefaults: Pass in a NSUserDefaults object for storing and retrieving the version history. Defaults to `NSUserDefaults.standardUserDefaults()`. + - parameter scope: A string to build keys for storing version history of a particular verion to track. + */ + public init(currentVersion: Version, inScope scope: String, userDefaults: NSUserDefaults? = nil) { + self.userDefaults = userDefaults ?? NSUserDefaults.standardUserDefaults() + self.userDefaultsScope = scope + // when initialize the first instance or the singleton instance, everything loaded in order to update the version history can be used + if let stuff = VersionTracker.updateVersionHistoryOnce( + withVersion: currentVersion, + inScope: userDefaultsScope, + onUserDefaults: self.userDefaults) { + self.versionHistory = stuff.installedVersions + self._previousVersion = stuff.previousVersion + self.currentVersion = stuff.currentVerstion + self.changeState = Version.changeStateForFromVersion(stuff.previousVersion, toVersion: stuff.currentVerstion) + } + // elsewise those properties will be lazy loaded + else { + // check if NSUserDefaults contain an entry for the current version + // otherwise another NSUserDefaults object was used already, which is not supported + if !self.userDefaults.hasLastLaunchedVersionSetInScope(userDefaultsScope) { + fatalError("โ—๏ธVersionTracker was already initialized with another NSUserDefaults object before.") + } + } + } + + + private struct OnceTokens { + static var appToken: dispatch_once_t = 0 + static var osToken: dispatch_once_t = 0 + } + + /** + Updates the version history once per session. To do so it loads the version history and creates a new version. This objects will be returned in a tuple. + However, if it was already called befor it will return `nil` as the versino history gets updates only once per app session. + */ + internal static func updateVersionHistoryOnce(var withVersion currentVersion: Version, inScope userDefaultsScope: String, onUserDefaults userDefaults: NSUserDefaults) -> (installedVersions: [Version], previousVersion: Version?, currentVerstion: Version)? { + + var result : (installedVersions: [Version], previousVersion: Version?, currentVerstion: Version)? + + let updateBlock:(Void)->Void = { () -> Void in + var installedVersions = userDefaults.versionsInScope(userDefaultsScope) + + if let knownCurrentVersion = installedVersions.filter({$0 == currentVersion}).first { + currentVersion = knownCurrentVersion + } else { + currentVersion.installDate = NSDate() + installedVersions.append(currentVersion) + userDefaults.setVersions(installedVersions, inScope: userDefaultsScope) + } + + userDefaults.setLastLaunchedVersion(currentVersion, inScope: userDefaultsScope) + + result = (installedVersions: installedVersions, + previousVersion: userDefaults.previousVersionForKey(userDefaultsScope), + currentVerstion: currentVersion) + } + + // FIXME: find a better solution, e.g. storing a map of dispatch_once_t by scopes + if userDefaultsScope == VersionsTracker.appVersionScope { + dispatch_once(&OnceTokens.appToken, updateBlock) + } + else if userDefaultsScope == VersionsTracker.osVersionScope { + dispatch_once(&OnceTokens.osToken, updateBlock) + } + else { + fatalError("unsupported version scope '\(userDefaultsScope)'") + } + + return result + } + + + +} + +private extension NSUserDefaults { + + private static let prefiex = "VersionsTracker" + + /** + key for storing the last launched version in NSUserDefaults: + - if the version stayed the same: holds the current version + - if the version has changed: + - holds the previous version before updateVersionHistoryOnce() + - becomes the current version after updateVersionHistoryOnce() + */ + private static let lastLaunchedVersionKey = "lastLaunchedVersion" + + /** + key for storing the antecessor version of the last launched version in NSUserDefaults + - holds the version of the previous launch after updateVersionHistory() + */ + private static let previousLaunchedVersionKey = "previousLaunchedVersion" + + /* + key for stroing the entire version history in NSUserDefaults + */ + private static let installedVersionsKey = "installedVersions" + + func versionsInScope(scope: String) -> [Version] { + let key = buildKeyForProperty(NSUserDefaults.installedVersionsKey, inScope: scope) + return (self.objectForKey(key) as? [NSDictionary])?.map { Version(dict: $0) } ?? [] + } + + func setVersions(versions: [Version], inScope scope: String) { + let key = buildKeyForProperty(NSUserDefaults.installedVersionsKey, inScope: scope) + self.setObject(versions.map{ $0.asDictionary }, forKey: key) + } + + func previousVersionForKey(key: String) -> Version? { + return versionForKey(key, property: NSUserDefaults.previousLaunchedVersionKey) + } + + func hasLastLaunchedVersionSetInScope(scope: String) -> Bool { + let key = buildKeyForProperty(NSUserDefaults.lastLaunchedVersionKey, inScope: scope) + return self.dictionaryForKey(key) != nil + } + + func lastLaunchedVersionForkey(key: String) -> Version? { + return self.versionForKey(key, property: NSUserDefaults.lastLaunchedVersionKey) + } + + private func versionForKey(var key: String, property: String) -> Version? { + key = buildKeyForProperty(property, inScope: key) + return Version.versionFromDictionary(self.dictionaryForKey(key)) + } + + private func buildKeyForProperty(property: String, inScope scope: String) -> String { + return [NSUserDefaults.prefiex, scope, property].joinWithSeparator(".") + } + + /** + Updates the last launched version with the given version. + + **It should only be called once per scope with the current version.** + + It will move the current stored last launched version to the previous launched version slot. + This allow retrieving the previous version at any time during the session. + */ + func setLastLaunchedVersion(version: Version, inScope scope: String) { + let lastLaunchedKey = buildKeyForProperty(NSUserDefaults.lastLaunchedVersionKey, inScope: scope) + let prevLaunchedKey = buildKeyForProperty(NSUserDefaults.previousLaunchedVersionKey, inScope: scope) + let lastLaunchedDictionary = self.dictionaryForKey(lastLaunchedKey) + self.setObject(lastLaunchedDictionary, forKey: prevLaunchedKey) // move the last version to the previous slot + self.setObject(version.asDictionary, forKey: lastLaunchedKey) // update last version with the current + } +} + + + + + +// MARK: - Testing helpers - + +internal extension VersionTracker { + + internal static func resetUpdateVersionHistoryOnceToken() { + guard NSClassFromString("XCTest") != nil else { fatalError("this method shall only be called in unit tests") } + OnceTokens.appToken = 0 + OnceTokens.osToken = 0 + } +} + +internal extension NSUserDefaults { + + internal func resetInScope(scope: String) { + self.removeObjectForKey(buildKeyForProperty(NSUserDefaults.previousLaunchedVersionKey, inScope: scope)) + self.removeObjectForKey(buildKeyForProperty(NSUserDefaults.lastLaunchedVersionKey, inScope: scope)) + self.removeObjectForKey(buildKeyForProperty(NSUserDefaults.installedVersionsKey, inScope: scope)) + } + +} + + + diff --git a/Pod/Classes/VersionsTracker.swift b/Pod/Classes/VersionsTracker.swift new file mode 100644 index 0000000..714c111 --- /dev/null +++ b/Pod/Classes/VersionsTracker.swift @@ -0,0 +1,126 @@ +// VersionsTracker.swift +// +// Copyright (c) 2016 Martin Stemmle +// +// Permission is hereby granted, free of charge, to any person obtaining a copy +// of this software and associated documentation files (the "Software"), to deal +// in the Software without restriction, including without limitation the rights +// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +// copies of the Software, and to permit persons to whom the Software is +// furnished to do so, subject to the following conditions: +// +// The above copyright notice and this permission notice shall be included in +// all copies or substantial portions of the Software. +// +// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN +// THE SOFTWARE. + + +import Foundation + +public class VersionsTracker { + + internal static let appVersionScope = "appVersion"; + internal static let osVersionScope = "osVersion"; + + + /** + Shared instance, for those who prefer using `VersionsTracker` as a singleton. + */ + public static var sharedInstance : VersionsTracker { + get { + if _sharedInstance == nil { + fatalError("โ—๏ธVersionsTracker.initialize() musted be called befor accessing the singleton") + } + return _sharedInstance! + } + } + + private static var _sharedInstance : VersionsTracker? + + + public lazy var appVersion : VersionTracker = VersionTracker(currentVersion: Version.currentAppVersion, + inScope: VersionsTracker.appVersionScope, + userDefaults: self.userDefaults) + + + public lazy var osVersion : VersionTracker = VersionTracker(currentVersion: Version.currentOSVersion, + inScope: VersionsTracker.osVersionScope, + userDefaults: self.userDefaults) + + /** + The user defaults to store the version history in. + */ + private let userDefaults: NSUserDefaults + + + + /** + **When using `VersionTracker` as a singleton, this should be called on each app launch.** + + Initializes the singleton causing it to load and update the version history. + + - parameter userDefaults: Pass in a NSUserDefaults object for storing and retrieving the version history. Defaults to `NSUserDefaults.standardUserDefaults()`. + + */ + public static func initialize(trackAppVersion trackAppVersion: Bool, trackOSVersion: Bool, withUserDefaults userDefaults: NSUserDefaults? = nil) { + if _sharedInstance != nil { + fatalError("โ—๏ธVersionsTracker.initialize() was already called before and must be called only once.") + } + _sharedInstance = VersionsTracker(trackAppVersion: trackAppVersion, + trackOSVersion: trackOSVersion, + withUserDefaults: userDefaults) + } + + /** + **When NOT using `VersionTracker` this should be called on each app launch.** + + Updates the version history once per app session. + + *It should but does not has to be called befor instantiating any instance. Any new instance will make sure the version history got updated internally.* + *However, calling* `updateVersionHistory()` *after the app is launched is recommended to not lose version updates if no instance gets ever initialized.* + + - parameter userDefaults: Pass in a NSUserDefaults object for storing and retrieving the version history. Defaults to `NSUserDefaults.standardUserDefaults()`. + */ + public static func updateVersionHistories(trackAppVersion trackAppVersion: Bool, trackOSVersion: Bool, withUserDefaults userDefaults: NSUserDefaults? = nil) { + let defaults = userDefaults ?? NSUserDefaults.standardUserDefaults() + if trackAppVersion { + let versionInfo = VersionTracker.updateVersionHistoryOnce( + withVersion: Version.currentAppVersion, + inScope: VersionsTracker.appVersionScope, + onUserDefaults: defaults) + if versionInfo == nil { + print("[VersionsTracker] โš ๏ธ App version history was already updated") + } + } + if trackOSVersion { + let versionInfo = VersionTracker.updateVersionHistoryOnce( + withVersion: Version.currentOSVersion, + inScope: VersionsTracker.osVersionScope, + onUserDefaults: defaults) + if versionInfo == nil { + print("[VersionsTracker] โš ๏ธ OS Version history was already updated") + } + } + } + + public init(trackAppVersion: Bool = false, trackOSVersion: Bool = false, withUserDefaults userDefaults: NSUserDefaults? = nil) { + self.userDefaults = userDefaults ?? NSUserDefaults.standardUserDefaults() + + if (trackAppVersion) { + // triggre version histroy update + self.appVersion.currentVersion + } + + if (trackOSVersion) { + // triggre version histroy update + self.osVersion.currentVersion + } + } + +} \ No newline at end of file diff --git a/README.md b/README.md index ce0774d..c761939 100644 --- a/README.md +++ b/README.md @@ -1,15 +1,33 @@ # VersionsTracker -[![CI Status](http://img.shields.io/travis/Martin Stemmle/VersionsTracker.svg?style=flat)](https://travis-ci.org/Martin Stemmle/VersionsTracker) +[![CI Status](http://img.shields.io/travis/maremmle/VersionsTracker.svg?style=flat)](https://travis-ci.org/Martin Stemmle/VersionsTracker) [![Version](https://img.shields.io/cocoapods/v/VersionsTracker.svg?style=flat)](http://cocoapods.org/pods/VersionsTracker) [![License](https://img.shields.io/cocoapods/l/VersionsTracker.svg?style=flat)](http://cocoapods.org/pods/VersionsTracker) [![Platform](https://img.shields.io/cocoapods/p/VersionsTracker.svg?style=flat)](http://cocoapods.org/pods/VersionsTracker) -## Usage -To run the example project, clone the repo, and run `pod install` from the Example directory first. + +**Keeping track of version installation history made easy.** + + +## Features + +- Track not just marketing version, but also build number and install date +- Track both App and OS version +- Access current version +- Check for version updates and first launch +- No use as Singleton required +- Ability to use custom NSUserDefaults (e.g. for [sharing it with extensions](https://developer.apple.com/library/ios/documentation/General/Conceptual/ExtensibilityPG/ExtensionScenarios.html) ) + + +## Example + +To run the example project, clone the repo, and run `pod install` from the Example directory first. Play with the Sample app's version and build numbers. ## Requirements +- Xcode 7.0+ +- iOS 8.0+ +- [Semantic Versioning](http://semver.org/) ## Installation @@ -20,6 +38,97 @@ it, simply add the following line to your Podfile: pod "VersionsTracker" ``` + + +## Usage + +### Initialization + +```swift +func application(application: UIApplication, didFinishLaunchingWithOptions launchOptions: [NSObject: AnyObject]?) -> Bool { + + if iDontMindSingletons { + VersionsTracker.initialize(trackAppVersion: true, trackOSVersion: true) + } + else { + // make sure to update the version history once once during you app's life time + VersionsTracker.updateVersionHistories(trackAppVersion: true, trackOSVersion: true) + } + + return true +} +``` + +### Get the current versions with build and install date + +```swift +let versionsTracker = iDontMindSingletons ? VersionsTracker.sharedInstance : VersionsTracker() + +let curAppVersion : Version = versionsTracker.appVersion.currentVersion +print("Current marketing version is \(curAppVersion.versionString) (Build \(curAppVersion.buildString)) and was first launched \(curAppVersion.installDate)") +``` + + +### Get the version history + +```swift +let versionsTracker = iDontMindSingletons ? VersionsTracker.sharedInstance : VersionsTracker() + +let appVersions: [Version] = versionsTracker.appVersion.versionHistory +let firstOSVerion : Version = versionsTracker.osVersion.versionHistory.first! +print("The very first time the app was launched on iOS \(firstOSVerion.versionString) on \(firstOSVerion.installDate)") + +``` + + +### Check version changes easily + +```swift +let versionsTracker = iDontMindSingletons ? VersionsTracker.sharedInstance : VersionsTracker() + +switch versionsTracker.appVersion.changeState { +case .Installed: + // ๐ŸŽ‰ Sweet, a new user just installed your app + // ... start tutorial / intro + +case NotChanged: + // ๐Ÿ˜ด nothing as changed + // ... nothing to do + +case Update(let previousVersion: Version): + // ๐Ÿ™ƒ new build of the same version + // ... hopefully it fixed those bugs the QA guy reported + +case Upgraded(let previousVersion: Version) + // ๐Ÿ˜„ marketing version increased + // ... migrate old app data + +case Downgraded(let previousVersion: Version) + // ๐Ÿ˜ต marketing version decreased (hopefully we are not on production) + // ... purge app data and start over + +} +``` + +Since the build is also kept track off, it enables detecting updates of it as well. However, the build fraction of a version is treated as an arbitrary string. This is to support any build number, from integer counts to git commit hashes. Therefor there is no way to determine the direction of a build change. + +### Compare Version + +```swift +let versionsTracker = iDontMindSingletons ? VersionsTracker.sharedInstance : VersionsTracker() +let curAppVersion : Version = versionsTracker.appVersion.currentVersion + +curAppVersion >= Version("1.2.3") +curAppVersion >= "1.2.3" +Version("1.2.3") < Version("3.2.1") +curAppVersion != Version("1.2.3", buildString: "B19") +``` + +Versions with the same marketing version but different build are not equal. + + + + ## Author Martin Stemmle, marste@msmix.de diff --git a/VersionsTracker.podspec b/VersionsTracker.podspec index 081bd4f..96403b7 100644 --- a/VersionsTracker.podspec +++ b/VersionsTracker.podspec @@ -9,22 +9,17 @@ Pod::Spec.new do |s| s.name = "VersionsTracker" s.version = "0.1.0" - s.summary = "A short description of VersionsTracker." - -# This description is used to generate tags and improve search results. -# * Think: What does it do? Why did you write it? What is the focus? -# * Try to keep it short, snappy and to the point. -# * Write the description between the DESC delimiters below. -# * Finally, don't worry about the indent, CocoaPods strips it! + s.summary = "Keeping track of version installation history made easy." s.description = <<-DESC + VersionsTracker is a Swift Library, which tracks install version history of app and os version. + It includes not just the marketing version, but also build number and install date. DESC - s.homepage = "https://github.com//VersionsTracker" - # s.screenshots = "www.example.com/screenshots_1", "www.example.com/screenshots_2" + s.homepage = "https://github.com/maremmle/VersionsTracker" s.license = 'MIT' s.author = { "Martin Stemmle" => "marste@msmix.de" } - s.source = { :git => "https://github.com//VersionsTracker.git", :tag => s.version.to_s } - # s.social_media_url = 'https://twitter.com/' + s.source = { :git => "https://github.com/maremmle/VersionsTracker.git", :tag => s.version.to_s } + s.social_media_url = 'https://twitter.com/maremmle' s.platform = :ios, '8.0' s.requires_arc = true @@ -34,7 +29,5 @@ Pod::Spec.new do |s| 'VersionsTracker' => ['Pod/Assets/*.png'] } - # s.public_header_files = 'Pod/Classes/**/*.h' - # s.frameworks = 'UIKit', 'MapKit' - # s.dependency 'AFNetworking', '~> 2.3' + end diff --git a/VersionsTracker.xcworkspace b/VersionsTracker.xcworkspace new file mode 120000 index 0000000..8821838 --- /dev/null +++ b/VersionsTracker.xcworkspace @@ -0,0 +1 @@ +Example/VersionsTracker.xcworkspace \ No newline at end of file