├── .gitignore ├── BetaExpiration.xcodeproj └── project.pbxproj ├── BetaExpiration ├── BetaExpiration.swift ├── BetaExpirationViewController.swift ├── BetaExpirationViewController.xib ├── BetaExpirationViewModel.swift ├── Clock.swift ├── Days.swift └── Info.plist ├── BetaExpirationTests ├── BetaExpirationTests.swift └── Info.plist ├── Example ├── AppDelegate.swift ├── Assets.xcassets │ └── AppIcon.appiconset │ │ └── Contents.json ├── Base.lproj │ └── MainMenu.xib └── Info.plist ├── LICENSE ├── README.md └── assets └── example.png /.gitignore: -------------------------------------------------------------------------------- 1 | # Xcode 2 | # 3 | # gitignore contributors: remember to update Global/Xcode.gitignore, Objective-C.gitignore & Swift.gitignore 4 | 5 | ## Build generated 6 | build/ 7 | DerivedData 8 | 9 | ## Various settings 10 | *.pbxuser 11 | !default.pbxuser 12 | *.mode1v3 13 | !default.mode1v3 14 | *.mode2v3 15 | !default.mode2v3 16 | *.perspectivev3 17 | !default.perspectivev3 18 | xcuserdata 19 | 20 | ## Other 21 | *.xccheckout 22 | *.moved-aside 23 | *.xcuserstate 24 | *.xcscmblueprint 25 | 26 | ## Obj-C/Swift specific 27 | *.hmap 28 | *.ipa 29 | 30 | ## Playgrounds 31 | timeline.xctimeline 32 | playground.xcworkspace 33 | 34 | # Swift Package Manager 35 | # 36 | # Add this line if you want to avoid checking in source code from Swift Package Manager dependencies. 37 | # Packages/ 38 | .build/ 39 | 40 | # CocoaPods 41 | # 42 | # We recommend against adding the Pods directory to your .gitignore. However 43 | # you should judge for yourself, the pros and cons are mentioned at: 44 | # https://guides.cocoapods.org/using/using-cocoapods.html#should-i-check-the-pods-directory-into-source-control 45 | # 46 | # Pods/ 47 | 48 | # Carthage 49 | # 50 | # Add this line if you want to avoid checking in source code from Carthage dependencies. 51 | Carthage/Checkouts 52 | 53 | Carthage/Build 54 | 55 | # fastlane 56 | # 57 | # It is recommended to not store the screenshots in the git repo. Instead, use fastlane to re-generate the 58 | # screenshots whenever they are needed. 59 | # For more information about the recommended setup visit: 60 | # https://github.com/fastlane/fastlane/blob/master/docs/Gitignore.md 61 | 62 | fastlane/report.xml 63 | fastlane/screenshots 64 | -------------------------------------------------------------------------------- /BetaExpiration.xcodeproj/project.pbxproj: -------------------------------------------------------------------------------- 1 | // !$*UTF8*$! 2 | { 3 | archiveVersion = 1; 4 | classes = { 5 | }; 6 | objectVersion = 46; 7 | objects = { 8 | 9 | /* Begin PBXBuildFile section */ 10 | 5067B4C81EAB412300428E2E /* BetaExpiration.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 5067B4BE1EAB412300428E2E /* BetaExpiration.framework */; }; 11 | 5067B4CD1EAB412300428E2E /* BetaExpirationTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5067B4CC1EAB412300428E2E /* BetaExpirationTests.swift */; }; 12 | 5067B4E21EAB413600428E2E /* BetaExpiration.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5067B4E11EAB413600428E2E /* BetaExpiration.swift */; }; 13 | 5067B4E41EAB414700428E2E /* Clock.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5067B4E31EAB414700428E2E /* Clock.swift */; }; 14 | 5067B4E61EAB415500428E2E /* Days.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5067B4E51EAB415500428E2E /* Days.swift */; }; 15 | 5067B50D1EAB44B400428E2E /* BetaExpirationViewModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5067B50C1EAB44B400428E2E /* BetaExpirationViewModel.swift */; }; 16 | 5067B5151EAB477400428E2E /* AppDelegate.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5067B5141EAB477400428E2E /* AppDelegate.swift */; }; 17 | 5067B5171EAB477400428E2E /* Assets.xcassets in Resources */ = {isa = PBXBuildFile; fileRef = 5067B5161EAB477400428E2E /* Assets.xcassets */; }; 18 | 5067B51A1EAB477400428E2E /* MainMenu.xib in Resources */ = {isa = PBXBuildFile; fileRef = 5067B5181EAB477400428E2E /* MainMenu.xib */; }; 19 | 5067B51F1EAB47D800428E2E /* BetaExpiration.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 5067B4BE1EAB412300428E2E /* BetaExpiration.framework */; }; 20 | 5067B5201EAB47D800428E2E /* BetaExpiration.framework in Embed Frameworks */ = {isa = PBXBuildFile; fileRef = 5067B4BE1EAB412300428E2E /* BetaExpiration.framework */; settings = {ATTRIBUTES = (CodeSignOnCopy, RemoveHeadersOnCopy, ); }; }; 21 | 5067B5281EAB48A000428E2E /* BetaExpirationViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5067B5261EAB48A000428E2E /* BetaExpirationViewController.swift */; }; 22 | 5067B5291EAB48A000428E2E /* BetaExpirationViewController.xib in Resources */ = {isa = PBXBuildFile; fileRef = 5067B5271EAB48A000428E2E /* BetaExpirationViewController.xib */; }; 23 | /* End PBXBuildFile section */ 24 | 25 | /* Begin PBXContainerItemProxy section */ 26 | 5067B4C91EAB412300428E2E /* PBXContainerItemProxy */ = { 27 | isa = PBXContainerItemProxy; 28 | containerPortal = 5067B4B51EAB412200428E2E /* Project object */; 29 | proxyType = 1; 30 | remoteGlobalIDString = 5067B4BD1EAB412300428E2E; 31 | remoteInfo = BetaExpiration; 32 | }; 33 | 5067B5211EAB47D800428E2E /* PBXContainerItemProxy */ = { 34 | isa = PBXContainerItemProxy; 35 | containerPortal = 5067B4B51EAB412200428E2E /* Project object */; 36 | proxyType = 1; 37 | remoteGlobalIDString = 5067B4BD1EAB412300428E2E; 38 | remoteInfo = BetaExpiration; 39 | }; 40 | /* End PBXContainerItemProxy section */ 41 | 42 | /* Begin PBXCopyFilesBuildPhase section */ 43 | 5067B5231EAB47D900428E2E /* Embed Frameworks */ = { 44 | isa = PBXCopyFilesBuildPhase; 45 | buildActionMask = 2147483647; 46 | dstPath = ""; 47 | dstSubfolderSpec = 10; 48 | files = ( 49 | 5067B5201EAB47D800428E2E /* BetaExpiration.framework in Embed Frameworks */, 50 | ); 51 | name = "Embed Frameworks"; 52 | runOnlyForDeploymentPostprocessing = 0; 53 | }; 54 | /* End PBXCopyFilesBuildPhase section */ 55 | 56 | /* Begin PBXFileReference section */ 57 | 5050CAC721AEB31400528988 /* LICENSE */ = {isa = PBXFileReference; lastKnownFileType = text; path = LICENSE; sourceTree = ""; }; 58 | 5050CAC821AEB31400528988 /* README.md */ = {isa = PBXFileReference; lastKnownFileType = net.daringfireball.markdown; path = README.md; sourceTree = ""; }; 59 | 5067B4BE1EAB412300428E2E /* BetaExpiration.framework */ = {isa = PBXFileReference; explicitFileType = wrapper.framework; includeInIndex = 0; path = BetaExpiration.framework; sourceTree = BUILT_PRODUCTS_DIR; }; 60 | 5067B4C21EAB412300428E2E /* Info.plist */ = {isa = PBXFileReference; lastKnownFileType = text.plist.xml; path = Info.plist; sourceTree = ""; }; 61 | 5067B4C71EAB412300428E2E /* BetaExpirationTests.xctest */ = {isa = PBXFileReference; explicitFileType = wrapper.cfbundle; includeInIndex = 0; path = BetaExpirationTests.xctest; sourceTree = BUILT_PRODUCTS_DIR; }; 62 | 5067B4CC1EAB412300428E2E /* BetaExpirationTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = BetaExpirationTests.swift; sourceTree = ""; }; 63 | 5067B4CE1EAB412300428E2E /* Info.plist */ = {isa = PBXFileReference; lastKnownFileType = text.plist.xml; path = Info.plist; sourceTree = ""; }; 64 | 5067B4E11EAB413600428E2E /* BetaExpiration.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = BetaExpiration.swift; sourceTree = ""; }; 65 | 5067B4E31EAB414700428E2E /* Clock.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = Clock.swift; sourceTree = ""; }; 66 | 5067B4E51EAB415500428E2E /* Days.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = Days.swift; sourceTree = ""; }; 67 | 5067B50C1EAB44B400428E2E /* BetaExpirationViewModel.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = BetaExpirationViewModel.swift; sourceTree = ""; }; 68 | 5067B5121EAB477400428E2E /* Example.app */ = {isa = PBXFileReference; explicitFileType = wrapper.application; includeInIndex = 0; path = Example.app; sourceTree = BUILT_PRODUCTS_DIR; }; 69 | 5067B5141EAB477400428E2E /* AppDelegate.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AppDelegate.swift; sourceTree = ""; }; 70 | 5067B5161EAB477400428E2E /* Assets.xcassets */ = {isa = PBXFileReference; lastKnownFileType = folder.assetcatalog; path = Assets.xcassets; sourceTree = ""; }; 71 | 5067B5191EAB477400428E2E /* Base */ = {isa = PBXFileReference; lastKnownFileType = file.xib; name = Base; path = Base.lproj/MainMenu.xib; sourceTree = ""; }; 72 | 5067B51B1EAB477400428E2E /* Info.plist */ = {isa = PBXFileReference; lastKnownFileType = text.plist.xml; path = Info.plist; sourceTree = ""; }; 73 | 5067B5261EAB48A000428E2E /* BetaExpirationViewController.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = BetaExpirationViewController.swift; sourceTree = ""; }; 74 | 5067B5271EAB48A000428E2E /* BetaExpirationViewController.xib */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = file.xib; path = BetaExpirationViewController.xib; sourceTree = ""; }; 75 | /* End PBXFileReference section */ 76 | 77 | /* Begin PBXFrameworksBuildPhase section */ 78 | 5067B4BA1EAB412300428E2E /* Frameworks */ = { 79 | isa = PBXFrameworksBuildPhase; 80 | buildActionMask = 2147483647; 81 | files = ( 82 | ); 83 | runOnlyForDeploymentPostprocessing = 0; 84 | }; 85 | 5067B4C41EAB412300428E2E /* Frameworks */ = { 86 | isa = PBXFrameworksBuildPhase; 87 | buildActionMask = 2147483647; 88 | files = ( 89 | 5067B4C81EAB412300428E2E /* BetaExpiration.framework in Frameworks */, 90 | ); 91 | runOnlyForDeploymentPostprocessing = 0; 92 | }; 93 | 5067B50F1EAB477400428E2E /* Frameworks */ = { 94 | isa = PBXFrameworksBuildPhase; 95 | buildActionMask = 2147483647; 96 | files = ( 97 | 5067B51F1EAB47D800428E2E /* BetaExpiration.framework in Frameworks */, 98 | ); 99 | runOnlyForDeploymentPostprocessing = 0; 100 | }; 101 | /* End PBXFrameworksBuildPhase section */ 102 | 103 | /* Begin PBXGroup section */ 104 | 5067B4B41EAB412200428E2E = { 105 | isa = PBXGroup; 106 | children = ( 107 | 5050CAC721AEB31400528988 /* LICENSE */, 108 | 5050CAC821AEB31400528988 /* README.md */, 109 | 5067B4C01EAB412300428E2E /* BetaExpiration */, 110 | 5067B4CB1EAB412300428E2E /* BetaExpirationTests */, 111 | 5067B5131EAB477400428E2E /* Example */, 112 | 5067B4BF1EAB412300428E2E /* Products */, 113 | ); 114 | sourceTree = ""; 115 | }; 116 | 5067B4BF1EAB412300428E2E /* Products */ = { 117 | isa = PBXGroup; 118 | children = ( 119 | 5067B4BE1EAB412300428E2E /* BetaExpiration.framework */, 120 | 5067B4C71EAB412300428E2E /* BetaExpirationTests.xctest */, 121 | 5067B5121EAB477400428E2E /* Example.app */, 122 | ); 123 | name = Products; 124 | sourceTree = ""; 125 | }; 126 | 5067B4C01EAB412300428E2E /* BetaExpiration */ = { 127 | isa = PBXGroup; 128 | children = ( 129 | 5067B4E31EAB414700428E2E /* Clock.swift */, 130 | 5067B4E51EAB415500428E2E /* Days.swift */, 131 | 5067B4E11EAB413600428E2E /* BetaExpiration.swift */, 132 | 5067B5261EAB48A000428E2E /* BetaExpirationViewController.swift */, 133 | 5067B5271EAB48A000428E2E /* BetaExpirationViewController.xib */, 134 | 5067B50C1EAB44B400428E2E /* BetaExpirationViewModel.swift */, 135 | 5067B4C21EAB412300428E2E /* Info.plist */, 136 | ); 137 | path = BetaExpiration; 138 | sourceTree = ""; 139 | }; 140 | 5067B4CB1EAB412300428E2E /* BetaExpirationTests */ = { 141 | isa = PBXGroup; 142 | children = ( 143 | 5067B4CC1EAB412300428E2E /* BetaExpirationTests.swift */, 144 | 5067B4CE1EAB412300428E2E /* Info.plist */, 145 | ); 146 | path = BetaExpirationTests; 147 | sourceTree = ""; 148 | }; 149 | 5067B5131EAB477400428E2E /* Example */ = { 150 | isa = PBXGroup; 151 | children = ( 152 | 5067B5141EAB477400428E2E /* AppDelegate.swift */, 153 | 5067B5161EAB477400428E2E /* Assets.xcassets */, 154 | 5067B5181EAB477400428E2E /* MainMenu.xib */, 155 | 5067B51B1EAB477400428E2E /* Info.plist */, 156 | ); 157 | path = Example; 158 | sourceTree = ""; 159 | }; 160 | /* End PBXGroup section */ 161 | 162 | /* Begin PBXHeadersBuildPhase section */ 163 | 5067B4BB1EAB412300428E2E /* Headers */ = { 164 | isa = PBXHeadersBuildPhase; 165 | buildActionMask = 2147483647; 166 | files = ( 167 | ); 168 | runOnlyForDeploymentPostprocessing = 0; 169 | }; 170 | /* End PBXHeadersBuildPhase section */ 171 | 172 | /* Begin PBXNativeTarget section */ 173 | 5067B4BD1EAB412300428E2E /* BetaExpiration */ = { 174 | isa = PBXNativeTarget; 175 | buildConfigurationList = 5067B4D21EAB412300428E2E /* Build configuration list for PBXNativeTarget "BetaExpiration" */; 176 | buildPhases = ( 177 | 5067B4B91EAB412300428E2E /* Sources */, 178 | 5067B4BA1EAB412300428E2E /* Frameworks */, 179 | 5067B4BB1EAB412300428E2E /* Headers */, 180 | 5067B4BC1EAB412300428E2E /* Resources */, 181 | ); 182 | buildRules = ( 183 | ); 184 | dependencies = ( 185 | ); 186 | name = BetaExpiration; 187 | productName = BetaExpiration; 188 | productReference = 5067B4BE1EAB412300428E2E /* BetaExpiration.framework */; 189 | productType = "com.apple.product-type.framework"; 190 | }; 191 | 5067B4C61EAB412300428E2E /* BetaExpirationTests */ = { 192 | isa = PBXNativeTarget; 193 | buildConfigurationList = 5067B4D51EAB412300428E2E /* Build configuration list for PBXNativeTarget "BetaExpirationTests" */; 194 | buildPhases = ( 195 | 5067B4C31EAB412300428E2E /* Sources */, 196 | 5067B4C41EAB412300428E2E /* Frameworks */, 197 | 5067B4C51EAB412300428E2E /* Resources */, 198 | ); 199 | buildRules = ( 200 | ); 201 | dependencies = ( 202 | 5067B4CA1EAB412300428E2E /* PBXTargetDependency */, 203 | ); 204 | name = BetaExpirationTests; 205 | productName = BetaExpirationTests; 206 | productReference = 5067B4C71EAB412300428E2E /* BetaExpirationTests.xctest */; 207 | productType = "com.apple.product-type.bundle.unit-test"; 208 | }; 209 | 5067B5111EAB477400428E2E /* Example */ = { 210 | isa = PBXNativeTarget; 211 | buildConfigurationList = 5067B51C1EAB477400428E2E /* Build configuration list for PBXNativeTarget "Example" */; 212 | buildPhases = ( 213 | 5067B50E1EAB477400428E2E /* Sources */, 214 | 5067B50F1EAB477400428E2E /* Frameworks */, 215 | 5067B5101EAB477400428E2E /* Resources */, 216 | 5067B5231EAB47D900428E2E /* Embed Frameworks */, 217 | ); 218 | buildRules = ( 219 | ); 220 | dependencies = ( 221 | 5067B5221EAB47D800428E2E /* PBXTargetDependency */, 222 | ); 223 | name = Example; 224 | productName = Example; 225 | productReference = 5067B5121EAB477400428E2E /* Example.app */; 226 | productType = "com.apple.product-type.application"; 227 | }; 228 | /* End PBXNativeTarget section */ 229 | 230 | /* Begin PBXProject section */ 231 | 5067B4B51EAB412200428E2E /* Project object */ = { 232 | isa = PBXProject; 233 | attributes = { 234 | LastSwiftUpdateCheck = 0820; 235 | LastUpgradeCheck = 1020; 236 | ORGANIZATIONNAME = "Christian Tietze"; 237 | TargetAttributes = { 238 | 5067B4BD1EAB412300428E2E = { 239 | CreatedOnToolsVersion = 8.2.1; 240 | DevelopmentTeam = FRMDA3XRGC; 241 | LastSwiftMigration = 1020; 242 | ProvisioningStyle = Automatic; 243 | }; 244 | 5067B4C61EAB412300428E2E = { 245 | CreatedOnToolsVersion = 8.2.1; 246 | DevelopmentTeam = FRMDA3XRGC; 247 | LastSwiftMigration = 1020; 248 | ProvisioningStyle = Automatic; 249 | }; 250 | 5067B5111EAB477400428E2E = { 251 | CreatedOnToolsVersion = 8.2.1; 252 | DevelopmentTeam = FRMDA3XRGC; 253 | LastSwiftMigration = 1020; 254 | ProvisioningStyle = Automatic; 255 | }; 256 | }; 257 | }; 258 | buildConfigurationList = 5067B4B81EAB412200428E2E /* Build configuration list for PBXProject "BetaExpiration" */; 259 | compatibilityVersion = "Xcode 3.2"; 260 | developmentRegion = en; 261 | hasScannedForEncodings = 0; 262 | knownRegions = ( 263 | en, 264 | Base, 265 | ); 266 | mainGroup = 5067B4B41EAB412200428E2E; 267 | productRefGroup = 5067B4BF1EAB412300428E2E /* Products */; 268 | projectDirPath = ""; 269 | projectRoot = ""; 270 | targets = ( 271 | 5067B4BD1EAB412300428E2E /* BetaExpiration */, 272 | 5067B4C61EAB412300428E2E /* BetaExpirationTests */, 273 | 5067B5111EAB477400428E2E /* Example */, 274 | ); 275 | }; 276 | /* End PBXProject section */ 277 | 278 | /* Begin PBXResourcesBuildPhase section */ 279 | 5067B4BC1EAB412300428E2E /* Resources */ = { 280 | isa = PBXResourcesBuildPhase; 281 | buildActionMask = 2147483647; 282 | files = ( 283 | 5067B5291EAB48A000428E2E /* BetaExpirationViewController.xib in Resources */, 284 | ); 285 | runOnlyForDeploymentPostprocessing = 0; 286 | }; 287 | 5067B4C51EAB412300428E2E /* Resources */ = { 288 | isa = PBXResourcesBuildPhase; 289 | buildActionMask = 2147483647; 290 | files = ( 291 | ); 292 | runOnlyForDeploymentPostprocessing = 0; 293 | }; 294 | 5067B5101EAB477400428E2E /* Resources */ = { 295 | isa = PBXResourcesBuildPhase; 296 | buildActionMask = 2147483647; 297 | files = ( 298 | 5067B5171EAB477400428E2E /* Assets.xcassets in Resources */, 299 | 5067B51A1EAB477400428E2E /* MainMenu.xib in Resources */, 300 | ); 301 | runOnlyForDeploymentPostprocessing = 0; 302 | }; 303 | /* End PBXResourcesBuildPhase section */ 304 | 305 | /* Begin PBXSourcesBuildPhase section */ 306 | 5067B4B91EAB412300428E2E /* Sources */ = { 307 | isa = PBXSourcesBuildPhase; 308 | buildActionMask = 2147483647; 309 | files = ( 310 | 5067B4E41EAB414700428E2E /* Clock.swift in Sources */, 311 | 5067B4E61EAB415500428E2E /* Days.swift in Sources */, 312 | 5067B5281EAB48A000428E2E /* BetaExpirationViewController.swift in Sources */, 313 | 5067B50D1EAB44B400428E2E /* BetaExpirationViewModel.swift in Sources */, 314 | 5067B4E21EAB413600428E2E /* BetaExpiration.swift in Sources */, 315 | ); 316 | runOnlyForDeploymentPostprocessing = 0; 317 | }; 318 | 5067B4C31EAB412300428E2E /* Sources */ = { 319 | isa = PBXSourcesBuildPhase; 320 | buildActionMask = 2147483647; 321 | files = ( 322 | 5067B4CD1EAB412300428E2E /* BetaExpirationTests.swift in Sources */, 323 | ); 324 | runOnlyForDeploymentPostprocessing = 0; 325 | }; 326 | 5067B50E1EAB477400428E2E /* Sources */ = { 327 | isa = PBXSourcesBuildPhase; 328 | buildActionMask = 2147483647; 329 | files = ( 330 | 5067B5151EAB477400428E2E /* AppDelegate.swift in Sources */, 331 | ); 332 | runOnlyForDeploymentPostprocessing = 0; 333 | }; 334 | /* End PBXSourcesBuildPhase section */ 335 | 336 | /* Begin PBXTargetDependency section */ 337 | 5067B4CA1EAB412300428E2E /* PBXTargetDependency */ = { 338 | isa = PBXTargetDependency; 339 | target = 5067B4BD1EAB412300428E2E /* BetaExpiration */; 340 | targetProxy = 5067B4C91EAB412300428E2E /* PBXContainerItemProxy */; 341 | }; 342 | 5067B5221EAB47D800428E2E /* PBXTargetDependency */ = { 343 | isa = PBXTargetDependency; 344 | target = 5067B4BD1EAB412300428E2E /* BetaExpiration */; 345 | targetProxy = 5067B5211EAB47D800428E2E /* PBXContainerItemProxy */; 346 | }; 347 | /* End PBXTargetDependency section */ 348 | 349 | /* Begin PBXVariantGroup section */ 350 | 5067B5181EAB477400428E2E /* MainMenu.xib */ = { 351 | isa = PBXVariantGroup; 352 | children = ( 353 | 5067B5191EAB477400428E2E /* Base */, 354 | ); 355 | name = MainMenu.xib; 356 | sourceTree = ""; 357 | }; 358 | /* End PBXVariantGroup section */ 359 | 360 | /* Begin XCBuildConfiguration section */ 361 | 5067B4D01EAB412300428E2E /* Debug */ = { 362 | isa = XCBuildConfiguration; 363 | buildSettings = { 364 | ALWAYS_SEARCH_USER_PATHS = NO; 365 | CLANG_ANALYZER_LOCALIZABILITY_NONLOCALIZED = YES; 366 | CLANG_ANALYZER_NONNULL = YES; 367 | CLANG_CXX_LANGUAGE_STANDARD = "gnu++0x"; 368 | CLANG_CXX_LIBRARY = "libc++"; 369 | CLANG_ENABLE_MODULES = YES; 370 | CLANG_ENABLE_OBJC_ARC = YES; 371 | CLANG_WARN_BLOCK_CAPTURE_AUTORELEASING = YES; 372 | CLANG_WARN_BOOL_CONVERSION = YES; 373 | CLANG_WARN_COMMA = YES; 374 | CLANG_WARN_CONSTANT_CONVERSION = YES; 375 | CLANG_WARN_DEPRECATED_OBJC_IMPLEMENTATIONS = YES; 376 | CLANG_WARN_DIRECT_OBJC_ISA_USAGE = YES_ERROR; 377 | CLANG_WARN_DOCUMENTATION_COMMENTS = YES; 378 | CLANG_WARN_EMPTY_BODY = YES; 379 | CLANG_WARN_ENUM_CONVERSION = YES; 380 | CLANG_WARN_INFINITE_RECURSION = YES; 381 | CLANG_WARN_INT_CONVERSION = YES; 382 | CLANG_WARN_NON_LITERAL_NULL_CONVERSION = YES; 383 | CLANG_WARN_OBJC_IMPLICIT_RETAIN_SELF = YES; 384 | CLANG_WARN_OBJC_LITERAL_CONVERSION = YES; 385 | CLANG_WARN_OBJC_ROOT_CLASS = YES_ERROR; 386 | CLANG_WARN_RANGE_LOOP_ANALYSIS = YES; 387 | CLANG_WARN_STRICT_PROTOTYPES = YES; 388 | CLANG_WARN_SUSPICIOUS_MOVE = YES; 389 | CLANG_WARN_UNREACHABLE_CODE = YES; 390 | CLANG_WARN__DUPLICATE_METHOD_MATCH = YES; 391 | CODE_SIGN_IDENTITY = "-"; 392 | COPY_PHASE_STRIP = NO; 393 | CURRENT_PROJECT_VERSION = 1; 394 | DEBUG_INFORMATION_FORMAT = dwarf; 395 | ENABLE_STRICT_OBJC_MSGSEND = YES; 396 | ENABLE_TESTABILITY = YES; 397 | GCC_C_LANGUAGE_STANDARD = gnu99; 398 | GCC_DYNAMIC_NO_PIC = NO; 399 | GCC_NO_COMMON_BLOCKS = YES; 400 | GCC_OPTIMIZATION_LEVEL = 0; 401 | GCC_PREPROCESSOR_DEFINITIONS = ( 402 | "DEBUG=1", 403 | "$(inherited)", 404 | ); 405 | GCC_WARN_64_TO_32_BIT_CONVERSION = YES; 406 | GCC_WARN_ABOUT_RETURN_TYPE = YES_ERROR; 407 | GCC_WARN_UNDECLARED_SELECTOR = YES; 408 | GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE; 409 | GCC_WARN_UNUSED_FUNCTION = YES; 410 | GCC_WARN_UNUSED_VARIABLE = YES; 411 | MACOSX_DEPLOYMENT_TARGET = 10.11; 412 | MTL_ENABLE_DEBUG_INFO = YES; 413 | ONLY_ACTIVE_ARCH = YES; 414 | SDKROOT = macosx; 415 | SWIFT_ACTIVE_COMPILATION_CONDITIONS = DEBUG; 416 | SWIFT_OPTIMIZATION_LEVEL = "-Onone"; 417 | VERSIONING_SYSTEM = "apple-generic"; 418 | VERSION_INFO_PREFIX = ""; 419 | }; 420 | name = Debug; 421 | }; 422 | 5067B4D11EAB412300428E2E /* Release */ = { 423 | isa = XCBuildConfiguration; 424 | buildSettings = { 425 | ALWAYS_SEARCH_USER_PATHS = NO; 426 | CLANG_ANALYZER_LOCALIZABILITY_NONLOCALIZED = YES; 427 | CLANG_ANALYZER_NONNULL = YES; 428 | CLANG_CXX_LANGUAGE_STANDARD = "gnu++0x"; 429 | CLANG_CXX_LIBRARY = "libc++"; 430 | CLANG_ENABLE_MODULES = YES; 431 | CLANG_ENABLE_OBJC_ARC = YES; 432 | CLANG_WARN_BLOCK_CAPTURE_AUTORELEASING = YES; 433 | CLANG_WARN_BOOL_CONVERSION = YES; 434 | CLANG_WARN_COMMA = YES; 435 | CLANG_WARN_CONSTANT_CONVERSION = YES; 436 | CLANG_WARN_DEPRECATED_OBJC_IMPLEMENTATIONS = YES; 437 | CLANG_WARN_DIRECT_OBJC_ISA_USAGE = YES_ERROR; 438 | CLANG_WARN_DOCUMENTATION_COMMENTS = YES; 439 | CLANG_WARN_EMPTY_BODY = YES; 440 | CLANG_WARN_ENUM_CONVERSION = YES; 441 | CLANG_WARN_INFINITE_RECURSION = YES; 442 | CLANG_WARN_INT_CONVERSION = YES; 443 | CLANG_WARN_NON_LITERAL_NULL_CONVERSION = YES; 444 | CLANG_WARN_OBJC_IMPLICIT_RETAIN_SELF = YES; 445 | CLANG_WARN_OBJC_LITERAL_CONVERSION = YES; 446 | CLANG_WARN_OBJC_ROOT_CLASS = YES_ERROR; 447 | CLANG_WARN_RANGE_LOOP_ANALYSIS = YES; 448 | CLANG_WARN_STRICT_PROTOTYPES = YES; 449 | CLANG_WARN_SUSPICIOUS_MOVE = YES; 450 | CLANG_WARN_UNREACHABLE_CODE = YES; 451 | CLANG_WARN__DUPLICATE_METHOD_MATCH = YES; 452 | CODE_SIGN_IDENTITY = "-"; 453 | COPY_PHASE_STRIP = NO; 454 | CURRENT_PROJECT_VERSION = 1; 455 | DEBUG_INFORMATION_FORMAT = "dwarf-with-dsym"; 456 | ENABLE_NS_ASSERTIONS = NO; 457 | ENABLE_STRICT_OBJC_MSGSEND = YES; 458 | GCC_C_LANGUAGE_STANDARD = gnu99; 459 | GCC_NO_COMMON_BLOCKS = YES; 460 | GCC_WARN_64_TO_32_BIT_CONVERSION = YES; 461 | GCC_WARN_ABOUT_RETURN_TYPE = YES_ERROR; 462 | GCC_WARN_UNDECLARED_SELECTOR = YES; 463 | GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE; 464 | GCC_WARN_UNUSED_FUNCTION = YES; 465 | GCC_WARN_UNUSED_VARIABLE = YES; 466 | MACOSX_DEPLOYMENT_TARGET = 10.11; 467 | MTL_ENABLE_DEBUG_INFO = NO; 468 | SDKROOT = macosx; 469 | SWIFT_OPTIMIZATION_LEVEL = "-Owholemodule"; 470 | VERSIONING_SYSTEM = "apple-generic"; 471 | VERSION_INFO_PREFIX = ""; 472 | }; 473 | name = Release; 474 | }; 475 | 5067B4D31EAB412300428E2E /* Debug */ = { 476 | isa = XCBuildConfiguration; 477 | buildSettings = { 478 | CODE_SIGN_IDENTITY = ""; 479 | COMBINE_HIDPI_IMAGES = YES; 480 | DEFINES_MODULE = YES; 481 | DEVELOPMENT_TEAM = FRMDA3XRGC; 482 | DYLIB_COMPATIBILITY_VERSION = 1; 483 | DYLIB_CURRENT_VERSION = 1; 484 | DYLIB_INSTALL_NAME_BASE = "@rpath"; 485 | FRAMEWORK_VERSION = A; 486 | INFOPLIST_FILE = BetaExpiration/Info.plist; 487 | INSTALL_PATH = "$(LOCAL_LIBRARY_DIR)/Frameworks"; 488 | LD_RUNPATH_SEARCH_PATHS = "$(inherited) @executable_path/../Frameworks @loader_path/Frameworks"; 489 | PRODUCT_BUNDLE_IDENTIFIER = de.christiantietze.BetaExpiration; 490 | PRODUCT_NAME = "$(TARGET_NAME)"; 491 | SKIP_INSTALL = YES; 492 | SWIFT_VERSION = 5.0; 493 | }; 494 | name = Debug; 495 | }; 496 | 5067B4D41EAB412300428E2E /* Release */ = { 497 | isa = XCBuildConfiguration; 498 | buildSettings = { 499 | CODE_SIGN_IDENTITY = ""; 500 | COMBINE_HIDPI_IMAGES = YES; 501 | DEFINES_MODULE = YES; 502 | DEVELOPMENT_TEAM = FRMDA3XRGC; 503 | DYLIB_COMPATIBILITY_VERSION = 1; 504 | DYLIB_CURRENT_VERSION = 1; 505 | DYLIB_INSTALL_NAME_BASE = "@rpath"; 506 | FRAMEWORK_VERSION = A; 507 | INFOPLIST_FILE = BetaExpiration/Info.plist; 508 | INSTALL_PATH = "$(LOCAL_LIBRARY_DIR)/Frameworks"; 509 | LD_RUNPATH_SEARCH_PATHS = "$(inherited) @executable_path/../Frameworks @loader_path/Frameworks"; 510 | PRODUCT_BUNDLE_IDENTIFIER = de.christiantietze.BetaExpiration; 511 | PRODUCT_NAME = "$(TARGET_NAME)"; 512 | SKIP_INSTALL = YES; 513 | SWIFT_VERSION = 5.0; 514 | }; 515 | name = Release; 516 | }; 517 | 5067B4D61EAB412300428E2E /* Debug */ = { 518 | isa = XCBuildConfiguration; 519 | buildSettings = { 520 | ALWAYS_EMBED_SWIFT_STANDARD_LIBRARIES = YES; 521 | COMBINE_HIDPI_IMAGES = YES; 522 | DEVELOPMENT_TEAM = FRMDA3XRGC; 523 | INFOPLIST_FILE = BetaExpirationTests/Info.plist; 524 | LD_RUNPATH_SEARCH_PATHS = "$(inherited) @executable_path/../Frameworks @loader_path/../Frameworks"; 525 | PRODUCT_BUNDLE_IDENTIFIER = de.christiantietze.BetaExpirationTests; 526 | PRODUCT_NAME = "$(TARGET_NAME)"; 527 | SWIFT_VERSION = 5.0; 528 | }; 529 | name = Debug; 530 | }; 531 | 5067B4D71EAB412300428E2E /* Release */ = { 532 | isa = XCBuildConfiguration; 533 | buildSettings = { 534 | ALWAYS_EMBED_SWIFT_STANDARD_LIBRARIES = YES; 535 | COMBINE_HIDPI_IMAGES = YES; 536 | DEVELOPMENT_TEAM = FRMDA3XRGC; 537 | INFOPLIST_FILE = BetaExpirationTests/Info.plist; 538 | LD_RUNPATH_SEARCH_PATHS = "$(inherited) @executable_path/../Frameworks @loader_path/../Frameworks"; 539 | PRODUCT_BUNDLE_IDENTIFIER = de.christiantietze.BetaExpirationTests; 540 | PRODUCT_NAME = "$(TARGET_NAME)"; 541 | SWIFT_VERSION = 5.0; 542 | }; 543 | name = Release; 544 | }; 545 | 5067B51D1EAB477400428E2E /* Debug */ = { 546 | isa = XCBuildConfiguration; 547 | buildSettings = { 548 | ALWAYS_EMBED_SWIFT_STANDARD_LIBRARIES = YES; 549 | ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon; 550 | COMBINE_HIDPI_IMAGES = YES; 551 | DEVELOPMENT_TEAM = FRMDA3XRGC; 552 | INFOPLIST_FILE = Example/Info.plist; 553 | LD_RUNPATH_SEARCH_PATHS = "$(inherited) @executable_path/../Frameworks"; 554 | PRODUCT_BUNDLE_IDENTIFIER = de.christiantietze.Example; 555 | PRODUCT_NAME = "$(TARGET_NAME)"; 556 | SWIFT_VERSION = 5.0; 557 | }; 558 | name = Debug; 559 | }; 560 | 5067B51E1EAB477400428E2E /* Release */ = { 561 | isa = XCBuildConfiguration; 562 | buildSettings = { 563 | ALWAYS_EMBED_SWIFT_STANDARD_LIBRARIES = YES; 564 | ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon; 565 | COMBINE_HIDPI_IMAGES = YES; 566 | DEVELOPMENT_TEAM = FRMDA3XRGC; 567 | INFOPLIST_FILE = Example/Info.plist; 568 | LD_RUNPATH_SEARCH_PATHS = "$(inherited) @executable_path/../Frameworks"; 569 | PRODUCT_BUNDLE_IDENTIFIER = de.christiantietze.Example; 570 | PRODUCT_NAME = "$(TARGET_NAME)"; 571 | SWIFT_VERSION = 5.0; 572 | }; 573 | name = Release; 574 | }; 575 | /* End XCBuildConfiguration section */ 576 | 577 | /* Begin XCConfigurationList section */ 578 | 5067B4B81EAB412200428E2E /* Build configuration list for PBXProject "BetaExpiration" */ = { 579 | isa = XCConfigurationList; 580 | buildConfigurations = ( 581 | 5067B4D01EAB412300428E2E /* Debug */, 582 | 5067B4D11EAB412300428E2E /* Release */, 583 | ); 584 | defaultConfigurationIsVisible = 0; 585 | defaultConfigurationName = Release; 586 | }; 587 | 5067B4D21EAB412300428E2E /* Build configuration list for PBXNativeTarget "BetaExpiration" */ = { 588 | isa = XCConfigurationList; 589 | buildConfigurations = ( 590 | 5067B4D31EAB412300428E2E /* Debug */, 591 | 5067B4D41EAB412300428E2E /* Release */, 592 | ); 593 | defaultConfigurationIsVisible = 0; 594 | defaultConfigurationName = Release; 595 | }; 596 | 5067B4D51EAB412300428E2E /* Build configuration list for PBXNativeTarget "BetaExpirationTests" */ = { 597 | isa = XCConfigurationList; 598 | buildConfigurations = ( 599 | 5067B4D61EAB412300428E2E /* Debug */, 600 | 5067B4D71EAB412300428E2E /* Release */, 601 | ); 602 | defaultConfigurationIsVisible = 0; 603 | defaultConfigurationName = Release; 604 | }; 605 | 5067B51C1EAB477400428E2E /* Build configuration list for PBXNativeTarget "Example" */ = { 606 | isa = XCConfigurationList; 607 | buildConfigurations = ( 608 | 5067B51D1EAB477400428E2E /* Debug */, 609 | 5067B51E1EAB477400428E2E /* Release */, 610 | ); 611 | defaultConfigurationIsVisible = 0; 612 | defaultConfigurationName = Release; 613 | }; 614 | /* End XCConfigurationList section */ 615 | }; 616 | rootObject = 5067B4B51EAB412200428E2E /* Project object */; 617 | } 618 | -------------------------------------------------------------------------------- /BetaExpiration/BetaExpiration.swift: -------------------------------------------------------------------------------- 1 | // Copyright © 2017 Christian Tietze. All rights reserved. Distributed under the MIT License. 2 | 3 | import Cocoa 4 | 5 | public struct BetaExpiration { 6 | 7 | public static let expiresAt = Date(timeIntervalSinceReferenceDate: 543425368.016923) 8 | 9 | public static func daysLeft(_ clock: Clock = SystemClock()) -> Days { 10 | 11 | let interval = expiresAt.timeIntervalSince(clock.now()) 12 | let amount = Days.amount(timeInterval: interval) 13 | return Days(amount) 14 | } 15 | 16 | public static func isExpired(_ clock: Clock = SystemClock()) -> Bool { 17 | 18 | return clock.now() > expiresAt 19 | } 20 | 21 | public static func guardExpiration() { 22 | 23 | if !isExpired() { return } 24 | 25 | let alert = NSAlert() 26 | alert.messageText = "Your beta has expired. Please download a new version or get in touch:\nsomeone@example.com" 27 | alert.addButton(withTitle: "Quit") 28 | alert.runModal() 29 | NSApp.terminate(nil) 30 | } 31 | 32 | /// Puts a titlebar accessory into the window, for example: 33 | /// 34 | /// β v1.2.0 (107) - 23 days left 35 | /// ^ ^ ^ 36 | /// | | |__ = (today - expiration date) 37 | /// | | 38 | /// | |__ build number 39 | /// | 40 | /// |__ version 41 | /// 42 | /// - parameter window: Window where the `NSTitlebarAccessoryViewController` 43 | /// is added to. 44 | /// - parameter hostBundle: The app's `Bundle` to obtain metadata. 45 | public static func decorate(window: NSWindow?, hostBundle: Bundle) { 46 | 47 | guard let window = window else { return } 48 | 49 | let viewController = BetaExpirationViewController() 50 | window.addTitlebarAccessoryViewController(viewController.titlebarAccessoryViewController()) 51 | 52 | let viewModel = BetaExpirationViewModel( 53 | bundle: hostBundle, 54 | daysLeft: String(describing: daysLeft())) 55 | viewController.display(viewModel: viewModel) 56 | } 57 | } 58 | 59 | extension BetaExpirationViewModel { 60 | 61 | public init(bundle: Bundle, daysLeft: String) { 62 | 63 | self.init( 64 | version: bundle.infoDictionary?["CFBundleShortVersionString"] as? String ?? "?", 65 | build: bundle.infoDictionary?["CFBundleVersion"] as? String ?? "?", 66 | daysLeft: daysLeft) 67 | } 68 | } 69 | -------------------------------------------------------------------------------- /BetaExpiration/BetaExpirationViewController.swift: -------------------------------------------------------------------------------- 1 | // Copyright © 2017 Christian Tietze. All rights reserved. Distributed under the MIT License. 2 | 3 | import Cocoa 4 | 5 | public class BetaExpirationViewController: NSViewController { 6 | 7 | public convenience init() { 8 | self.init(nibName: "BetaExpirationViewController", bundle: Bundle(for: BetaExpirationViewController.self)) 9 | } 10 | 11 | @IBOutlet weak var label: NSTextField! 12 | 13 | public override func awakeFromNib() { 14 | super.awakeFromNib() 15 | 16 | label.drawsBackground = true 17 | } 18 | 19 | /// Expects `label` to have the following text in Interface Builder: 20 | /// DD days left (VVVV) 21 | public func display(viewModel: BetaExpirationViewModel) { 22 | 23 | label.stringValue = label.stringValue 24 | .replacingOccurrences(of: "DD", with: "\(viewModel.daysLeft)") 25 | .replacingOccurrences(of: "VVVV", with: viewModel.version) 26 | .replacingOccurrences(of: "BB", with: viewModel.build) 27 | } 28 | 29 | public func titlebarAccessoryViewController() -> NSTitlebarAccessoryViewController { 30 | 31 | let accessoryViewController = NSTitlebarAccessoryViewController() 32 | accessoryViewController.view = view 33 | accessoryViewController.layoutAttribute = .right 34 | 35 | return accessoryViewController 36 | } 37 | } 38 | -------------------------------------------------------------------------------- /BetaExpiration/BetaExpirationViewController.xib: -------------------------------------------------------------------------------- 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 | -------------------------------------------------------------------------------- /BetaExpiration/BetaExpirationViewModel.swift: -------------------------------------------------------------------------------- 1 | // Copyright © 2017 Christian Tietze. All rights reserved. Distributed under the MIT License. 2 | 3 | public struct BetaExpirationViewModel { 4 | 5 | public let version: String 6 | public let build: String 7 | public let daysLeft: String 8 | 9 | public init(version: String, build: String, daysLeft: String) { 10 | 11 | self.version = version 12 | self.build = build 13 | self.daysLeft = daysLeft 14 | } 15 | } 16 | -------------------------------------------------------------------------------- /BetaExpiration/Clock.swift: -------------------------------------------------------------------------------- 1 | // Copyright © 2017 Christian Tietze. All rights reserved. Distributed under the MIT License. 2 | 3 | import struct Foundation.Date 4 | 5 | public protocol Clock { 6 | func now() -> Date 7 | } 8 | 9 | public struct SystemClock: Clock { 10 | public init() { } 11 | 12 | public func now() -> Date { 13 | return Date() 14 | } 15 | } 16 | -------------------------------------------------------------------------------- /BetaExpiration/Days.swift: -------------------------------------------------------------------------------- 1 | // Copyright © 2017 Christian Tietze. All rights reserved. Distributed under the MIT License. 2 | 3 | import Foundation 4 | 5 | public struct Days { 6 | 7 | public static func timeInterval(amount: Double) -> Double { 8 | return amount * 60 * 60 * 24 9 | } 10 | 11 | public static func amount(timeInterval: TimeInterval) -> Double { 12 | return timeInterval / 60 / 60 / 24 13 | } 14 | 15 | public let amount: Double 16 | 17 | /// Rounded to the next integer. 18 | public var userFacingAmount: Int { 19 | 20 | return Int(ceil(amount)) 21 | } 22 | 23 | public init(_ anAmount: Double) { 24 | amount = anAmount 25 | } 26 | 27 | public var timeInterval: TimeInterval { 28 | return Days.timeInterval(amount: amount) 29 | } 30 | 31 | public var isPast: Bool { 32 | return amount < 0 33 | } 34 | } 35 | 36 | extension Days: CustomStringConvertible { 37 | 38 | public var description: String { 39 | return "\(userFacingAmount)" 40 | } 41 | } 42 | 43 | extension Days: Equatable { } 44 | 45 | public func ==(lhs: Days, rhs: Days) -> Bool { 46 | 47 | return lhs.amount == rhs.amount 48 | } 49 | -------------------------------------------------------------------------------- /BetaExpiration/Info.plist: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | CFBundleDevelopmentRegion 6 | en 7 | CFBundleExecutable 8 | $(EXECUTABLE_NAME) 9 | CFBundleIdentifier 10 | $(PRODUCT_BUNDLE_IDENTIFIER) 11 | CFBundleInfoDictionaryVersion 12 | 6.0 13 | CFBundleName 14 | $(PRODUCT_NAME) 15 | CFBundlePackageType 16 | FMWK 17 | CFBundleShortVersionString 18 | 1.0 19 | CFBundleVersion 20 | $(CURRENT_PROJECT_VERSION) 21 | NSHumanReadableCopyright 22 | Copyright © 2017 Christian Tietze. All rights reserved. 23 | NSPrincipalClass 24 | 25 | 26 | 27 | -------------------------------------------------------------------------------- /BetaExpirationTests/BetaExpirationTests.swift: -------------------------------------------------------------------------------- 1 | // Copyright © 2017 Christian Tietze. All rights reserved. Distributed under the MIT License. 2 | 3 | import XCTest 4 | @testable import BetaExpiration 5 | 6 | class BetaExpirationTests: XCTestCase { 7 | 8 | func testNotTimeForUpdateYet() { 9 | 10 | let then = BetaExpiration.expiresAt 11 | let calendar = Calendar.current 12 | let threshold = (calendar as NSCalendar).date(byAdding: .day, value: -10, to: then, options: [])! 13 | let now = Date() 14 | XCTAssertFalse(now > threshold) 15 | } 16 | 17 | func testComputeNextExpirationDate() { 18 | let now = Date() 19 | let calendar = Calendar.current 20 | var comps = DateComponents() 21 | comps.day = 30 22 | let expiresAt = (calendar as NSCalendar).date(byAdding: comps, to: now, options: [])! 23 | let interval = expiresAt.timeIntervalSinceReferenceDate 24 | print(interval) 25 | } 26 | } 27 | -------------------------------------------------------------------------------- /BetaExpirationTests/Info.plist: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | CFBundleDevelopmentRegion 6 | en 7 | CFBundleExecutable 8 | $(EXECUTABLE_NAME) 9 | CFBundleIdentifier 10 | $(PRODUCT_BUNDLE_IDENTIFIER) 11 | CFBundleInfoDictionaryVersion 12 | 6.0 13 | CFBundleName 14 | $(PRODUCT_NAME) 15 | CFBundlePackageType 16 | BNDL 17 | CFBundleShortVersionString 18 | 1.0 19 | CFBundleVersion 20 | 1 21 | 22 | 23 | -------------------------------------------------------------------------------- /Example/AppDelegate.swift: -------------------------------------------------------------------------------- 1 | // Copyright © 2017 Christian Tietze. All rights reserved. Distributed under the MIT License. 2 | 3 | import Cocoa 4 | import BetaExpiration 5 | 6 | @NSApplicationMain 7 | class AppDelegate: NSObject, NSApplicationDelegate { 8 | 9 | @IBOutlet weak var window: NSWindow! 10 | 11 | func applicationDidFinishLaunching(_ aNotification: Notification) { 12 | 13 | // BetaExpiration.guardExpiration() 14 | 15 | BetaExpiration.decorate( 16 | window: self.window, 17 | hostBundle: Bundle.main) 18 | } 19 | 20 | } 21 | 22 | -------------------------------------------------------------------------------- /Example/Assets.xcassets/AppIcon.appiconset/Contents.json: -------------------------------------------------------------------------------- 1 | { 2 | "images" : [ 3 | { 4 | "idiom" : "mac", 5 | "size" : "16x16", 6 | "scale" : "1x" 7 | }, 8 | { 9 | "idiom" : "mac", 10 | "size" : "16x16", 11 | "scale" : "2x" 12 | }, 13 | { 14 | "idiom" : "mac", 15 | "size" : "32x32", 16 | "scale" : "1x" 17 | }, 18 | { 19 | "idiom" : "mac", 20 | "size" : "32x32", 21 | "scale" : "2x" 22 | }, 23 | { 24 | "idiom" : "mac", 25 | "size" : "128x128", 26 | "scale" : "1x" 27 | }, 28 | { 29 | "idiom" : "mac", 30 | "size" : "128x128", 31 | "scale" : "2x" 32 | }, 33 | { 34 | "idiom" : "mac", 35 | "size" : "256x256", 36 | "scale" : "1x" 37 | }, 38 | { 39 | "idiom" : "mac", 40 | "size" : "256x256", 41 | "scale" : "2x" 42 | }, 43 | { 44 | "idiom" : "mac", 45 | "size" : "512x512", 46 | "scale" : "1x" 47 | }, 48 | { 49 | "idiom" : "mac", 50 | "size" : "512x512", 51 | "scale" : "2x" 52 | } 53 | ], 54 | "info" : { 55 | "version" : 1, 56 | "author" : "xcode" 57 | } 58 | } -------------------------------------------------------------------------------- /Example/Base.lproj/MainMenu.xib: -------------------------------------------------------------------------------- 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 | 96 | 97 | 98 | 99 | 100 | 101 | 102 | 103 | 104 | 105 | 106 | 107 | 108 | 109 | 110 | 111 | 112 | 113 | 114 | 115 | 116 | 117 | 118 | 119 | 120 | 121 | 122 | 123 | 124 | 125 | 126 | 127 | 128 | 129 | 130 | 131 | 132 | 133 | 134 | 135 | 136 | 137 | 138 | 139 | 140 | 141 | 142 | 143 | 144 | 145 | 146 | 147 | 148 | 149 | 150 | 151 | 152 | 153 | 154 | 155 | 156 | 157 | 158 | 159 | 160 | 161 | 162 | 163 | 164 | 165 | 166 | 167 | 168 | 169 | 170 | 171 | 172 | 173 | 174 | 175 | 176 | 177 | 178 | 179 | 180 | 181 | 182 | 183 | 184 | 185 | 186 | 187 | 188 | 189 | 190 | 191 | 192 | 193 | 194 | 195 | 196 | 197 | 198 | 199 | 200 | 201 | 202 | 203 | 204 | 205 | 206 | 207 | 208 | 209 | 210 | 211 | 212 | 213 | 214 | 215 | 216 | 217 | 218 | 219 | 220 | 221 | 222 | 223 | 224 | 225 | 226 | 227 | 228 | 229 | 230 | 231 | 232 | 233 | 234 | 235 | 236 | 237 | 238 | 239 | 240 | 241 | 242 | 243 | 244 | 245 | 246 | 247 | 248 | 249 | 250 | 251 | 252 | 253 | 254 | 255 | 256 | 257 | 258 | 259 | 260 | 261 | 262 | 263 | 264 | 265 | 266 | 267 | 268 | 269 | 270 | 271 | 272 | 273 | 274 | 275 | 276 | 277 | 278 | 279 | 280 | 281 | 282 | 283 | 284 | 285 | 286 | 287 | 288 | 289 | 290 | 291 | 292 | 293 | 294 | 295 | 296 | 297 | 298 | 299 | 300 | 301 | 302 | 303 | 304 | 305 | 306 | 307 | 308 | 309 | 310 | 311 | 312 | 313 | 314 | 315 | 316 | 317 | 318 | 319 | 320 | 321 | 322 | 323 | 324 | 325 | 326 | 327 | 328 | 329 | 330 | 331 | 332 | 333 | 334 | 335 | 336 | 337 | 338 | 339 | 340 | 341 | 342 | 343 | 344 | 345 | 346 | 347 | 348 | 349 | 350 | 351 | 352 | 353 | 354 | 355 | 356 | 357 | 358 | 359 | 360 | 361 | 362 | 363 | 364 | 365 | 366 | 367 | 368 | 369 | 370 | 371 | 372 | 373 | 374 | 375 | 376 | 377 | 378 | 379 | 380 | 381 | 382 | 383 | 384 | 385 | 386 | 387 | 388 | 389 | 390 | 391 | 392 | 393 | 394 | 395 | 396 | 397 | 398 | 399 | 400 | 401 | 402 | 403 | 404 | 405 | 406 | 407 | 408 | 409 | 410 | 411 | 412 | 413 | 414 | 415 | 416 | 417 | 418 | 419 | 420 | 421 | 422 | 423 | 424 | 425 | 426 | 427 | 428 | 429 | 430 | 431 | 432 | 433 | 434 | 435 | 436 | 437 | 438 | 439 | 440 | 441 | 442 | 443 | 444 | 445 | 446 | 447 | 448 | 449 | 450 | 451 | 452 | 453 | 454 | 455 | 456 | 457 | 458 | 459 | 460 | 461 | 462 | 463 | 464 | 465 | 466 | 467 | 468 | 469 | 470 | 471 | 472 | 473 | 474 | 475 | 476 | 477 | 478 | 479 | 480 | 481 | 482 | 483 | 484 | 485 | 486 | 487 | 488 | 489 | 490 | 491 | 492 | 493 | 494 | 495 | 496 | 497 | 498 | 499 | 500 | 501 | 502 | 503 | 504 | 505 | 506 | 507 | 508 | 509 | 510 | 511 | 512 | 513 | 514 | 515 | 516 | 517 | 518 | 519 | 520 | 521 | 522 | 523 | 524 | 525 | 526 | 527 | 528 | 529 | 530 | 531 | 532 | 533 | 534 | 535 | 536 | 537 | Default 538 | 539 | 540 | 541 | 542 | 543 | 544 | Left to Right 545 | 546 | 547 | 548 | 549 | 550 | 551 | Right to Left 552 | 553 | 554 | 555 | 556 | 557 | 558 | 559 | 560 | 561 | 562 | Default 563 | 564 | 565 | 566 | 567 | 568 | 569 | Left to Right 570 | 571 | 572 | 573 | 574 | 575 | 576 | Right to Left 577 | 578 | 579 | 580 | 581 | 582 | 583 | 584 | 585 | 586 | 587 | 588 | 589 | 590 | 591 | 592 | 593 | 594 | 595 | 596 | 597 | 598 | 599 | 600 | 601 | 602 | 603 | 604 | 605 | 606 | 607 | 608 | 609 | 610 | 611 | 612 | 613 | 614 | 615 | 616 | 617 | 618 | 619 | 620 | 621 | 622 | 623 | 624 | 625 | 626 | 627 | 628 | 629 | 630 | 631 | 632 | 633 | 634 | 635 | 636 | 637 | 638 | 639 | 640 | 641 | 642 | 643 | 644 | 645 | 646 | 647 | 648 | 649 | 650 | 651 | 652 | 653 | 654 | 655 | 656 | 657 | 658 | 659 | 660 | 661 | 662 | 663 | 664 | 665 | 666 | 667 | 668 | 669 | 670 | 671 | 672 | 673 | 674 | 675 | 676 | 677 | 678 | 679 | 680 | 681 | 682 | 683 | 684 | 685 | 686 | 687 | 688 | 689 | 690 | 691 | 692 | 693 | -------------------------------------------------------------------------------- /Example/Info.plist: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | CFBundleDevelopmentRegion 6 | en 7 | CFBundleExecutable 8 | $(EXECUTABLE_NAME) 9 | CFBundleIconFile 10 | 11 | CFBundleIdentifier 12 | $(PRODUCT_BUNDLE_IDENTIFIER) 13 | CFBundleInfoDictionaryVersion 14 | 6.0 15 | CFBundleName 16 | $(PRODUCT_NAME) 17 | CFBundlePackageType 18 | APPL 19 | CFBundleShortVersionString 20 | 2.4 21 | CFBundleVersion 22 | 3012 23 | LSMinimumSystemVersion 24 | $(MACOSX_DEPLOYMENT_TARGET) 25 | NSHumanReadableCopyright 26 | Copyright © 2017 Christian Tietze. All rights reserved. 27 | NSMainNibFile 28 | MainMenu 29 | NSPrincipalClass 30 | NSApplication 31 | 32 | 33 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2017 Christian Tietze 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # BetaExpiration 2 | 3 | ![Swift 5.0](https://img.shields.io/badge/Swift-5.0-blue.svg?style=flat) 4 | ![Version](https://img.shields.io/github/tag/CleanCocoa/BetaExpiration.svg?style=flat) 5 | ![License](https://img.shields.io/github/license/CleanCocoa/BetaExpiration.svg?style=flat) 6 | ![Platform](https://img.shields.io/badge/platform-macOS-lightgrey.svg?style=flat) 7 | 8 | Example showing how to implement a usable expiring beta to macOS apps. 9 | 10 | ## Setup 11 | 12 | ⚠️ **It doesn't make sense to put the example setup directly into your project.** 13 | 14 | That's because you frequently will want to modify `BetaExpiration.swift`. The library code has a hard-coded expiration date. You will want to change this with ongoing releases. To make this a Carthage or CocoaPods library would be useless. 15 | 16 | There are two variants that'll work: 17 | 18 | 1. You can copy the files from the library over to your project. 19 | 2. You can check the whole project out (as a git submodule, for example) and include it as a sub-project in your app. 20 | 21 | Whatever you do, it's important that you can easily change the expiration date in `BetaExpiration.swift`. 22 | 23 | ### Integration 24 | 25 | 1. Hard code a new expiration date in `BetaExpiration.expiresAt`. You can compute the date by running the unit tests and looking at the console output. 26 | 2. Change the alert in `BetaExpiration.guardExpiration()` to show your e-mail address. 27 | 3. Call `BetaExpiration.guardExpiration()` from your `AppDelegate`. This will display an alert and quit the app if the beta period is over. 28 | 4. _Optional:_ Decorate your app window with useful information for your beta testers, see below. 29 | 30 | ### Window decoration 31 | 32 |
33 | 34 |
35 | 36 | The decoration is achieved by adding a `NSTitlebarAccessoryViewController` to the window. The format is as follows: 37 | 38 | β v1.2.0 (107) - 23 days left 39 | ^ ^ ^ 40 | | | |__ = (today - expiration date) 41 | | | 42 | | |__ build number 43 | | 44 | |__ version 45 | 46 | You get all of this by calling `BetaExpiration.decorate(window:hostBundle:)`. The `hostBundle` should be your app's main `Bundle` because that'll include version and build number information. 47 | 48 | 49 | ## Code License 50 | 51 | Copyright (c) 2017-2019 Christian Tietze. Distributed under the MIT License. 52 | -------------------------------------------------------------------------------- /assets/example.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/CleanCocoa/BetaExpiration/19c9a289f0506081220d460d2a8b73109b774720/assets/example.png --------------------------------------------------------------------------------