├── CHANGELOG.md ├── Example ├── MissionControlDemo.xcodeproj │ ├── project.pbxproj │ ├── project.xcworkspace │ │ └── contents.xcworkspacedata │ └── xcshareddata │ │ └── xcschemes │ │ └── MissionControlDemo.xcscheme └── MissionControlDemo │ ├── AppDelegate.swift │ ├── Assets.xcassets │ ├── AppIcon.appiconset │ │ ├── Contents.json │ │ ├── Icon-29.png │ │ ├── Icon-29@2x-1.png │ │ ├── Icon-29@2x.png │ │ ├── Icon-29@3x.png │ │ ├── Icon-40.png │ │ ├── Icon-40@2x-1.png │ │ ├── Icon-40@2x.png │ │ ├── Icon-40@3x.png │ │ ├── Icon-60@2x.png │ │ ├── Icon-60@3x.png │ │ ├── Icon-76.png │ │ ├── Icon-76@2x.png │ │ └── Icon-83.5@2x.png │ ├── Contents.json │ ├── Launch Image.imageset │ │ ├── Contents.json │ │ └── Launch Image.pdf │ └── appculture.imageset │ │ ├── Contents.json │ │ └── appculture.pdf │ ├── Base.lproj │ ├── Launch.storyboard │ └── LaunchScreen.storyboard │ ├── BaseLaunchView.swift │ ├── Info.plist │ ├── LaunchBrain.swift │ ├── LaunchView.swift │ ├── LaunchViewController.swift │ └── NAS966.TTF ├── Images ├── MissionControl-01-Offline.png ├── MissionControl-02-Ready.png └── MissionControl-03-Countdown.png ├── LICENSE ├── MissionControl.podspec ├── MissionControl.xcodeproj ├── project.pbxproj ├── project.xcworkspace │ └── contents.xcworkspacedata └── xcshareddata │ └── xcschemes │ ├── MissionControl OSX.xcscheme │ ├── MissionControl iOS.xcscheme │ ├── MissionControl tvOS.xcscheme │ └── MissionControl watchOS.xcscheme ├── Package.swift ├── README.md ├── Sources ├── Info.plist ├── MissionControl.h └── MissionControl.swift └── Tests ├── Info.plist └── MissionControlTests.swift /CHANGELOG.md: -------------------------------------------------------------------------------- 1 | # Changelog 2 | 3 | ## Version 1.0.0 4 | 5 | - Initial version -------------------------------------------------------------------------------- /Example/MissionControlDemo.xcodeproj/project.pbxproj: -------------------------------------------------------------------------------- 1 | // !$*UTF8*$! 2 | { 3 | archiveVersion = 1; 4 | classes = { 5 | }; 6 | objectVersion = 46; 7 | objects = { 8 | 9 | /* Begin PBXBuildFile section */ 10 | 8B2A29D01CEA27FD00FAE67F /* MissionControl.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 8B2A29CC1CEA27DF00FAE67F /* MissionControl.framework */; }; 11 | 8B2A29D11CEA27FD00FAE67F /* MissionControl.framework in Embed Frameworks */ = {isa = PBXBuildFile; fileRef = 8B2A29CC1CEA27DF00FAE67F /* MissionControl.framework */; settings = {ATTRIBUTES = (CodeSignOnCopy, RemoveHeadersOnCopy, ); }; }; 12 | 8B464DD01CE3742F00BAE834 /* AppDelegate.swift in Sources */ = {isa = PBXBuildFile; fileRef = 8B464DCF1CE3742F00BAE834 /* AppDelegate.swift */; }; 13 | 8B464DD51CE3742F00BAE834 /* Launch.storyboard in Resources */ = {isa = PBXBuildFile; fileRef = 8B464DD31CE3742F00BAE834 /* Launch.storyboard */; }; 14 | 8B464DD71CE3742F00BAE834 /* Assets.xcassets in Resources */ = {isa = PBXBuildFile; fileRef = 8B464DD61CE3742F00BAE834 /* Assets.xcassets */; }; 15 | 8B464DDA1CE3742F00BAE834 /* LaunchScreen.storyboard in Resources */ = {isa = PBXBuildFile; fileRef = 8B464DD81CE3742F00BAE834 /* LaunchScreen.storyboard */; }; 16 | 8B9B81D91CEDB04700E78198 /* BaseLaunchView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 8B9B81D81CEDB04700E78198 /* BaseLaunchView.swift */; }; 17 | 8B9B81DD1CEDB4B800E78198 /* LaunchViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 8B9B81DC1CEDB4B800E78198 /* LaunchViewController.swift */; }; 18 | 8B9B81DF1CEDC58200E78198 /* NAS966.TTF in Resources */ = {isa = PBXBuildFile; fileRef = 8B9B81DE1CEDC58200E78198 /* NAS966.TTF */; }; 19 | 8B9B81E31CEDEB7600E78198 /* LaunchView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 8B9B81E21CEDEB7600E78198 /* LaunchView.swift */; }; 20 | 8B9B81EA1CEF3C1300E78198 /* LaunchBrain.swift in Sources */ = {isa = PBXBuildFile; fileRef = 8B9B81E91CEF3C1300E78198 /* LaunchBrain.swift */; }; 21 | /* End PBXBuildFile section */ 22 | 23 | /* Begin PBXContainerItemProxy section */ 24 | 8B2A29CB1CEA27DF00FAE67F /* PBXContainerItemProxy */ = { 25 | isa = PBXContainerItemProxy; 26 | containerPortal = 8B2A29C61CEA27DF00FAE67F /* MissionControl.xcodeproj */; 27 | proxyType = 2; 28 | remoteGlobalIDString = 8B63137B1CE5F9A10029DC98; 29 | remoteInfo = "MissionControl iOS"; 30 | }; 31 | 8B2A29CD1CEA27DF00FAE67F /* PBXContainerItemProxy */ = { 32 | isa = PBXContainerItemProxy; 33 | containerPortal = 8B2A29C61CEA27DF00FAE67F /* MissionControl.xcodeproj */; 34 | proxyType = 2; 35 | remoteGlobalIDString = 8B6313851CE5F9A10029DC98; 36 | remoteInfo = "MissionControl iOS Tests"; 37 | }; 38 | 8B2A29D21CEA27FD00FAE67F /* PBXContainerItemProxy */ = { 39 | isa = PBXContainerItemProxy; 40 | containerPortal = 8B2A29C61CEA27DF00FAE67F /* MissionControl.xcodeproj */; 41 | proxyType = 1; 42 | remoteGlobalIDString = 8B63137A1CE5F9A10029DC98; 43 | remoteInfo = "MissionControl iOS"; 44 | }; 45 | /* End PBXContainerItemProxy section */ 46 | 47 | /* Begin PBXCopyFilesBuildPhase section */ 48 | 8B6313A91CE5FC2D0029DC98 /* Embed Frameworks */ = { 49 | isa = PBXCopyFilesBuildPhase; 50 | buildActionMask = 2147483647; 51 | dstPath = ""; 52 | dstSubfolderSpec = 10; 53 | files = ( 54 | 8B2A29D11CEA27FD00FAE67F /* MissionControl.framework in Embed Frameworks */, 55 | ); 56 | name = "Embed Frameworks"; 57 | runOnlyForDeploymentPostprocessing = 0; 58 | }; 59 | /* End PBXCopyFilesBuildPhase section */ 60 | 61 | /* Begin PBXFileReference section */ 62 | 8B2A29C61CEA27DF00FAE67F /* MissionControl.xcodeproj */ = {isa = PBXFileReference; lastKnownFileType = "wrapper.pb-project"; name = MissionControl.xcodeproj; path = ../MissionControl.xcodeproj; sourceTree = ""; }; 63 | 8B464DCC1CE3742F00BAE834 /* MissionControlDemo.app */ = {isa = PBXFileReference; explicitFileType = wrapper.application; includeInIndex = 0; path = MissionControlDemo.app; sourceTree = BUILT_PRODUCTS_DIR; }; 64 | 8B464DCF1CE3742F00BAE834 /* AppDelegate.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AppDelegate.swift; sourceTree = ""; }; 65 | 8B464DD41CE3742F00BAE834 /* Base */ = {isa = PBXFileReference; lastKnownFileType = file.storyboard; name = Base; path = Base.lproj/Launch.storyboard; sourceTree = ""; }; 66 | 8B464DD61CE3742F00BAE834 /* Assets.xcassets */ = {isa = PBXFileReference; lastKnownFileType = folder.assetcatalog; path = Assets.xcassets; sourceTree = ""; }; 67 | 8B464DD91CE3742F00BAE834 /* Base */ = {isa = PBXFileReference; lastKnownFileType = file.storyboard; name = Base; path = Base.lproj/LaunchScreen.storyboard; sourceTree = ""; }; 68 | 8B464DDB1CE3742F00BAE834 /* Info.plist */ = {isa = PBXFileReference; lastKnownFileType = text.plist.xml; path = Info.plist; sourceTree = ""; }; 69 | 8B9B81D81CEDB04700E78198 /* BaseLaunchView.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = BaseLaunchView.swift; sourceTree = ""; }; 70 | 8B9B81DC1CEDB4B800E78198 /* LaunchViewController.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = LaunchViewController.swift; sourceTree = ""; }; 71 | 8B9B81DE1CEDC58200E78198 /* NAS966.TTF */ = {isa = PBXFileReference; lastKnownFileType = file; path = NAS966.TTF; sourceTree = ""; }; 72 | 8B9B81E21CEDEB7600E78198 /* LaunchView.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = LaunchView.swift; sourceTree = ""; }; 73 | 8B9B81E91CEF3C1300E78198 /* LaunchBrain.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = LaunchBrain.swift; sourceTree = ""; }; 74 | /* End PBXFileReference section */ 75 | 76 | /* Begin PBXFrameworksBuildPhase section */ 77 | 8B464DC91CE3742F00BAE834 /* Frameworks */ = { 78 | isa = PBXFrameworksBuildPhase; 79 | buildActionMask = 2147483647; 80 | files = ( 81 | 8B2A29D01CEA27FD00FAE67F /* MissionControl.framework in Frameworks */, 82 | ); 83 | runOnlyForDeploymentPostprocessing = 0; 84 | }; 85 | /* End PBXFrameworksBuildPhase section */ 86 | 87 | /* Begin PBXGroup section */ 88 | 8B2A29C71CEA27DF00FAE67F /* Products */ = { 89 | isa = PBXGroup; 90 | children = ( 91 | 8B2A29CC1CEA27DF00FAE67F /* MissionControl.framework */, 92 | 8B2A29CE1CEA27DF00FAE67F /* MissionControlTests.xctest */, 93 | ); 94 | name = Products; 95 | sourceTree = ""; 96 | }; 97 | 8B464DC31CE3742F00BAE834 = { 98 | isa = PBXGroup; 99 | children = ( 100 | 8B464DCE1CE3742F00BAE834 /* MissionControlDemo */, 101 | 8B464DCD1CE3742F00BAE834 /* Products */, 102 | 8B2A29C61CEA27DF00FAE67F /* MissionControl.xcodeproj */, 103 | ); 104 | sourceTree = ""; 105 | }; 106 | 8B464DCD1CE3742F00BAE834 /* Products */ = { 107 | isa = PBXGroup; 108 | children = ( 109 | 8B464DCC1CE3742F00BAE834 /* MissionControlDemo.app */, 110 | ); 111 | name = Products; 112 | sourceTree = ""; 113 | }; 114 | 8B464DCE1CE3742F00BAE834 /* MissionControlDemo */ = { 115 | isa = PBXGroup; 116 | children = ( 117 | 8B464DCF1CE3742F00BAE834 /* AppDelegate.swift */, 118 | 8B464DD81CE3742F00BAE834 /* LaunchScreen.storyboard */, 119 | 8B9B81EB1CEF3E1200E78198 /* Launch */, 120 | 8B63139A1CE5FB290029DC98 /* Resources */, 121 | ); 122 | path = MissionControlDemo; 123 | sourceTree = ""; 124 | }; 125 | 8B63139A1CE5FB290029DC98 /* Resources */ = { 126 | isa = PBXGroup; 127 | children = ( 128 | 8B464DD61CE3742F00BAE834 /* Assets.xcassets */, 129 | 8B9B81DE1CEDC58200E78198 /* NAS966.TTF */, 130 | 8B9B81DB1CEDB49B00E78198 /* Settings */, 131 | ); 132 | name = Resources; 133 | sourceTree = ""; 134 | }; 135 | 8B9B81DB1CEDB49B00E78198 /* Settings */ = { 136 | isa = PBXGroup; 137 | children = ( 138 | 8B464DDB1CE3742F00BAE834 /* Info.plist */, 139 | ); 140 | name = Settings; 141 | sourceTree = ""; 142 | }; 143 | 8B9B81EB1CEF3E1200E78198 /* Launch */ = { 144 | isa = PBXGroup; 145 | children = ( 146 | 8B464DD31CE3742F00BAE834 /* Launch.storyboard */, 147 | 8B9B81E91CEF3C1300E78198 /* LaunchBrain.swift */, 148 | 8B9B81EC1CEF3E5800E78198 /* Launch View */, 149 | 8B9B81DC1CEDB4B800E78198 /* LaunchViewController.swift */, 150 | ); 151 | name = Launch; 152 | sourceTree = ""; 153 | }; 154 | 8B9B81EC1CEF3E5800E78198 /* Launch View */ = { 155 | isa = PBXGroup; 156 | children = ( 157 | 8B9B81D81CEDB04700E78198 /* BaseLaunchView.swift */, 158 | 8B9B81E21CEDEB7600E78198 /* LaunchView.swift */, 159 | ); 160 | name = "Launch View"; 161 | sourceTree = ""; 162 | }; 163 | /* End PBXGroup section */ 164 | 165 | /* Begin PBXNativeTarget section */ 166 | 8B464DCB1CE3742F00BAE834 /* MissionControlDemo */ = { 167 | isa = PBXNativeTarget; 168 | buildConfigurationList = 8B464DDE1CE3742F00BAE834 /* Build configuration list for PBXNativeTarget "MissionControlDemo" */; 169 | buildPhases = ( 170 | 8B464DC81CE3742F00BAE834 /* Sources */, 171 | 8B464DC91CE3742F00BAE834 /* Frameworks */, 172 | 8B464DCA1CE3742F00BAE834 /* Resources */, 173 | 8B6313A91CE5FC2D0029DC98 /* Embed Frameworks */, 174 | ); 175 | buildRules = ( 176 | ); 177 | dependencies = ( 178 | 8B2A29D31CEA27FD00FAE67F /* PBXTargetDependency */, 179 | ); 180 | name = MissionControlDemo; 181 | productName = ACCloudConfig; 182 | productReference = 8B464DCC1CE3742F00BAE834 /* MissionControlDemo.app */; 183 | productType = "com.apple.product-type.application"; 184 | }; 185 | /* End PBXNativeTarget section */ 186 | 187 | /* Begin PBXProject section */ 188 | 8B464DC41CE3742F00BAE834 /* Project object */ = { 189 | isa = PBXProject; 190 | attributes = { 191 | LastSwiftUpdateCheck = 0730; 192 | LastUpgradeCheck = 0730; 193 | ORGANIZATIONNAME = appculture; 194 | TargetAttributes = { 195 | 8B464DCB1CE3742F00BAE834 = { 196 | CreatedOnToolsVersion = 7.3.1; 197 | }; 198 | }; 199 | }; 200 | buildConfigurationList = 8B464DC71CE3742F00BAE834 /* Build configuration list for PBXProject "MissionControlDemo" */; 201 | compatibilityVersion = "Xcode 3.2"; 202 | developmentRegion = English; 203 | hasScannedForEncodings = 0; 204 | knownRegions = ( 205 | en, 206 | Base, 207 | ); 208 | mainGroup = 8B464DC31CE3742F00BAE834; 209 | productRefGroup = 8B464DCD1CE3742F00BAE834 /* Products */; 210 | projectDirPath = ""; 211 | projectReferences = ( 212 | { 213 | ProductGroup = 8B2A29C71CEA27DF00FAE67F /* Products */; 214 | ProjectRef = 8B2A29C61CEA27DF00FAE67F /* MissionControl.xcodeproj */; 215 | }, 216 | ); 217 | projectRoot = ""; 218 | targets = ( 219 | 8B464DCB1CE3742F00BAE834 /* MissionControlDemo */, 220 | ); 221 | }; 222 | /* End PBXProject section */ 223 | 224 | /* Begin PBXReferenceProxy section */ 225 | 8B2A29CC1CEA27DF00FAE67F /* MissionControl.framework */ = { 226 | isa = PBXReferenceProxy; 227 | fileType = wrapper.framework; 228 | path = MissionControl.framework; 229 | remoteRef = 8B2A29CB1CEA27DF00FAE67F /* PBXContainerItemProxy */; 230 | sourceTree = BUILT_PRODUCTS_DIR; 231 | }; 232 | 8B2A29CE1CEA27DF00FAE67F /* MissionControlTests.xctest */ = { 233 | isa = PBXReferenceProxy; 234 | fileType = wrapper.cfbundle; 235 | path = MissionControlTests.xctest; 236 | remoteRef = 8B2A29CD1CEA27DF00FAE67F /* PBXContainerItemProxy */; 237 | sourceTree = BUILT_PRODUCTS_DIR; 238 | }; 239 | /* End PBXReferenceProxy section */ 240 | 241 | /* Begin PBXResourcesBuildPhase section */ 242 | 8B464DCA1CE3742F00BAE834 /* Resources */ = { 243 | isa = PBXResourcesBuildPhase; 244 | buildActionMask = 2147483647; 245 | files = ( 246 | 8B464DDA1CE3742F00BAE834 /* LaunchScreen.storyboard in Resources */, 247 | 8B9B81DF1CEDC58200E78198 /* NAS966.TTF in Resources */, 248 | 8B464DD71CE3742F00BAE834 /* Assets.xcassets in Resources */, 249 | 8B464DD51CE3742F00BAE834 /* Launch.storyboard in Resources */, 250 | ); 251 | runOnlyForDeploymentPostprocessing = 0; 252 | }; 253 | /* End PBXResourcesBuildPhase section */ 254 | 255 | /* Begin PBXSourcesBuildPhase section */ 256 | 8B464DC81CE3742F00BAE834 /* Sources */ = { 257 | isa = PBXSourcesBuildPhase; 258 | buildActionMask = 2147483647; 259 | files = ( 260 | 8B9B81D91CEDB04700E78198 /* BaseLaunchView.swift in Sources */, 261 | 8B464DD01CE3742F00BAE834 /* AppDelegate.swift in Sources */, 262 | 8B9B81DD1CEDB4B800E78198 /* LaunchViewController.swift in Sources */, 263 | 8B9B81EA1CEF3C1300E78198 /* LaunchBrain.swift in Sources */, 264 | 8B9B81E31CEDEB7600E78198 /* LaunchView.swift in Sources */, 265 | ); 266 | runOnlyForDeploymentPostprocessing = 0; 267 | }; 268 | /* End PBXSourcesBuildPhase section */ 269 | 270 | /* Begin PBXTargetDependency section */ 271 | 8B2A29D31CEA27FD00FAE67F /* PBXTargetDependency */ = { 272 | isa = PBXTargetDependency; 273 | name = "MissionControl iOS"; 274 | targetProxy = 8B2A29D21CEA27FD00FAE67F /* PBXContainerItemProxy */; 275 | }; 276 | /* End PBXTargetDependency section */ 277 | 278 | /* Begin PBXVariantGroup section */ 279 | 8B464DD31CE3742F00BAE834 /* Launch.storyboard */ = { 280 | isa = PBXVariantGroup; 281 | children = ( 282 | 8B464DD41CE3742F00BAE834 /* Base */, 283 | ); 284 | name = Launch.storyboard; 285 | sourceTree = ""; 286 | }; 287 | 8B464DD81CE3742F00BAE834 /* LaunchScreen.storyboard */ = { 288 | isa = PBXVariantGroup; 289 | children = ( 290 | 8B464DD91CE3742F00BAE834 /* Base */, 291 | ); 292 | name = LaunchScreen.storyboard; 293 | sourceTree = ""; 294 | }; 295 | /* End PBXVariantGroup section */ 296 | 297 | /* Begin XCBuildConfiguration section */ 298 | 8B464DDC1CE3742F00BAE834 /* Debug */ = { 299 | isa = XCBuildConfiguration; 300 | buildSettings = { 301 | ALWAYS_SEARCH_USER_PATHS = NO; 302 | CLANG_ANALYZER_NONNULL = YES; 303 | CLANG_CXX_LANGUAGE_STANDARD = "gnu++0x"; 304 | CLANG_CXX_LIBRARY = "libc++"; 305 | CLANG_ENABLE_MODULES = YES; 306 | CLANG_ENABLE_OBJC_ARC = YES; 307 | CLANG_WARN_BOOL_CONVERSION = YES; 308 | CLANG_WARN_CONSTANT_CONVERSION = YES; 309 | CLANG_WARN_DIRECT_OBJC_ISA_USAGE = YES_ERROR; 310 | CLANG_WARN_EMPTY_BODY = YES; 311 | CLANG_WARN_ENUM_CONVERSION = YES; 312 | CLANG_WARN_INT_CONVERSION = YES; 313 | CLANG_WARN_OBJC_ROOT_CLASS = YES_ERROR; 314 | CLANG_WARN_UNREACHABLE_CODE = YES; 315 | CLANG_WARN__DUPLICATE_METHOD_MATCH = YES; 316 | "CODE_SIGN_IDENTITY[sdk=iphoneos*]" = "iPhone Developer"; 317 | COPY_PHASE_STRIP = NO; 318 | DEBUG_INFORMATION_FORMAT = dwarf; 319 | ENABLE_STRICT_OBJC_MSGSEND = YES; 320 | ENABLE_TESTABILITY = YES; 321 | GCC_C_LANGUAGE_STANDARD = gnu99; 322 | GCC_DYNAMIC_NO_PIC = NO; 323 | GCC_NO_COMMON_BLOCKS = YES; 324 | GCC_OPTIMIZATION_LEVEL = 0; 325 | GCC_PREPROCESSOR_DEFINITIONS = ( 326 | "DEBUG=1", 327 | "$(inherited)", 328 | ); 329 | GCC_WARN_64_TO_32_BIT_CONVERSION = YES; 330 | GCC_WARN_ABOUT_RETURN_TYPE = YES_ERROR; 331 | GCC_WARN_UNDECLARED_SELECTOR = YES; 332 | GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE; 333 | GCC_WARN_UNUSED_FUNCTION = YES; 334 | GCC_WARN_UNUSED_VARIABLE = YES; 335 | IPHONEOS_DEPLOYMENT_TARGET = 9.0; 336 | MTL_ENABLE_DEBUG_INFO = YES; 337 | ONLY_ACTIVE_ARCH = YES; 338 | SDKROOT = iphoneos; 339 | SWIFT_OPTIMIZATION_LEVEL = "-Onone"; 340 | TARGETED_DEVICE_FAMILY = "1,2"; 341 | }; 342 | name = Debug; 343 | }; 344 | 8B464DDD1CE3742F00BAE834 /* Release */ = { 345 | isa = XCBuildConfiguration; 346 | buildSettings = { 347 | ALWAYS_SEARCH_USER_PATHS = NO; 348 | CLANG_ANALYZER_NONNULL = YES; 349 | CLANG_CXX_LANGUAGE_STANDARD = "gnu++0x"; 350 | CLANG_CXX_LIBRARY = "libc++"; 351 | CLANG_ENABLE_MODULES = YES; 352 | CLANG_ENABLE_OBJC_ARC = YES; 353 | CLANG_WARN_BOOL_CONVERSION = YES; 354 | CLANG_WARN_CONSTANT_CONVERSION = YES; 355 | CLANG_WARN_DIRECT_OBJC_ISA_USAGE = YES_ERROR; 356 | CLANG_WARN_EMPTY_BODY = YES; 357 | CLANG_WARN_ENUM_CONVERSION = YES; 358 | CLANG_WARN_INT_CONVERSION = YES; 359 | CLANG_WARN_OBJC_ROOT_CLASS = YES_ERROR; 360 | CLANG_WARN_UNREACHABLE_CODE = YES; 361 | CLANG_WARN__DUPLICATE_METHOD_MATCH = YES; 362 | "CODE_SIGN_IDENTITY[sdk=iphoneos*]" = "iPhone Developer"; 363 | COPY_PHASE_STRIP = NO; 364 | DEBUG_INFORMATION_FORMAT = "dwarf-with-dsym"; 365 | ENABLE_NS_ASSERTIONS = NO; 366 | ENABLE_STRICT_OBJC_MSGSEND = YES; 367 | GCC_C_LANGUAGE_STANDARD = gnu99; 368 | GCC_NO_COMMON_BLOCKS = YES; 369 | GCC_WARN_64_TO_32_BIT_CONVERSION = YES; 370 | GCC_WARN_ABOUT_RETURN_TYPE = YES_ERROR; 371 | GCC_WARN_UNDECLARED_SELECTOR = YES; 372 | GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE; 373 | GCC_WARN_UNUSED_FUNCTION = YES; 374 | GCC_WARN_UNUSED_VARIABLE = YES; 375 | IPHONEOS_DEPLOYMENT_TARGET = 9.0; 376 | MTL_ENABLE_DEBUG_INFO = NO; 377 | SDKROOT = iphoneos; 378 | TARGETED_DEVICE_FAMILY = "1,2"; 379 | VALIDATE_PRODUCT = YES; 380 | }; 381 | name = Release; 382 | }; 383 | 8B464DDF1CE3742F00BAE834 /* Debug */ = { 384 | isa = XCBuildConfiguration; 385 | buildSettings = { 386 | ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon; 387 | EMBEDDED_CONTENT_CONTAINS_SWIFT = YES; 388 | INFOPLIST_FILE = MissionControlDemo/Info.plist; 389 | LD_RUNPATH_SEARCH_PATHS = "$(inherited) @executable_path/Frameworks"; 390 | PRODUCT_BUNDLE_IDENTIFIER = com.appculture.MissionControlDemo; 391 | PRODUCT_NAME = MissionControlDemo; 392 | }; 393 | name = Debug; 394 | }; 395 | 8B464DE01CE3742F00BAE834 /* Release */ = { 396 | isa = XCBuildConfiguration; 397 | buildSettings = { 398 | ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon; 399 | EMBEDDED_CONTENT_CONTAINS_SWIFT = YES; 400 | INFOPLIST_FILE = MissionControlDemo/Info.plist; 401 | LD_RUNPATH_SEARCH_PATHS = "$(inherited) @executable_path/Frameworks"; 402 | PRODUCT_BUNDLE_IDENTIFIER = com.appculture.MissionControlDemo; 403 | PRODUCT_NAME = MissionControlDemo; 404 | }; 405 | name = Release; 406 | }; 407 | /* End XCBuildConfiguration section */ 408 | 409 | /* Begin XCConfigurationList section */ 410 | 8B464DC71CE3742F00BAE834 /* Build configuration list for PBXProject "MissionControlDemo" */ = { 411 | isa = XCConfigurationList; 412 | buildConfigurations = ( 413 | 8B464DDC1CE3742F00BAE834 /* Debug */, 414 | 8B464DDD1CE3742F00BAE834 /* Release */, 415 | ); 416 | defaultConfigurationIsVisible = 0; 417 | defaultConfigurationName = Release; 418 | }; 419 | 8B464DDE1CE3742F00BAE834 /* Build configuration list for PBXNativeTarget "MissionControlDemo" */ = { 420 | isa = XCConfigurationList; 421 | buildConfigurations = ( 422 | 8B464DDF1CE3742F00BAE834 /* Debug */, 423 | 8B464DE01CE3742F00BAE834 /* Release */, 424 | ); 425 | defaultConfigurationIsVisible = 0; 426 | defaultConfigurationName = Release; 427 | }; 428 | /* End XCConfigurationList section */ 429 | }; 430 | rootObject = 8B464DC41CE3742F00BAE834 /* Project object */; 431 | } 432 | -------------------------------------------------------------------------------- /Example/MissionControlDemo.xcodeproj/project.xcworkspace/contents.xcworkspacedata: -------------------------------------------------------------------------------- 1 | 2 | 4 | 6 | 7 | 8 | -------------------------------------------------------------------------------- /Example/MissionControlDemo.xcodeproj/xcshareddata/xcschemes/MissionControlDemo.xcscheme: -------------------------------------------------------------------------------- 1 | 2 | 5 | 8 | 9 | 15 | 21 | 22 | 23 | 24 | 25 | 30 | 31 | 32 | 33 | 39 | 40 | 41 | 42 | 43 | 44 | 54 | 56 | 62 | 63 | 64 | 65 | 66 | 67 | 73 | 75 | 81 | 82 | 83 | 84 | 86 | 87 | 90 | 91 | 92 | -------------------------------------------------------------------------------- /Example/MissionControlDemo/AppDelegate.swift: -------------------------------------------------------------------------------- 1 | // 2 | // AppDelegate.swift 3 | // MissionControlDemo 4 | // 5 | // Copyright (c) 2016 appculture http://appculture.com 6 | // 7 | // Permission is hereby granted, free of charge, to any person obtaining a copy 8 | // of this software and associated documentation files (the "Software"), to deal 9 | // in the Software without restriction, including without limitation the rights 10 | // to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 11 | // copies of the Software, and to permit persons to whom the Software is 12 | // furnished to do so, subject to the following conditions: 13 | // 14 | // The above copyright notice and this permission notice shall be included in all 15 | // copies or substantial portions of the Software. 16 | // 17 | // THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 18 | // IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 19 | // FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 20 | // AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 21 | // LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 22 | // OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 23 | // SOFTWARE. 24 | // 25 | 26 | import UIKit 27 | import MissionControl 28 | 29 | @UIApplicationMain 30 | class AppDelegate: UIResponder, UIApplicationDelegate { 31 | 32 | var window: UIWindow? 33 | 34 | func application(application: UIApplication, didFinishLaunchingWithOptions launchOptions: [NSObject: AnyObject]?) -> Bool { 35 | 36 | let url = NSURL(string: "http://private-83024-missioncontrol5.apiary-mock.com/mission-control/launch-config")! 37 | MissionControl.launch(remoteConfigURL: url) 38 | 39 | return true 40 | } 41 | 42 | func applicationWillEnterForeground(application: UIApplication) { 43 | MissionControl.refresh() 44 | } 45 | 46 | func applicationDidBecomeActive(application: UIApplication) { 47 | MissionControl.refresh() 48 | } 49 | 50 | } 51 | -------------------------------------------------------------------------------- /Example/MissionControlDemo/Assets.xcassets/AppIcon.appiconset/Contents.json: -------------------------------------------------------------------------------- 1 | { 2 | "images" : [ 3 | { 4 | "size" : "29x29", 5 | "idiom" : "iphone", 6 | "filename" : "Icon-29@2x.png", 7 | "scale" : "2x" 8 | }, 9 | { 10 | "size" : "29x29", 11 | "idiom" : "iphone", 12 | "filename" : "Icon-29@3x.png", 13 | "scale" : "3x" 14 | }, 15 | { 16 | "size" : "40x40", 17 | "idiom" : "iphone", 18 | "filename" : "Icon-40@2x.png", 19 | "scale" : "2x" 20 | }, 21 | { 22 | "size" : "40x40", 23 | "idiom" : "iphone", 24 | "filename" : "Icon-40@3x.png", 25 | "scale" : "3x" 26 | }, 27 | { 28 | "size" : "60x60", 29 | "idiom" : "iphone", 30 | "filename" : "Icon-60@2x.png", 31 | "scale" : "2x" 32 | }, 33 | { 34 | "size" : "60x60", 35 | "idiom" : "iphone", 36 | "filename" : "Icon-60@3x.png", 37 | "scale" : "3x" 38 | }, 39 | { 40 | "size" : "29x29", 41 | "idiom" : "ipad", 42 | "filename" : "Icon-29.png", 43 | "scale" : "1x" 44 | }, 45 | { 46 | "size" : "29x29", 47 | "idiom" : "ipad", 48 | "filename" : "Icon-29@2x-1.png", 49 | "scale" : "2x" 50 | }, 51 | { 52 | "size" : "40x40", 53 | "idiom" : "ipad", 54 | "filename" : "Icon-40.png", 55 | "scale" : "1x" 56 | }, 57 | { 58 | "size" : "40x40", 59 | "idiom" : "ipad", 60 | "filename" : "Icon-40@2x-1.png", 61 | "scale" : "2x" 62 | }, 63 | { 64 | "size" : "76x76", 65 | "idiom" : "ipad", 66 | "filename" : "Icon-76.png", 67 | "scale" : "1x" 68 | }, 69 | { 70 | "size" : "76x76", 71 | "idiom" : "ipad", 72 | "filename" : "Icon-76@2x.png", 73 | "scale" : "2x" 74 | }, 75 | { 76 | "size" : "83.5x83.5", 77 | "idiom" : "ipad", 78 | "filename" : "Icon-83.5@2x.png", 79 | "scale" : "2x" 80 | } 81 | ], 82 | "info" : { 83 | "version" : 1, 84 | "author" : "xcode" 85 | } 86 | } -------------------------------------------------------------------------------- /Example/MissionControlDemo/Assets.xcassets/AppIcon.appiconset/Icon-29.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/appculture/MissionControl-iOS/4516310d82780f4753a3c3c7b23021af5a0fc299/Example/MissionControlDemo/Assets.xcassets/AppIcon.appiconset/Icon-29.png -------------------------------------------------------------------------------- /Example/MissionControlDemo/Assets.xcassets/AppIcon.appiconset/Icon-29@2x-1.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/appculture/MissionControl-iOS/4516310d82780f4753a3c3c7b23021af5a0fc299/Example/MissionControlDemo/Assets.xcassets/AppIcon.appiconset/Icon-29@2x-1.png -------------------------------------------------------------------------------- /Example/MissionControlDemo/Assets.xcassets/AppIcon.appiconset/Icon-29@2x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/appculture/MissionControl-iOS/4516310d82780f4753a3c3c7b23021af5a0fc299/Example/MissionControlDemo/Assets.xcassets/AppIcon.appiconset/Icon-29@2x.png -------------------------------------------------------------------------------- /Example/MissionControlDemo/Assets.xcassets/AppIcon.appiconset/Icon-29@3x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/appculture/MissionControl-iOS/4516310d82780f4753a3c3c7b23021af5a0fc299/Example/MissionControlDemo/Assets.xcassets/AppIcon.appiconset/Icon-29@3x.png -------------------------------------------------------------------------------- /Example/MissionControlDemo/Assets.xcassets/AppIcon.appiconset/Icon-40.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/appculture/MissionControl-iOS/4516310d82780f4753a3c3c7b23021af5a0fc299/Example/MissionControlDemo/Assets.xcassets/AppIcon.appiconset/Icon-40.png -------------------------------------------------------------------------------- /Example/MissionControlDemo/Assets.xcassets/AppIcon.appiconset/Icon-40@2x-1.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/appculture/MissionControl-iOS/4516310d82780f4753a3c3c7b23021af5a0fc299/Example/MissionControlDemo/Assets.xcassets/AppIcon.appiconset/Icon-40@2x-1.png -------------------------------------------------------------------------------- /Example/MissionControlDemo/Assets.xcassets/AppIcon.appiconset/Icon-40@2x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/appculture/MissionControl-iOS/4516310d82780f4753a3c3c7b23021af5a0fc299/Example/MissionControlDemo/Assets.xcassets/AppIcon.appiconset/Icon-40@2x.png -------------------------------------------------------------------------------- /Example/MissionControlDemo/Assets.xcassets/AppIcon.appiconset/Icon-40@3x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/appculture/MissionControl-iOS/4516310d82780f4753a3c3c7b23021af5a0fc299/Example/MissionControlDemo/Assets.xcassets/AppIcon.appiconset/Icon-40@3x.png -------------------------------------------------------------------------------- /Example/MissionControlDemo/Assets.xcassets/AppIcon.appiconset/Icon-60@2x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/appculture/MissionControl-iOS/4516310d82780f4753a3c3c7b23021af5a0fc299/Example/MissionControlDemo/Assets.xcassets/AppIcon.appiconset/Icon-60@2x.png -------------------------------------------------------------------------------- /Example/MissionControlDemo/Assets.xcassets/AppIcon.appiconset/Icon-60@3x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/appculture/MissionControl-iOS/4516310d82780f4753a3c3c7b23021af5a0fc299/Example/MissionControlDemo/Assets.xcassets/AppIcon.appiconset/Icon-60@3x.png -------------------------------------------------------------------------------- /Example/MissionControlDemo/Assets.xcassets/AppIcon.appiconset/Icon-76.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/appculture/MissionControl-iOS/4516310d82780f4753a3c3c7b23021af5a0fc299/Example/MissionControlDemo/Assets.xcassets/AppIcon.appiconset/Icon-76.png -------------------------------------------------------------------------------- /Example/MissionControlDemo/Assets.xcassets/AppIcon.appiconset/Icon-76@2x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/appculture/MissionControl-iOS/4516310d82780f4753a3c3c7b23021af5a0fc299/Example/MissionControlDemo/Assets.xcassets/AppIcon.appiconset/Icon-76@2x.png -------------------------------------------------------------------------------- /Example/MissionControlDemo/Assets.xcassets/AppIcon.appiconset/Icon-83.5@2x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/appculture/MissionControl-iOS/4516310d82780f4753a3c3c7b23021af5a0fc299/Example/MissionControlDemo/Assets.xcassets/AppIcon.appiconset/Icon-83.5@2x.png -------------------------------------------------------------------------------- /Example/MissionControlDemo/Assets.xcassets/Contents.json: -------------------------------------------------------------------------------- 1 | { 2 | "info" : { 3 | "version" : 1, 4 | "author" : "xcode" 5 | } 6 | } -------------------------------------------------------------------------------- /Example/MissionControlDemo/Assets.xcassets/Launch Image.imageset/Contents.json: -------------------------------------------------------------------------------- 1 | { 2 | "images" : [ 3 | { 4 | "idiom" : "universal", 5 | "filename" : "Launch Image.pdf" 6 | } 7 | ], 8 | "info" : { 9 | "version" : 1, 10 | "author" : "xcode" 11 | } 12 | } -------------------------------------------------------------------------------- /Example/MissionControlDemo/Assets.xcassets/Launch Image.imageset/Launch Image.pdf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/appculture/MissionControl-iOS/4516310d82780f4753a3c3c7b23021af5a0fc299/Example/MissionControlDemo/Assets.xcassets/Launch Image.imageset/Launch Image.pdf -------------------------------------------------------------------------------- /Example/MissionControlDemo/Assets.xcassets/appculture.imageset/Contents.json: -------------------------------------------------------------------------------- 1 | { 2 | "images" : [ 3 | { 4 | "idiom" : "universal", 5 | "filename" : "appculture.pdf" 6 | } 7 | ], 8 | "info" : { 9 | "version" : 1, 10 | "author" : "xcode" 11 | } 12 | } -------------------------------------------------------------------------------- /Example/MissionControlDemo/Assets.xcassets/appculture.imageset/appculture.pdf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/appculture/MissionControl-iOS/4516310d82780f4753a3c3c7b23021af5a0fc299/Example/MissionControlDemo/Assets.xcassets/appculture.imageset/appculture.pdf -------------------------------------------------------------------------------- /Example/MissionControlDemo/Base.lproj/Launch.storyboard: -------------------------------------------------------------------------------- 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 | -------------------------------------------------------------------------------- /Example/MissionControlDemo/Base.lproj/LaunchScreen.storyboard: -------------------------------------------------------------------------------- 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 | -------------------------------------------------------------------------------- /Example/MissionControlDemo/BaseLaunchView.swift: -------------------------------------------------------------------------------- 1 | // 2 | // BaseLaunchView.swift 3 | // MissionControlDemo 4 | // 5 | // Copyright (c) 2016 appculture http://appculture.com 6 | // 7 | // Permission is hereby granted, free of charge, to any person obtaining a copy 8 | // of this software and associated documentation files (the "Software"), to deal 9 | // in the Software without restriction, including without limitation the rights 10 | // to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 11 | // copies of the Software, and to permit persons to whom the Software is 12 | // furnished to do so, subject to the following conditions: 13 | // 14 | // The above copyright notice and this permission notice shall be included in all 15 | // copies or substantial portions of the Software. 16 | // 17 | // THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 18 | // IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 19 | // FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 20 | // AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 21 | // LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 22 | // OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 23 | // SOFTWARE. 24 | // 25 | 26 | import UIKit 27 | 28 | @IBDesignable 29 | class BaseLaunchView: UIView { 30 | 31 | // MARK: - Outlets 32 | 33 | let gradient = UIView() 34 | let gradientLayer = CAGradientLayer() 35 | 36 | let button = UIView() 37 | let buttonImage = UIImageView() 38 | let buttonTitle = UILabel() 39 | 40 | let statusTitle = UILabel() 41 | let statusLight = UIView() 42 | 43 | let countdown = UILabel() 44 | 45 | // MARK: - Properties 46 | 47 | var didTapButtonAction: ((sender: AnyObject) -> Void)? 48 | 49 | var padding: CGFloat = 24.0 50 | 51 | var buttonHighlightColor = UIColor.lightGrayColor() 52 | var buttonColor = UIColor.whiteColor() { 53 | didSet { 54 | button.backgroundColor = buttonColor 55 | } 56 | } 57 | var buttonTitleColor = UIColor.darkGrayColor() { 58 | didSet { 59 | buttonTitle.textColor = buttonTitleColor 60 | } 61 | } 62 | 63 | var statusLightColor = UIColor.darkGrayColor() { 64 | didSet { 65 | statusLight.backgroundColor = statusLightColor 66 | } 67 | } 68 | var statusTitleColor = UIColor.whiteColor() { 69 | didSet { 70 | statusTitle.textColor = statusTitleColor 71 | statusLight.layer.borderColor = statusTitleColor.CGColor 72 | } 73 | } 74 | 75 | var countdownColor = UIColor.whiteColor() { 76 | didSet { 77 | countdown.textColor = countdownColor 78 | } 79 | } 80 | 81 | // MARK: - Init 82 | 83 | override init(frame: CGRect) { 84 | super.init(frame: frame) 85 | commonInit() 86 | } 87 | 88 | required init?(coder aDecoder: NSCoder) { 89 | super.init(coder: aDecoder) 90 | commonInit() 91 | } 92 | 93 | init() { 94 | super.init(frame: CGRectZero) 95 | commonInit() 96 | } 97 | 98 | func commonInit() { 99 | configureOutlets() 100 | configureHierarchy() 101 | updateConstraints() 102 | } 103 | 104 | // MARK: - Override 105 | 106 | override func layoutSublayersOfLayer(layer: CALayer) { 107 | super.layoutSublayersOfLayer(layer) 108 | gradientLayer.frame = gradient.bounds 109 | } 110 | 111 | override func touchesBegan(touches: Set, withEvent event: UIEvent?) { 112 | super.touchesBegan(touches, withEvent: event) 113 | 114 | if touchesInsideView(touches, view: button) { 115 | highlightButton() 116 | } 117 | } 118 | 119 | override func touchesMoved(touches: Set, withEvent event: UIEvent?) { 120 | super.touchesMoved(touches, withEvent: event) 121 | 122 | if !touchesInsideView(touches, view: button) { 123 | restoreButton() 124 | } 125 | } 126 | 127 | override func touchesEnded(touches: Set, withEvent event: UIEvent?) { 128 | super.touchesEnded(touches, withEvent: event) 129 | 130 | if touchesInsideView(touches, view: button) { 131 | restoreButton() 132 | if let action = didTapButtonAction { 133 | action(sender: button) 134 | } 135 | } 136 | } 137 | 138 | private func touchesInsideView(touches: Set, view: UIView) -> Bool { 139 | guard let touch = touches.first else { return false } 140 | let location = touch.locationInView(view) 141 | let insideView = CGRectContainsPoint(view.bounds, location) 142 | return insideView 143 | } 144 | 145 | private func highlightButton() { 146 | UIView.animateWithDuration(0.2, animations: { [unowned self] in 147 | self.button.backgroundColor = self.buttonHighlightColor 148 | self.buttonImage.transform = CGAffineTransformMakeRotation(CGFloat(M_PI_2)) 149 | }) 150 | } 151 | 152 | private func restoreButton() { 153 | UIView.animateWithDuration(0.2, animations: { [unowned self] in 154 | self.button.backgroundColor = self.buttonColor 155 | self.buttonImage.transform = CGAffineTransformIdentity 156 | }) 157 | } 158 | 159 | // MARK: - Configure Outlets 160 | 161 | private func configureOutlets() { 162 | configureGradient() 163 | configureButton() 164 | configureStatus() 165 | configureCountdown() 166 | } 167 | 168 | private func configureGradient() { 169 | gradient.translatesAutoresizingMaskIntoConstraints = false 170 | gradient.layer.insertSublayer(gradientLayer, atIndex: 0) 171 | 172 | gradientLayer.colors = [UIColor.orangeColor().CGColor, UIColor.blueColor().CGColor] 173 | gradientLayer.contentsScale = UIScreen.mainScreen().scale 174 | gradientLayer.drawsAsynchronously = true 175 | gradientLayer.needsDisplayOnBoundsChange = true 176 | gradientLayer.setNeedsDisplay() 177 | } 178 | 179 | private func configureButton() { 180 | button.translatesAutoresizingMaskIntoConstraints = false 181 | button.backgroundColor = buttonColor 182 | button.layer.borderColor = statusLightColor.CGColor 183 | button.layer.borderWidth = 10.0 184 | button.layer.cornerRadius = 10.0 185 | button.clipsToBounds = true 186 | 187 | buttonImage.translatesAutoresizingMaskIntoConstraints = false 188 | buttonImage.contentMode = .ScaleAspectFill 189 | buttonImage.image = UIImage(named: "appculture") 190 | 191 | buttonTitle.translatesAutoresizingMaskIntoConstraints = false 192 | buttonTitle.adjustsFontSizeToFitWidth = true 193 | buttonTitle.textAlignment = .Center 194 | buttonTitle.textColor = buttonTitleColor 195 | buttonTitle.text = "BUTTON" 196 | } 197 | 198 | private func configureStatus() { 199 | statusTitle.translatesAutoresizingMaskIntoConstraints = false 200 | statusTitle.setContentHuggingPriority(251.0, forAxis: .Vertical) 201 | statusTitle.adjustsFontSizeToFitWidth = true 202 | statusTitle.textAlignment = .Center 203 | statusTitle.textColor = statusTitleColor 204 | statusTitle.text = "STATUS" 205 | 206 | statusLight.translatesAutoresizingMaskIntoConstraints = false 207 | statusLight.backgroundColor = statusLightColor 208 | statusLight.layer.borderColor = statusTitleColor.CGColor 209 | statusLight.layer.borderWidth = 2.0 210 | statusLight.layer.cornerRadius = 16.0 211 | } 212 | 213 | private func configureCountdown() { 214 | countdown.translatesAutoresizingMaskIntoConstraints = false 215 | countdown.adjustsFontSizeToFitWidth = true 216 | countdown.textAlignment = .Center 217 | countdown.textColor = countdownColor 218 | countdown.text = "00" 219 | } 220 | 221 | // MARK: - Configure Layout 222 | 223 | private func configureHierarchy() { 224 | button.addSubview(buttonImage) 225 | button.addSubview(buttonTitle) 226 | 227 | gradient.addSubview(button) 228 | gradient.addSubview(statusTitle) 229 | gradient.addSubview(statusLight) 230 | gradient.addSubview(countdown) 231 | 232 | addSubview(gradient) 233 | } 234 | 235 | override func updateConstraints() { 236 | removeConstraints(constraints) 237 | addConstraints(allConstraints) 238 | super.updateConstraints() 239 | } 240 | 241 | // MARK: - Constraints 242 | 243 | private var allConstraints: [NSLayoutConstraint] { 244 | var constraints = gradientConstraints 245 | constraints += buttonConstraints + buttonImageConstraints + buttonTitleConstraints 246 | constraints += statusTitleConstraints + statusLightConstraints 247 | constraints += countdownConstraints 248 | return constraints 249 | } 250 | 251 | private var gradientConstraints: [NSLayoutConstraint] { 252 | let leading = gradient.leadingAnchor.constraintEqualToAnchor(leadingAnchor) 253 | let trailing = gradient.trailingAnchor.constraintEqualToAnchor(trailingAnchor) 254 | let top = gradient.topAnchor.constraintEqualToAnchor(topAnchor) 255 | let bottom = gradient.bottomAnchor.constraintEqualToAnchor(bottomAnchor) 256 | return [leading, trailing, top, bottom] 257 | } 258 | 259 | private var buttonConstraints: [NSLayoutConstraint] { 260 | let leading = button.leadingAnchor.constraintEqualToAnchor(leadingAnchor, constant: padding) 261 | let trailing = button.trailingAnchor.constraintEqualToAnchor(trailingAnchor, constant: -padding) 262 | let bottom = button.bottomAnchor.constraintEqualToAnchor(bottomAnchor, constant: -padding) 263 | let height = button.heightAnchor.constraintEqualToConstant(90.0) 264 | return [leading, trailing, bottom, height] 265 | } 266 | 267 | private var buttonImageConstraints: [NSLayoutConstraint] { 268 | let leading = buttonImage.leadingAnchor.constraintEqualToAnchor(button.leadingAnchor, constant: 20.0) 269 | let top = buttonImage.topAnchor.constraintEqualToAnchor(button.topAnchor, constant: 22.0) 270 | let bottom = buttonImage.bottomAnchor.constraintEqualToAnchor(button.bottomAnchor, constant: -22.0) 271 | let width = buttonImage.widthAnchor.constraintEqualToAnchor(buttonImage.heightAnchor) 272 | return [leading, top, bottom, width] 273 | } 274 | 275 | private var buttonTitleConstraints: [NSLayoutConstraint] { 276 | let leading = buttonTitle.leadingAnchor.constraintEqualToAnchor(buttonImage.trailingAnchor, constant: 12.0) 277 | let trailing = buttonTitle.trailingAnchor.constraintEqualToAnchor(button.trailingAnchor, constant: -22.0) 278 | let centerY = buttonTitle.centerYAnchor.constraintEqualToAnchor(button.centerYAnchor) 279 | return [leading, trailing, centerY] 280 | } 281 | 282 | private var statusTitleConstraints: [NSLayoutConstraint] { 283 | let leading = statusTitle.leadingAnchor.constraintEqualToAnchor(button.leadingAnchor) 284 | let trailing = statusTitle.trailingAnchor.constraintEqualToAnchor(button.trailingAnchor) 285 | let bottom = statusTitle.bottomAnchor.constraintEqualToAnchor(button.topAnchor, constant: -padding) 286 | return [leading, trailing, bottom] 287 | } 288 | 289 | private var statusLightConstraints: [NSLayoutConstraint] { 290 | let centerX = statusLight.centerXAnchor.constraintEqualToAnchor(centerXAnchor) 291 | let bottom = statusLight.bottomAnchor.constraintEqualToAnchor(statusTitle.topAnchor, constant: -padding) 292 | let width = statusLight.widthAnchor.constraintEqualToConstant(32.0) 293 | let height = statusLight.heightAnchor.constraintEqualToConstant(32.0) 294 | return [centerX, bottom, width, height] 295 | } 296 | 297 | private var countdownConstraints: [NSLayoutConstraint] { 298 | let leading = countdown.leadingAnchor.constraintEqualToAnchor(leadingAnchor) 299 | let trailing = countdown.trailingAnchor.constraintEqualToAnchor(trailingAnchor) 300 | let top = countdown.topAnchor.constraintEqualToAnchor(topAnchor) 301 | let bottom = countdown.bottomAnchor.constraintEqualToAnchor(statusLight.topAnchor) 302 | return [leading, trailing, top, bottom] 303 | } 304 | 305 | // MARK: - Interface Builder 306 | 307 | override func prepareForInterfaceBuilder() { 308 | let bundle = NSBundle(forClass: self.dynamicType) 309 | let image = UIImage(named: "appculture", inBundle: bundle, compatibleWithTraitCollection: traitCollection) 310 | buttonImage.image = image 311 | } 312 | 313 | } 314 | 315 | extension UIColor { 316 | 317 | // MARK: - HEX Color 318 | 319 | convenience init (hex: String) { 320 | var colorString: String = hex 321 | if (hex.hasPrefix("#")) { 322 | let index = hex.startIndex.advancedBy(1) 323 | colorString = colorString.substringFromIndex(index) 324 | } 325 | 326 | var rgbValue:UInt32 = 0 327 | NSScanner(string: colorString).scanHexInt(&rgbValue) 328 | 329 | self.init( 330 | red: CGFloat((rgbValue & 0xFF0000) >> 16) / 255.0, 331 | green: CGFloat((rgbValue & 0x00FF00) >> 8) / 255.0, 332 | blue: CGFloat(rgbValue & 0x0000FF) / 255.0, 333 | alpha: CGFloat(1.0) 334 | ) 335 | } 336 | 337 | } 338 | -------------------------------------------------------------------------------- /Example/MissionControlDemo/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 | APPL 17 | CFBundleShortVersionString 18 | 1.0 19 | CFBundleSignature 20 | ???? 21 | CFBundleVersion 22 | 1 23 | LSRequiresIPhoneOS 24 | 25 | UIAppFonts 26 | 27 | NAS966.TTF 28 | 29 | UILaunchStoryboardName 30 | LaunchScreen 31 | UIMainStoryboardFile 32 | Launch 33 | UIRequiredDeviceCapabilities 34 | 35 | armv7 36 | 37 | UIStatusBarHidden 38 | 39 | UISupportedInterfaceOrientations 40 | 41 | UIInterfaceOrientationPortrait 42 | 43 | UISupportedInterfaceOrientations~ipad 44 | 45 | UIInterfaceOrientationPortrait 46 | UIInterfaceOrientationPortraitUpsideDown 47 | UIInterfaceOrientationLandscapeLeft 48 | UIInterfaceOrientationLandscapeRight 49 | 50 | NSAppTransportSecurity 51 | 52 | NSAllowsArbitraryLoads 53 | 54 | 55 | 56 | 57 | -------------------------------------------------------------------------------- /Example/MissionControlDemo/LaunchBrain.swift: -------------------------------------------------------------------------------- 1 | // 2 | // LaunchBrain.swift 3 | // MissionControlDemo 4 | // 5 | // Copyright (c) 2016 appculture http://appculture.com 6 | // 7 | // Permission is hereby granted, free of charge, to any person obtaining a copy 8 | // of this software and associated documentation files (the "Software"), to deal 9 | // in the Software without restriction, including without limitation the rights 10 | // to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 11 | // copies of the Software, and to permit persons to whom the Software is 12 | // furnished to do so, subject to the following conditions: 13 | // 14 | // The above copyright notice and this permission notice shall be included in all 15 | // copies or substantial portions of the Software. 16 | // 17 | // THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 18 | // IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 19 | // FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 20 | // AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 21 | // LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 22 | // OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 23 | // SOFTWARE. 24 | // 25 | 26 | import UIKit 27 | import MissionControl 28 | 29 | protocol LaunchDelegate: class { 30 | 31 | } 32 | 33 | enum LaunchState: String { 34 | case Offline 35 | case Ready 36 | case Countdown 37 | case Launched 38 | case Failed 39 | case Aborted 40 | } 41 | 42 | class LaunchBrain: MissionControlDelegate { 43 | 44 | // MARK: - Properties 45 | 46 | var view: LaunchView! 47 | weak var delegate: LaunchDelegate? 48 | 49 | var state: LaunchState = .Offline { 50 | didSet { 51 | updateUIForState(state) 52 | } 53 | } 54 | 55 | var seconds: Int = 0 { 56 | didSet { 57 | view.countdown.text = String(format: "%02d", seconds) 58 | } 59 | } 60 | 61 | var timer: NSTimer? 62 | 63 | private var launchForce: Double { 64 | return 1.0 - ConfigDouble("LaunchForce", fallback: 0.5) 65 | } 66 | 67 | // MARK: - Init 68 | 69 | init(view: LaunchView, delegate: LaunchDelegate) { 70 | self.view = view 71 | self.delegate = delegate 72 | 73 | MissionControl.delegate = self 74 | 75 | self.view.didTapButtonAction = { sender in 76 | self.didTapButton(sender) 77 | } 78 | 79 | updateUI() 80 | } 81 | 82 | // MARK: - MissionControlDelegate 83 | 84 | func missionControlDidRefreshConfig(old old: [String : AnyObject]?, new: [String : AnyObject]) { 85 | print("missionControlDidRefreshConfig") 86 | updateUIForState(state) 87 | } 88 | 89 | func missionControlDidFailRefreshingConfig(error error: ErrorType) { 90 | print("missionControlDidFailRefreshingConfig") 91 | 92 | stopCountdown() 93 | 94 | switch state { 95 | case .Countdown: 96 | state = .Failed 97 | default: 98 | state = .Offline 99 | } 100 | } 101 | 102 | // MARK: - Actions 103 | 104 | func didTapButton(sender: AnyObject) { 105 | switch state { 106 | case .Offline: 107 | ConfigBoolForce("Ready", fallback: false, completion: { (forced) in 108 | if forced { 109 | self.state = .Ready 110 | } else { 111 | self.state = .Failed 112 | } 113 | }) 114 | case .Ready: 115 | state = .Countdown 116 | case .Countdown: 117 | state = .Aborted 118 | case .Failed, .Aborted, .Launched: 119 | state = .Offline 120 | } 121 | } 122 | 123 | // MARK: - UI 124 | 125 | func updateUI() { 126 | updateUIForState(state) 127 | } 128 | 129 | private func updateUIForState(state: LaunchState) { 130 | updateUIForAnyState(state) 131 | 132 | switch state { 133 | case .Offline: 134 | updateUIForOfflineState() 135 | case .Ready: 136 | updateUIForReadyState() 137 | case .Countdown: 138 | updateUIForCountdownState() 139 | startCountdown() 140 | case .Launched: 141 | updateUIForLaunchedState() 142 | case .Failed: 143 | updateUIForFailedState() 144 | case .Aborted: 145 | stopCountdown() 146 | updateUIForAbortedState() 147 | } 148 | } 149 | 150 | private func updateUIForAnyState(state: LaunchState) { 151 | let color1 = UIColor(hex: ConfigString("TopColor", fallback: "#000000")) 152 | let color2 = UIColor(hex: ConfigString("BottomColor", fallback: "#4A90E2")) 153 | view.gradientLayer.colors = [color1.CGColor, color2.CGColor] 154 | 155 | view.button.layer.borderColor = colorForState(state).CGColor 156 | view.buttonTitle.text = commandForState(state) 157 | 158 | view.stopBlinkingStatusLight() 159 | view.statusTitle.text = "STATUS: \(state.rawValue.capitalizedString)" 160 | view.statusLightOnColor = colorForState(state) 161 | view.statusLightOn = true 162 | 163 | view.countdown.alpha = 1.0 164 | } 165 | 166 | private func updateUIForOfflineState() { 167 | view.stopAnimatingGradient() 168 | view.stopRotatingButtonImage() 169 | 170 | view.button.layer.borderColor = view.statusLightOffColor.CGColor 171 | view.countdown.alpha = 0.1 172 | seconds = 0 173 | view.startBlinkingStatusLight(timeInterval: 0.5) 174 | } 175 | 176 | private func updateUIForReadyState() { 177 | seconds = ConfigInt("CountdownDuration", fallback: 10) 178 | } 179 | 180 | private func updateUIForCountdownState() { 181 | let duration = launchForce * 4 182 | view.rotateButtonImageWithDuration(duration) 183 | view.startBlinkingStatusLight(timeInterval: 0.25) 184 | } 185 | 186 | private func updateUIForLaunchedState() { 187 | view.countdown.text = "OK" 188 | 189 | view.animateGradientWithDuration(launchForce * 8) 190 | 191 | view.stopRotatingButtonImage() 192 | let duration = launchForce * 2 193 | view.rotateButtonImageWithDuration(duration) 194 | } 195 | 196 | private func updateUIForFailedState() { 197 | view.countdown.text = "F" 198 | view.startBlinkingStatusLight(timeInterval: 0.5) 199 | } 200 | 201 | private func updateUIForAbortedState() { 202 | view.stopRotatingButtonImage() 203 | view.countdown.text = "A" 204 | view.startBlinkingStatusLight(timeInterval: 0.25) 205 | } 206 | 207 | private func commandForState(state: LaunchState) -> String { 208 | switch state { 209 | case .Offline: 210 | return "CONNECT" 211 | case .Ready: 212 | return "LAUNCH" 213 | case .Countdown: 214 | return "ABORT" 215 | case .Launched, .Failed, .Aborted: 216 | return "RETRY" 217 | } 218 | } 219 | 220 | private func colorForState(state: LaunchState) -> UIColor { 221 | switch state { 222 | case .Offline: 223 | return UIColor(hex: ConfigString("OfflineColor", fallback: "#F8E71C")) 224 | case .Ready: 225 | return UIColor(hex: ConfigString("ReadyColor", fallback: "#7ED321")) 226 | case .Countdown: 227 | return UIColor(hex: ConfigString("CountdownColor", fallback: "#F5A623")) 228 | case .Launched: 229 | return UIColor(hex: ConfigString("LaunchedColor", fallback: "#BD10E0")) 230 | case .Failed: 231 | return UIColor(hex: ConfigString("FailedColor", fallback: "#D0021B")) 232 | case .Aborted: 233 | return UIColor(hex: ConfigString("AbortedColor", fallback: "#D0021B")) 234 | } 235 | } 236 | 237 | // MARK: - Countdown 238 | 239 | private func startCountdown() { 240 | if timer == nil { 241 | timer = NSTimer.scheduledTimerWithTimeInterval(1.0, 242 | target: self, 243 | selector: #selector(timerTick(_:)), 244 | userInfo: nil, repeats: true) 245 | } 246 | } 247 | 248 | private func stopCountdown() { 249 | timer?.invalidate() 250 | timer = nil 251 | } 252 | 253 | @objc func timerTick(sender: NSTimer) { 254 | ConfigBoolForce("Abort", fallback: true) { (forced) in 255 | if forced { 256 | self.stopCountdown() 257 | self.state = .Aborted 258 | } 259 | } 260 | 261 | if seconds - 1 >= 0 { 262 | seconds -= 1 263 | } else { 264 | stopCountdown() 265 | state = .Launched 266 | } 267 | } 268 | 269 | } 270 | -------------------------------------------------------------------------------- /Example/MissionControlDemo/LaunchView.swift: -------------------------------------------------------------------------------- 1 | // 2 | // LaunchView.swift 3 | // MissionControlDemo 4 | // 5 | // Copyright (c) 2016 appculture http://appculture.com 6 | // 7 | // Permission is hereby granted, free of charge, to any person obtaining a copy 8 | // of this software and associated documentation files (the "Software"), to deal 9 | // in the Software without restriction, including without limitation the rights 10 | // to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 11 | // copies of the Software, and to permit persons to whom the Software is 12 | // furnished to do so, subject to the following conditions: 13 | // 14 | // The above copyright notice and this permission notice shall be included in all 15 | // copies or substantial portions of the Software. 16 | // 17 | // THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 18 | // IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 19 | // FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 20 | // AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 21 | // LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 22 | // OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 23 | // SOFTWARE. 24 | // 25 | 26 | import UIKit 27 | 28 | class LaunchView: BaseLaunchView { 29 | 30 | // MARK: - Properties 31 | 32 | var blinkTimer: NSTimer? 33 | var statusLightOffColor = UIColor(hex: "#4A4A4A") 34 | var statusLightOnColor = UIColor.whiteColor() 35 | var statusLightOn = false { 36 | didSet { 37 | if statusLightOn { 38 | UIView.animateWithDuration(0.2) { 39 | self.turnStatusLightOn() 40 | } 41 | } else { 42 | UIView.animateWithDuration(0.2) { 43 | self.turnStatusLightOff() 44 | } 45 | } 46 | } 47 | } 48 | 49 | // MARK: - Lifecycle 50 | 51 | override func commonInit() { 52 | super.commonInit() 53 | 54 | configureDefaultUI() 55 | } 56 | 57 | // MARK: - UI 58 | 59 | private func configureDefaultUI() { 60 | padding = 24.0 61 | 62 | gradientLayer.colors = [UIColor(hex: "#000000").CGColor, UIColor(hex: "#4A90E2").CGColor] 63 | gradientLayer.locations = [0.0, 1.0] 64 | 65 | buttonColor = UIColor.whiteColor() 66 | buttonHighlightColor = UIColor(hex: "#E4F6F6") 67 | statusTitleColor = UIColor.whiteColor() 68 | countdownColor = UIColor.whiteColor() 69 | 70 | buttonTitle.font = UIFont(name: "AvenirNext-Heavy", size: 36.0) 71 | statusTitle.font = UIFont(name: "Nasa-Display", size: 40.0) 72 | countdown.font = UIFont(name: "Nasa-Display", size: 256.0) 73 | } 74 | 75 | // MARK: - Blink 76 | 77 | func startBlinkingStatusLight(timeInterval timeInterval: NSTimeInterval) { 78 | blinkTimer = NSTimer.scheduledTimerWithTimeInterval(timeInterval, 79 | target: self, 80 | selector: #selector(blinkStatusLight), 81 | userInfo: nil, repeats: true) 82 | } 83 | 84 | func stopBlinkingStatusLight() { 85 | blinkTimer?.invalidate() 86 | blinkTimer = nil 87 | } 88 | 89 | @objc func blinkStatusLight() { 90 | statusLightOn = !statusLightOn 91 | } 92 | 93 | func turnStatusLightOn() { 94 | statusLightColor = statusLightOnColor 95 | 96 | statusLight.layer.shadowColor = statusLightOnColor.CGColor 97 | statusLight.layer.shadowOffset = CGSize(width: 0.0, height: 0.0) 98 | statusLight.layer.shadowOpacity = 1.0 99 | statusLight.layer.shadowRadius = 5.0 100 | } 101 | 102 | func turnStatusLightOff() { 103 | statusLightColor = statusLightOffColor 104 | statusLight.layer.shadowOpacity = 0.0 105 | } 106 | 107 | // MARK: - Button Image Rotation 108 | 109 | func rotateButtonImageWithDuration(duration: Double) { 110 | buttonImage.rotate(withDuration: duration) 111 | } 112 | 113 | func stopRotatingButtonImage() { 114 | buttonImage.stopRotation() 115 | } 116 | 117 | // MARK: - Gradient Animation 118 | 119 | func animateGradientWithDuration(duration: Double) { 120 | animateGradientLayer(gradientLayer, withDuration: duration) 121 | } 122 | 123 | func stopAnimatingGradient() { 124 | stopGradientAnimation(gradientLayer) 125 | } 126 | 127 | } 128 | 129 | private extension UIView { 130 | 131 | @nonobjc static let rotationKey = "AERotation" 132 | 133 | func rotate(withDuration duration: Double = 1.0) { 134 | if layer.animationForKey(UIView.rotationKey) == nil { 135 | let rotationAnimation = CABasicAnimation(keyPath: "transform.rotation") 136 | 137 | rotationAnimation.fromValue = 0.0 138 | rotationAnimation.toValue = Float(M_PI * 2.0) 139 | rotationAnimation.duration = duration 140 | rotationAnimation.repeatCount = Float.infinity 141 | 142 | layer.addAnimation(rotationAnimation, forKey: UIView.rotationKey) 143 | } 144 | } 145 | 146 | func stopRotation() { 147 | layer.removeAnimationForKey(UIView.rotationKey) 148 | } 149 | 150 | @nonobjc static let gradientKey = "AEGradientAnimation" 151 | 152 | func animateGradientLayer(gradientLayer: CAGradientLayer, withDuration duration: Double = 2.0) { 153 | if gradientLayer.animationForKey(UIView.gradientKey) == nil { 154 | 155 | let sequenceDuration = duration / 4.0 156 | let currentLocations = [0.0, 1.0] 157 | let newLocations = [1.0, 1.0] 158 | 159 | let color1 = gradientLayer.colors![0] 160 | let color2 = gradientLayer.colors![1] 161 | 162 | // 1 / 4 163 | 164 | let locationAnimation1 = CABasicAnimation(keyPath: "locations") 165 | locationAnimation1.fromValue = currentLocations 166 | locationAnimation1.toValue = newLocations 167 | locationAnimation1.duration = sequenceDuration 168 | locationAnimation1.beginTime = 0.0 169 | 170 | // 2 / 4 171 | 172 | let colorAnimation1 = CABasicAnimation(keyPath: "colors") 173 | colorAnimation1.fromValue = [color1, color1] 174 | colorAnimation1.toValue = gradientLayer.colors?.reverse() 175 | colorAnimation1.duration = sequenceDuration 176 | colorAnimation1.removedOnCompletion = false 177 | colorAnimation1.fillMode = kCAFillModeForwards 178 | colorAnimation1.beginTime = sequenceDuration 179 | 180 | // 3 / 4 181 | 182 | let locationAnimation2 = CABasicAnimation(keyPath: "locations") 183 | locationAnimation2.fromValue = currentLocations 184 | locationAnimation2.toValue = newLocations 185 | locationAnimation2.duration = sequenceDuration 186 | locationAnimation2.beginTime = 2 * sequenceDuration 187 | 188 | // 4 / 4 189 | 190 | let colorAnimation2 = CABasicAnimation(keyPath: "colors") 191 | colorAnimation2.fromValue = [color2, color2] 192 | colorAnimation2.toValue = gradientLayer.colors 193 | colorAnimation2.duration = sequenceDuration 194 | colorAnimation2.removedOnCompletion = false 195 | colorAnimation2.fillMode = kCAFillModeForwards 196 | colorAnimation2.beginTime = 3 * sequenceDuration 197 | 198 | // Group 199 | 200 | let group = CAAnimationGroup() 201 | group.duration = duration 202 | group.animations = [locationAnimation1, colorAnimation1, locationAnimation2, colorAnimation2] 203 | group.repeatCount = Float.infinity 204 | 205 | gradientLayer.addAnimation(group, forKey: UIView.gradientKey) 206 | } 207 | } 208 | 209 | func stopGradientAnimation(gradientLayer: CAGradientLayer) { 210 | gradientLayer.removeAnimationForKey(UIView.gradientKey) 211 | } 212 | 213 | } 214 | -------------------------------------------------------------------------------- /Example/MissionControlDemo/LaunchViewController.swift: -------------------------------------------------------------------------------- 1 | // 2 | // LaunchViewController.swift 3 | // MissionControlDemo 4 | // 5 | // Copyright (c) 2016 appculture http://appculture.com 6 | // 7 | // Permission is hereby granted, free of charge, to any person obtaining a copy 8 | // of this software and associated documentation files (the "Software"), to deal 9 | // in the Software without restriction, including without limitation the rights 10 | // to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 11 | // copies of the Software, and to permit persons to whom the Software is 12 | // furnished to do so, subject to the following conditions: 13 | // 14 | // The above copyright notice and this permission notice shall be included in all 15 | // copies or substantial portions of the Software. 16 | // 17 | // THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 18 | // IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 19 | // FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 20 | // AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 21 | // LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 22 | // OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 23 | // SOFTWARE. 24 | // 25 | 26 | import UIKit 27 | 28 | class LaunchViewController: UIViewController, LaunchDelegate { 29 | 30 | // MARK: - Properties 31 | 32 | var launch: LaunchBrain! 33 | @IBOutlet var launchView: LaunchView! 34 | 35 | // MARK: - Lifecycle 36 | 37 | override func viewDidLoad() { 38 | super.viewDidLoad() 39 | 40 | launch = LaunchBrain(view: launchView, delegate: self) 41 | } 42 | 43 | override func prefersStatusBarHidden() -> Bool { 44 | return true 45 | } 46 | 47 | } 48 | -------------------------------------------------------------------------------- /Example/MissionControlDemo/NAS966.TTF: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/appculture/MissionControl-iOS/4516310d82780f4753a3c3c7b23021af5a0fc299/Example/MissionControlDemo/NAS966.TTF -------------------------------------------------------------------------------- /Images/MissionControl-01-Offline.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/appculture/MissionControl-iOS/4516310d82780f4753a3c3c7b23021af5a0fc299/Images/MissionControl-01-Offline.png -------------------------------------------------------------------------------- /Images/MissionControl-02-Ready.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/appculture/MissionControl-iOS/4516310d82780f4753a3c3c7b23021af5a0fc299/Images/MissionControl-02-Ready.png -------------------------------------------------------------------------------- /Images/MissionControl-03-Countdown.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/appculture/MissionControl-iOS/4516310d82780f4753a3c3c7b23021af5a0fc299/Images/MissionControl-03-Countdown.png -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | Copyright (c) 2016 appculture http://appculture.com 2 | 3 | Permission is hereby granted, free of charge, to any person obtaining a copy 4 | of this software and associated documentation files (the "Software"), to deal 5 | in the Software without restriction, including without limitation the rights 6 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 7 | copies of the Software, and to permit persons to whom the Software is 8 | furnished to do so, subject to the following conditions: 9 | 10 | The above copyright notice and this permission notice shall be included in all 11 | copies or substantial portions of the Software. 12 | 13 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 14 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 15 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 16 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 17 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 18 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 19 | SOFTWARE. -------------------------------------------------------------------------------- /MissionControl.podspec: -------------------------------------------------------------------------------- 1 | Pod::Spec.new do |s| 2 | s.name = 'MissionControl' 3 | s.version = '1.0.0' 4 | s.summary = 'Super powerfull remote config utility written in Swift (iOS, watchOS, tvOS, OSX)' 5 | 6 | s.homepage = 'http://appculture.com' 7 | s.license = { :type => 'MIT', :file => 'LICENSE' } 8 | s.author = { 'appculture' => 'dev@appculture.com' } 9 | s.social_media_url = 'http://twitter.com/appculture_ag' 10 | 11 | s.ios.deployment_target = '8.0' 12 | s.watchos.deployment_target = '2.0' 13 | s.tvos.deployment_target = '9.0' 14 | s.osx.deployment_target = '10.10' 15 | 16 | s.source = { :git => 'https://github.com/appculture/MissionControl-iOS.git', :tag => s.version } 17 | s.source_files = 'Sources/*.swift' 18 | end -------------------------------------------------------------------------------- /MissionControl.xcodeproj/project.pbxproj: -------------------------------------------------------------------------------- 1 | // !$*UTF8*$! 2 | { 3 | archiveVersion = 1; 4 | classes = { 5 | }; 6 | objectVersion = 46; 7 | objects = { 8 | 9 | /* Begin PBXBuildFile section */ 10 | 8B03C1E81CF5E17F00B09B48 /* MissionControl.swift in Sources */ = {isa = PBXBuildFile; fileRef = 8B6313951CE5FA2B0029DC98 /* MissionControl.swift */; }; 11 | 8B03C1E91CF5E18300B09B48 /* MissionControl.h in Headers */ = {isa = PBXBuildFile; fileRef = 8B63137E1CE5F9A10029DC98 /* MissionControl.h */; settings = {ATTRIBUTES = (Public, ); }; }; 12 | 8B03C1F91CF5E1DD00B09B48 /* MissionControl.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 8B03C1EF1CF5E1DD00B09B48 /* MissionControl.framework */; }; 13 | 8B03C2061CF5E22E00B09B48 /* MissionControl.swift in Sources */ = {isa = PBXBuildFile; fileRef = 8B6313951CE5FA2B0029DC98 /* MissionControl.swift */; }; 14 | 8B03C2071CF5E23200B09B48 /* MissionControl.h in Headers */ = {isa = PBXBuildFile; fileRef = 8B63137E1CE5F9A10029DC98 /* MissionControl.h */; settings = {ATTRIBUTES = (Public, ); }; }; 15 | 8B03C2081CF5E24500B09B48 /* MissionControlTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 8B63138A1CE5F9A10029DC98 /* MissionControlTests.swift */; }; 16 | 8B03C2181CF5E28C00B09B48 /* MissionControl.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 8B03C20E1CF5E28C00B09B48 /* MissionControl.framework */; }; 17 | 8B03C2251CF5E2B800B09B48 /* MissionControl.swift in Sources */ = {isa = PBXBuildFile; fileRef = 8B6313951CE5FA2B0029DC98 /* MissionControl.swift */; }; 18 | 8B03C2261CF5E2C600B09B48 /* MissionControl.h in Headers */ = {isa = PBXBuildFile; fileRef = 8B63137E1CE5F9A10029DC98 /* MissionControl.h */; settings = {ATTRIBUTES = (Public, ); }; }; 19 | 8B03C2271CF5E2D200B09B48 /* MissionControlTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 8B63138A1CE5F9A10029DC98 /* MissionControlTests.swift */; }; 20 | 8B63137F1CE5F9A10029DC98 /* MissionControl.h in Headers */ = {isa = PBXBuildFile; fileRef = 8B63137E1CE5F9A10029DC98 /* MissionControl.h */; settings = {ATTRIBUTES = (Public, ); }; }; 21 | 8B6313861CE5F9A10029DC98 /* MissionControl.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 8B63137B1CE5F9A10029DC98 /* MissionControl.framework */; }; 22 | 8B63138B1CE5F9A10029DC98 /* MissionControlTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 8B63138A1CE5F9A10029DC98 /* MissionControlTests.swift */; }; 23 | 8B6313961CE5FA2B0029DC98 /* MissionControl.swift in Sources */ = {isa = PBXBuildFile; fileRef = 8B6313951CE5FA2B0029DC98 /* MissionControl.swift */; }; 24 | /* End PBXBuildFile section */ 25 | 26 | /* Begin PBXContainerItemProxy section */ 27 | 8B03C1FA1CF5E1DD00B09B48 /* PBXContainerItemProxy */ = { 28 | isa = PBXContainerItemProxy; 29 | containerPortal = 8B6313721CE5F9A10029DC98 /* Project object */; 30 | proxyType = 1; 31 | remoteGlobalIDString = 8B03C1EE1CF5E1DD00B09B48; 32 | remoteInfo = "MissionControl tvOS"; 33 | }; 34 | 8B03C2191CF5E28C00B09B48 /* PBXContainerItemProxy */ = { 35 | isa = PBXContainerItemProxy; 36 | containerPortal = 8B6313721CE5F9A10029DC98 /* Project object */; 37 | proxyType = 1; 38 | remoteGlobalIDString = 8B03C20D1CF5E28C00B09B48; 39 | remoteInfo = "MissionControl OSX"; 40 | }; 41 | 8B6313871CE5F9A10029DC98 /* PBXContainerItemProxy */ = { 42 | isa = PBXContainerItemProxy; 43 | containerPortal = 8B6313721CE5F9A10029DC98 /* Project object */; 44 | proxyType = 1; 45 | remoteGlobalIDString = 8B63137A1CE5F9A10029DC98; 46 | remoteInfo = ACConfig; 47 | }; 48 | /* End PBXContainerItemProxy section */ 49 | 50 | /* Begin PBXFileReference section */ 51 | 8B03C1D11CF5DC7C00B09B48 /* LICENSE */ = {isa = PBXFileReference; lastKnownFileType = text; path = LICENSE; sourceTree = ""; }; 52 | 8B03C1DA1CF5DF1300B09B48 /* Package.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Package.swift; sourceTree = ""; }; 53 | 8B03C1E01CF5E10500B09B48 /* MissionControl.framework */ = {isa = PBXFileReference; explicitFileType = wrapper.framework; includeInIndex = 0; path = MissionControl.framework; sourceTree = BUILT_PRODUCTS_DIR; }; 54 | 8B03C1EF1CF5E1DD00B09B48 /* MissionControl.framework */ = {isa = PBXFileReference; explicitFileType = wrapper.framework; includeInIndex = 0; path = MissionControl.framework; sourceTree = BUILT_PRODUCTS_DIR; }; 55 | 8B03C1F81CF5E1DD00B09B48 /* MissionControl tvOS Tests.xctest */ = {isa = PBXFileReference; explicitFileType = wrapper.cfbundle; includeInIndex = 0; path = "MissionControl tvOS Tests.xctest"; sourceTree = BUILT_PRODUCTS_DIR; }; 56 | 8B03C20E1CF5E28C00B09B48 /* MissionControl.framework */ = {isa = PBXFileReference; explicitFileType = wrapper.framework; includeInIndex = 0; path = MissionControl.framework; sourceTree = BUILT_PRODUCTS_DIR; }; 57 | 8B03C2171CF5E28C00B09B48 /* MissionControl OSX Tests.xctest */ = {isa = PBXFileReference; explicitFileType = wrapper.cfbundle; includeInIndex = 0; path = "MissionControl OSX Tests.xctest"; sourceTree = BUILT_PRODUCTS_DIR; }; 58 | 8B03C2281CF5E83900B09B48 /* MissionControl.podspec */ = {isa = PBXFileReference; lastKnownFileType = text; path = MissionControl.podspec; sourceTree = ""; }; 59 | 8B03C2291CF5EEA900B09B48 /* CHANGELOG.md */ = {isa = PBXFileReference; lastKnownFileType = net.daringfireball.markdown; path = CHANGELOG.md; sourceTree = ""; }; 60 | 8B03C22A1CF5EEF500B09B48 /* README.md */ = {isa = PBXFileReference; lastKnownFileType = net.daringfireball.markdown; path = README.md; sourceTree = ""; }; 61 | 8B63137B1CE5F9A10029DC98 /* MissionControl.framework */ = {isa = PBXFileReference; explicitFileType = wrapper.framework; includeInIndex = 0; path = MissionControl.framework; sourceTree = BUILT_PRODUCTS_DIR; }; 62 | 8B63137E1CE5F9A10029DC98 /* MissionControl.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = MissionControl.h; sourceTree = ""; }; 63 | 8B6313801CE5F9A10029DC98 /* Info.plist */ = {isa = PBXFileReference; lastKnownFileType = text.plist.xml; path = Info.plist; sourceTree = ""; }; 64 | 8B6313851CE5F9A10029DC98 /* MissionControl iOS Tests.xctest */ = {isa = PBXFileReference; explicitFileType = wrapper.cfbundle; includeInIndex = 0; path = "MissionControl iOS Tests.xctest"; sourceTree = BUILT_PRODUCTS_DIR; }; 65 | 8B63138A1CE5F9A10029DC98 /* MissionControlTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MissionControlTests.swift; sourceTree = ""; }; 66 | 8B63138C1CE5F9A10029DC98 /* Info.plist */ = {isa = PBXFileReference; lastKnownFileType = text.plist.xml; path = Info.plist; sourceTree = ""; }; 67 | 8B6313951CE5FA2B0029DC98 /* MissionControl.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = MissionControl.swift; sourceTree = ""; }; 68 | /* End PBXFileReference section */ 69 | 70 | /* Begin PBXFrameworksBuildPhase section */ 71 | 8B03C1DC1CF5E10500B09B48 /* Frameworks */ = { 72 | isa = PBXFrameworksBuildPhase; 73 | buildActionMask = 2147483647; 74 | files = ( 75 | ); 76 | runOnlyForDeploymentPostprocessing = 0; 77 | }; 78 | 8B03C1EB1CF5E1DD00B09B48 /* Frameworks */ = { 79 | isa = PBXFrameworksBuildPhase; 80 | buildActionMask = 2147483647; 81 | files = ( 82 | ); 83 | runOnlyForDeploymentPostprocessing = 0; 84 | }; 85 | 8B03C1F51CF5E1DD00B09B48 /* Frameworks */ = { 86 | isa = PBXFrameworksBuildPhase; 87 | buildActionMask = 2147483647; 88 | files = ( 89 | 8B03C1F91CF5E1DD00B09B48 /* MissionControl.framework in Frameworks */, 90 | ); 91 | runOnlyForDeploymentPostprocessing = 0; 92 | }; 93 | 8B03C20A1CF5E28C00B09B48 /* Frameworks */ = { 94 | isa = PBXFrameworksBuildPhase; 95 | buildActionMask = 2147483647; 96 | files = ( 97 | ); 98 | runOnlyForDeploymentPostprocessing = 0; 99 | }; 100 | 8B03C2141CF5E28C00B09B48 /* Frameworks */ = { 101 | isa = PBXFrameworksBuildPhase; 102 | buildActionMask = 2147483647; 103 | files = ( 104 | 8B03C2181CF5E28C00B09B48 /* MissionControl.framework in Frameworks */, 105 | ); 106 | runOnlyForDeploymentPostprocessing = 0; 107 | }; 108 | 8B6313771CE5F9A10029DC98 /* Frameworks */ = { 109 | isa = PBXFrameworksBuildPhase; 110 | buildActionMask = 2147483647; 111 | files = ( 112 | ); 113 | runOnlyForDeploymentPostprocessing = 0; 114 | }; 115 | 8B6313821CE5F9A10029DC98 /* Frameworks */ = { 116 | isa = PBXFrameworksBuildPhase; 117 | buildActionMask = 2147483647; 118 | files = ( 119 | 8B6313861CE5F9A10029DC98 /* MissionControl.framework in Frameworks */, 120 | ); 121 | runOnlyForDeploymentPostprocessing = 0; 122 | }; 123 | /* End PBXFrameworksBuildPhase section */ 124 | 125 | /* Begin PBXGroup section */ 126 | 8B03C1D61CF5DC9B00B09B48 /* Supporting Files */ = { 127 | isa = PBXGroup; 128 | children = ( 129 | 8B03C1D11CF5DC7C00B09B48 /* LICENSE */, 130 | 8B03C22A1CF5EEF500B09B48 /* README.md */, 131 | 8B03C2291CF5EEA900B09B48 /* CHANGELOG.md */, 132 | 8B03C2281CF5E83900B09B48 /* MissionControl.podspec */, 133 | 8B03C1DA1CF5DF1300B09B48 /* Package.swift */, 134 | ); 135 | name = "Supporting Files"; 136 | sourceTree = ""; 137 | }; 138 | 8B6313711CE5F9A10029DC98 = { 139 | isa = PBXGroup; 140 | children = ( 141 | 8B63137D1CE5F9A10029DC98 /* Sources */, 142 | 8B6313891CE5F9A10029DC98 /* Tests */, 143 | 8B63137C1CE5F9A10029DC98 /* Products */, 144 | 8B03C1D61CF5DC9B00B09B48 /* Supporting Files */, 145 | ); 146 | sourceTree = ""; 147 | }; 148 | 8B63137C1CE5F9A10029DC98 /* Products */ = { 149 | isa = PBXGroup; 150 | children = ( 151 | 8B63137B1CE5F9A10029DC98 /* MissionControl.framework */, 152 | 8B6313851CE5F9A10029DC98 /* MissionControl iOS Tests.xctest */, 153 | 8B03C1E01CF5E10500B09B48 /* MissionControl.framework */, 154 | 8B03C1EF1CF5E1DD00B09B48 /* MissionControl.framework */, 155 | 8B03C1F81CF5E1DD00B09B48 /* MissionControl tvOS Tests.xctest */, 156 | 8B03C20E1CF5E28C00B09B48 /* MissionControl.framework */, 157 | 8B03C2171CF5E28C00B09B48 /* MissionControl OSX Tests.xctest */, 158 | ); 159 | name = Products; 160 | sourceTree = ""; 161 | }; 162 | 8B63137D1CE5F9A10029DC98 /* Sources */ = { 163 | isa = PBXGroup; 164 | children = ( 165 | 8B6313951CE5FA2B0029DC98 /* MissionControl.swift */, 166 | 8B6313971CE5FA3C0029DC98 /* Supporting Files */, 167 | ); 168 | path = Sources; 169 | sourceTree = ""; 170 | }; 171 | 8B6313891CE5F9A10029DC98 /* Tests */ = { 172 | isa = PBXGroup; 173 | children = ( 174 | 8B63138A1CE5F9A10029DC98 /* MissionControlTests.swift */, 175 | 8B6313981CE5FA440029DC98 /* Supporting Files */, 176 | ); 177 | path = Tests; 178 | sourceTree = ""; 179 | }; 180 | 8B6313971CE5FA3C0029DC98 /* Supporting Files */ = { 181 | isa = PBXGroup; 182 | children = ( 183 | 8B63137E1CE5F9A10029DC98 /* MissionControl.h */, 184 | 8B6313801CE5F9A10029DC98 /* Info.plist */, 185 | ); 186 | name = "Supporting Files"; 187 | sourceTree = ""; 188 | }; 189 | 8B6313981CE5FA440029DC98 /* Supporting Files */ = { 190 | isa = PBXGroup; 191 | children = ( 192 | 8B63138C1CE5F9A10029DC98 /* Info.plist */, 193 | ); 194 | name = "Supporting Files"; 195 | sourceTree = ""; 196 | }; 197 | /* End PBXGroup section */ 198 | 199 | /* Begin PBXHeadersBuildPhase section */ 200 | 8B03C1DD1CF5E10500B09B48 /* Headers */ = { 201 | isa = PBXHeadersBuildPhase; 202 | buildActionMask = 2147483647; 203 | files = ( 204 | 8B03C1E91CF5E18300B09B48 /* MissionControl.h in Headers */, 205 | ); 206 | runOnlyForDeploymentPostprocessing = 0; 207 | }; 208 | 8B03C1EC1CF5E1DD00B09B48 /* Headers */ = { 209 | isa = PBXHeadersBuildPhase; 210 | buildActionMask = 2147483647; 211 | files = ( 212 | 8B03C2071CF5E23200B09B48 /* MissionControl.h in Headers */, 213 | ); 214 | runOnlyForDeploymentPostprocessing = 0; 215 | }; 216 | 8B03C20B1CF5E28C00B09B48 /* Headers */ = { 217 | isa = PBXHeadersBuildPhase; 218 | buildActionMask = 2147483647; 219 | files = ( 220 | 8B03C2261CF5E2C600B09B48 /* MissionControl.h in Headers */, 221 | ); 222 | runOnlyForDeploymentPostprocessing = 0; 223 | }; 224 | 8B6313781CE5F9A10029DC98 /* Headers */ = { 225 | isa = PBXHeadersBuildPhase; 226 | buildActionMask = 2147483647; 227 | files = ( 228 | 8B63137F1CE5F9A10029DC98 /* MissionControl.h in Headers */, 229 | ); 230 | runOnlyForDeploymentPostprocessing = 0; 231 | }; 232 | /* End PBXHeadersBuildPhase section */ 233 | 234 | /* Begin PBXNativeTarget section */ 235 | 8B03C1DF1CF5E10500B09B48 /* MissionControl watchOS */ = { 236 | isa = PBXNativeTarget; 237 | buildConfigurationList = 8B03C1E71CF5E10500B09B48 /* Build configuration list for PBXNativeTarget "MissionControl watchOS" */; 238 | buildPhases = ( 239 | 8B03C1DB1CF5E10500B09B48 /* Sources */, 240 | 8B03C1DC1CF5E10500B09B48 /* Frameworks */, 241 | 8B03C1DD1CF5E10500B09B48 /* Headers */, 242 | 8B03C1DE1CF5E10500B09B48 /* Resources */, 243 | ); 244 | buildRules = ( 245 | ); 246 | dependencies = ( 247 | ); 248 | name = "MissionControl watchOS"; 249 | productName = "MissionControl watchOS"; 250 | productReference = 8B03C1E01CF5E10500B09B48 /* MissionControl.framework */; 251 | productType = "com.apple.product-type.framework"; 252 | }; 253 | 8B03C1EE1CF5E1DD00B09B48 /* MissionControl tvOS */ = { 254 | isa = PBXNativeTarget; 255 | buildConfigurationList = 8B03C2001CF5E1DD00B09B48 /* Build configuration list for PBXNativeTarget "MissionControl tvOS" */; 256 | buildPhases = ( 257 | 8B03C1EA1CF5E1DD00B09B48 /* Sources */, 258 | 8B03C1EB1CF5E1DD00B09B48 /* Frameworks */, 259 | 8B03C1EC1CF5E1DD00B09B48 /* Headers */, 260 | 8B03C1ED1CF5E1DD00B09B48 /* Resources */, 261 | ); 262 | buildRules = ( 263 | ); 264 | dependencies = ( 265 | ); 266 | name = "MissionControl tvOS"; 267 | productName = "MissionControl tvOS"; 268 | productReference = 8B03C1EF1CF5E1DD00B09B48 /* MissionControl.framework */; 269 | productType = "com.apple.product-type.framework"; 270 | }; 271 | 8B03C1F71CF5E1DD00B09B48 /* MissionControl tvOS Tests */ = { 272 | isa = PBXNativeTarget; 273 | buildConfigurationList = 8B03C2031CF5E1DD00B09B48 /* Build configuration list for PBXNativeTarget "MissionControl tvOS Tests" */; 274 | buildPhases = ( 275 | 8B03C1F41CF5E1DD00B09B48 /* Sources */, 276 | 8B03C1F51CF5E1DD00B09B48 /* Frameworks */, 277 | 8B03C1F61CF5E1DD00B09B48 /* Resources */, 278 | ); 279 | buildRules = ( 280 | ); 281 | dependencies = ( 282 | 8B03C1FB1CF5E1DD00B09B48 /* PBXTargetDependency */, 283 | ); 284 | name = "MissionControl tvOS Tests"; 285 | productName = "MissionControl tvOSTests"; 286 | productReference = 8B03C1F81CF5E1DD00B09B48 /* MissionControl tvOS Tests.xctest */; 287 | productType = "com.apple.product-type.bundle.unit-test"; 288 | }; 289 | 8B03C20D1CF5E28C00B09B48 /* MissionControl OSX */ = { 290 | isa = PBXNativeTarget; 291 | buildConfigurationList = 8B03C21F1CF5E28C00B09B48 /* Build configuration list for PBXNativeTarget "MissionControl OSX" */; 292 | buildPhases = ( 293 | 8B03C2091CF5E28C00B09B48 /* Sources */, 294 | 8B03C20A1CF5E28C00B09B48 /* Frameworks */, 295 | 8B03C20B1CF5E28C00B09B48 /* Headers */, 296 | 8B03C20C1CF5E28C00B09B48 /* Resources */, 297 | ); 298 | buildRules = ( 299 | ); 300 | dependencies = ( 301 | ); 302 | name = "MissionControl OSX"; 303 | productName = "MissionControl OSX"; 304 | productReference = 8B03C20E1CF5E28C00B09B48 /* MissionControl.framework */; 305 | productType = "com.apple.product-type.framework"; 306 | }; 307 | 8B03C2161CF5E28C00B09B48 /* MissionControl OSX Tests */ = { 308 | isa = PBXNativeTarget; 309 | buildConfigurationList = 8B03C2221CF5E28C00B09B48 /* Build configuration list for PBXNativeTarget "MissionControl OSX Tests" */; 310 | buildPhases = ( 311 | 8B03C2131CF5E28C00B09B48 /* Sources */, 312 | 8B03C2141CF5E28C00B09B48 /* Frameworks */, 313 | 8B03C2151CF5E28C00B09B48 /* Resources */, 314 | ); 315 | buildRules = ( 316 | ); 317 | dependencies = ( 318 | 8B03C21A1CF5E28C00B09B48 /* PBXTargetDependency */, 319 | ); 320 | name = "MissionControl OSX Tests"; 321 | productName = "MissionControl OSXTests"; 322 | productReference = 8B03C2171CF5E28C00B09B48 /* MissionControl OSX Tests.xctest */; 323 | productType = "com.apple.product-type.bundle.unit-test"; 324 | }; 325 | 8B63137A1CE5F9A10029DC98 /* MissionControl iOS */ = { 326 | isa = PBXNativeTarget; 327 | buildConfigurationList = 8B63138F1CE5F9A10029DC98 /* Build configuration list for PBXNativeTarget "MissionControl iOS" */; 328 | buildPhases = ( 329 | 8B6313761CE5F9A10029DC98 /* Sources */, 330 | 8B6313771CE5F9A10029DC98 /* Frameworks */, 331 | 8B6313781CE5F9A10029DC98 /* Headers */, 332 | 8B6313791CE5F9A10029DC98 /* Resources */, 333 | ); 334 | buildRules = ( 335 | ); 336 | dependencies = ( 337 | ); 338 | name = "MissionControl iOS"; 339 | productName = ACConfig; 340 | productReference = 8B63137B1CE5F9A10029DC98 /* MissionControl.framework */; 341 | productType = "com.apple.product-type.framework"; 342 | }; 343 | 8B6313841CE5F9A10029DC98 /* MissionControl iOS Tests */ = { 344 | isa = PBXNativeTarget; 345 | buildConfigurationList = 8B6313921CE5F9A10029DC98 /* Build configuration list for PBXNativeTarget "MissionControl iOS Tests" */; 346 | buildPhases = ( 347 | 8B6313811CE5F9A10029DC98 /* Sources */, 348 | 8B6313821CE5F9A10029DC98 /* Frameworks */, 349 | 8B6313831CE5F9A10029DC98 /* Resources */, 350 | ); 351 | buildRules = ( 352 | ); 353 | dependencies = ( 354 | 8B6313881CE5F9A10029DC98 /* PBXTargetDependency */, 355 | ); 356 | name = "MissionControl iOS Tests"; 357 | productName = ACConfigTests; 358 | productReference = 8B6313851CE5F9A10029DC98 /* MissionControl iOS Tests.xctest */; 359 | productType = "com.apple.product-type.bundle.unit-test"; 360 | }; 361 | /* End PBXNativeTarget section */ 362 | 363 | /* Begin PBXProject section */ 364 | 8B6313721CE5F9A10029DC98 /* Project object */ = { 365 | isa = PBXProject; 366 | attributes = { 367 | LastSwiftUpdateCheck = 0730; 368 | LastUpgradeCheck = 0730; 369 | ORGANIZATIONNAME = appculture; 370 | TargetAttributes = { 371 | 8B03C1DF1CF5E10500B09B48 = { 372 | CreatedOnToolsVersion = 7.3.1; 373 | }; 374 | 8B03C1EE1CF5E1DD00B09B48 = { 375 | CreatedOnToolsVersion = 7.3.1; 376 | }; 377 | 8B03C1F71CF5E1DD00B09B48 = { 378 | CreatedOnToolsVersion = 7.3.1; 379 | }; 380 | 8B03C20D1CF5E28C00B09B48 = { 381 | CreatedOnToolsVersion = 7.3.1; 382 | }; 383 | 8B03C2161CF5E28C00B09B48 = { 384 | CreatedOnToolsVersion = 7.3.1; 385 | }; 386 | 8B63137A1CE5F9A10029DC98 = { 387 | CreatedOnToolsVersion = 7.3.1; 388 | }; 389 | 8B6313841CE5F9A10029DC98 = { 390 | CreatedOnToolsVersion = 7.3.1; 391 | }; 392 | }; 393 | }; 394 | buildConfigurationList = 8B6313751CE5F9A10029DC98 /* Build configuration list for PBXProject "MissionControl" */; 395 | compatibilityVersion = "Xcode 3.2"; 396 | developmentRegion = English; 397 | hasScannedForEncodings = 0; 398 | knownRegions = ( 399 | en, 400 | ); 401 | mainGroup = 8B6313711CE5F9A10029DC98; 402 | productRefGroup = 8B63137C1CE5F9A10029DC98 /* Products */; 403 | projectDirPath = ""; 404 | projectRoot = ""; 405 | targets = ( 406 | 8B63137A1CE5F9A10029DC98 /* MissionControl iOS */, 407 | 8B6313841CE5F9A10029DC98 /* MissionControl iOS Tests */, 408 | 8B03C1DF1CF5E10500B09B48 /* MissionControl watchOS */, 409 | 8B03C1EE1CF5E1DD00B09B48 /* MissionControl tvOS */, 410 | 8B03C1F71CF5E1DD00B09B48 /* MissionControl tvOS Tests */, 411 | 8B03C20D1CF5E28C00B09B48 /* MissionControl OSX */, 412 | 8B03C2161CF5E28C00B09B48 /* MissionControl OSX Tests */, 413 | ); 414 | }; 415 | /* End PBXProject section */ 416 | 417 | /* Begin PBXResourcesBuildPhase section */ 418 | 8B03C1DE1CF5E10500B09B48 /* Resources */ = { 419 | isa = PBXResourcesBuildPhase; 420 | buildActionMask = 2147483647; 421 | files = ( 422 | ); 423 | runOnlyForDeploymentPostprocessing = 0; 424 | }; 425 | 8B03C1ED1CF5E1DD00B09B48 /* Resources */ = { 426 | isa = PBXResourcesBuildPhase; 427 | buildActionMask = 2147483647; 428 | files = ( 429 | ); 430 | runOnlyForDeploymentPostprocessing = 0; 431 | }; 432 | 8B03C1F61CF5E1DD00B09B48 /* Resources */ = { 433 | isa = PBXResourcesBuildPhase; 434 | buildActionMask = 2147483647; 435 | files = ( 436 | ); 437 | runOnlyForDeploymentPostprocessing = 0; 438 | }; 439 | 8B03C20C1CF5E28C00B09B48 /* Resources */ = { 440 | isa = PBXResourcesBuildPhase; 441 | buildActionMask = 2147483647; 442 | files = ( 443 | ); 444 | runOnlyForDeploymentPostprocessing = 0; 445 | }; 446 | 8B03C2151CF5E28C00B09B48 /* Resources */ = { 447 | isa = PBXResourcesBuildPhase; 448 | buildActionMask = 2147483647; 449 | files = ( 450 | ); 451 | runOnlyForDeploymentPostprocessing = 0; 452 | }; 453 | 8B6313791CE5F9A10029DC98 /* Resources */ = { 454 | isa = PBXResourcesBuildPhase; 455 | buildActionMask = 2147483647; 456 | files = ( 457 | ); 458 | runOnlyForDeploymentPostprocessing = 0; 459 | }; 460 | 8B6313831CE5F9A10029DC98 /* Resources */ = { 461 | isa = PBXResourcesBuildPhase; 462 | buildActionMask = 2147483647; 463 | files = ( 464 | ); 465 | runOnlyForDeploymentPostprocessing = 0; 466 | }; 467 | /* End PBXResourcesBuildPhase section */ 468 | 469 | /* Begin PBXSourcesBuildPhase section */ 470 | 8B03C1DB1CF5E10500B09B48 /* Sources */ = { 471 | isa = PBXSourcesBuildPhase; 472 | buildActionMask = 2147483647; 473 | files = ( 474 | 8B03C1E81CF5E17F00B09B48 /* MissionControl.swift in Sources */, 475 | ); 476 | runOnlyForDeploymentPostprocessing = 0; 477 | }; 478 | 8B03C1EA1CF5E1DD00B09B48 /* Sources */ = { 479 | isa = PBXSourcesBuildPhase; 480 | buildActionMask = 2147483647; 481 | files = ( 482 | 8B03C2061CF5E22E00B09B48 /* MissionControl.swift in Sources */, 483 | ); 484 | runOnlyForDeploymentPostprocessing = 0; 485 | }; 486 | 8B03C1F41CF5E1DD00B09B48 /* Sources */ = { 487 | isa = PBXSourcesBuildPhase; 488 | buildActionMask = 2147483647; 489 | files = ( 490 | 8B03C2081CF5E24500B09B48 /* MissionControlTests.swift in Sources */, 491 | ); 492 | runOnlyForDeploymentPostprocessing = 0; 493 | }; 494 | 8B03C2091CF5E28C00B09B48 /* Sources */ = { 495 | isa = PBXSourcesBuildPhase; 496 | buildActionMask = 2147483647; 497 | files = ( 498 | 8B03C2251CF5E2B800B09B48 /* MissionControl.swift in Sources */, 499 | ); 500 | runOnlyForDeploymentPostprocessing = 0; 501 | }; 502 | 8B03C2131CF5E28C00B09B48 /* Sources */ = { 503 | isa = PBXSourcesBuildPhase; 504 | buildActionMask = 2147483647; 505 | files = ( 506 | 8B03C2271CF5E2D200B09B48 /* MissionControlTests.swift in Sources */, 507 | ); 508 | runOnlyForDeploymentPostprocessing = 0; 509 | }; 510 | 8B6313761CE5F9A10029DC98 /* Sources */ = { 511 | isa = PBXSourcesBuildPhase; 512 | buildActionMask = 2147483647; 513 | files = ( 514 | 8B6313961CE5FA2B0029DC98 /* MissionControl.swift in Sources */, 515 | ); 516 | runOnlyForDeploymentPostprocessing = 0; 517 | }; 518 | 8B6313811CE5F9A10029DC98 /* Sources */ = { 519 | isa = PBXSourcesBuildPhase; 520 | buildActionMask = 2147483647; 521 | files = ( 522 | 8B63138B1CE5F9A10029DC98 /* MissionControlTests.swift in Sources */, 523 | ); 524 | runOnlyForDeploymentPostprocessing = 0; 525 | }; 526 | /* End PBXSourcesBuildPhase section */ 527 | 528 | /* Begin PBXTargetDependency section */ 529 | 8B03C1FB1CF5E1DD00B09B48 /* PBXTargetDependency */ = { 530 | isa = PBXTargetDependency; 531 | target = 8B03C1EE1CF5E1DD00B09B48 /* MissionControl tvOS */; 532 | targetProxy = 8B03C1FA1CF5E1DD00B09B48 /* PBXContainerItemProxy */; 533 | }; 534 | 8B03C21A1CF5E28C00B09B48 /* PBXTargetDependency */ = { 535 | isa = PBXTargetDependency; 536 | target = 8B03C20D1CF5E28C00B09B48 /* MissionControl OSX */; 537 | targetProxy = 8B03C2191CF5E28C00B09B48 /* PBXContainerItemProxy */; 538 | }; 539 | 8B6313881CE5F9A10029DC98 /* PBXTargetDependency */ = { 540 | isa = PBXTargetDependency; 541 | target = 8B63137A1CE5F9A10029DC98 /* MissionControl iOS */; 542 | targetProxy = 8B6313871CE5F9A10029DC98 /* PBXContainerItemProxy */; 543 | }; 544 | /* End PBXTargetDependency section */ 545 | 546 | /* Begin XCBuildConfiguration section */ 547 | 8B03C1E51CF5E10500B09B48 /* Debug */ = { 548 | isa = XCBuildConfiguration; 549 | buildSettings = { 550 | APPLICATION_EXTENSION_API_ONLY = YES; 551 | DEFINES_MODULE = YES; 552 | DYLIB_COMPATIBILITY_VERSION = 1; 553 | DYLIB_CURRENT_VERSION = 1; 554 | DYLIB_INSTALL_NAME_BASE = "@rpath"; 555 | INFOPLIST_FILE = Sources/Info.plist; 556 | INSTALL_PATH = "$(LOCAL_LIBRARY_DIR)/Frameworks"; 557 | LD_RUNPATH_SEARCH_PATHS = "$(inherited) @executable_path/Frameworks @loader_path/Frameworks"; 558 | PRODUCT_BUNDLE_IDENTIFIER = "com.appculture.MissionControl-watchOS"; 559 | PRODUCT_NAME = MissionControl; 560 | SDKROOT = watchos; 561 | SKIP_INSTALL = YES; 562 | TARGETED_DEVICE_FAMILY = 4; 563 | }; 564 | name = Debug; 565 | }; 566 | 8B03C1E61CF5E10500B09B48 /* Release */ = { 567 | isa = XCBuildConfiguration; 568 | buildSettings = { 569 | APPLICATION_EXTENSION_API_ONLY = YES; 570 | DEFINES_MODULE = YES; 571 | DYLIB_COMPATIBILITY_VERSION = 1; 572 | DYLIB_CURRENT_VERSION = 1; 573 | DYLIB_INSTALL_NAME_BASE = "@rpath"; 574 | INFOPLIST_FILE = Sources/Info.plist; 575 | INSTALL_PATH = "$(LOCAL_LIBRARY_DIR)/Frameworks"; 576 | LD_RUNPATH_SEARCH_PATHS = "$(inherited) @executable_path/Frameworks @loader_path/Frameworks"; 577 | PRODUCT_BUNDLE_IDENTIFIER = "com.appculture.MissionControl-watchOS"; 578 | PRODUCT_NAME = MissionControl; 579 | SDKROOT = watchos; 580 | SKIP_INSTALL = YES; 581 | TARGETED_DEVICE_FAMILY = 4; 582 | }; 583 | name = Release; 584 | }; 585 | 8B03C2011CF5E1DD00B09B48 /* Debug */ = { 586 | isa = XCBuildConfiguration; 587 | buildSettings = { 588 | APPLICATION_EXTENSION_API_ONLY = YES; 589 | DEFINES_MODULE = YES; 590 | DYLIB_COMPATIBILITY_VERSION = 1; 591 | DYLIB_CURRENT_VERSION = 1; 592 | DYLIB_INSTALL_NAME_BASE = "@rpath"; 593 | INFOPLIST_FILE = Sources/Info.plist; 594 | INSTALL_PATH = "$(LOCAL_LIBRARY_DIR)/Frameworks"; 595 | LD_RUNPATH_SEARCH_PATHS = "$(inherited) @executable_path/Frameworks @loader_path/Frameworks"; 596 | PRODUCT_BUNDLE_IDENTIFIER = "com.appculture.MissionControl-tvOS"; 597 | PRODUCT_NAME = MissionControl; 598 | SDKROOT = appletvos; 599 | SKIP_INSTALL = YES; 600 | TARGETED_DEVICE_FAMILY = 3; 601 | }; 602 | name = Debug; 603 | }; 604 | 8B03C2021CF5E1DD00B09B48 /* Release */ = { 605 | isa = XCBuildConfiguration; 606 | buildSettings = { 607 | APPLICATION_EXTENSION_API_ONLY = YES; 608 | DEFINES_MODULE = YES; 609 | DYLIB_COMPATIBILITY_VERSION = 1; 610 | DYLIB_CURRENT_VERSION = 1; 611 | DYLIB_INSTALL_NAME_BASE = "@rpath"; 612 | INFOPLIST_FILE = Sources/Info.plist; 613 | INSTALL_PATH = "$(LOCAL_LIBRARY_DIR)/Frameworks"; 614 | LD_RUNPATH_SEARCH_PATHS = "$(inherited) @executable_path/Frameworks @loader_path/Frameworks"; 615 | PRODUCT_BUNDLE_IDENTIFIER = "com.appculture.MissionControl-tvOS"; 616 | PRODUCT_NAME = MissionControl; 617 | SDKROOT = appletvos; 618 | SKIP_INSTALL = YES; 619 | TARGETED_DEVICE_FAMILY = 3; 620 | }; 621 | name = Release; 622 | }; 623 | 8B03C2041CF5E1DD00B09B48 /* Debug */ = { 624 | isa = XCBuildConfiguration; 625 | buildSettings = { 626 | INFOPLIST_FILE = Tests/Info.plist; 627 | LD_RUNPATH_SEARCH_PATHS = "$(inherited) @executable_path/Frameworks @loader_path/Frameworks"; 628 | PRODUCT_BUNDLE_IDENTIFIER = "com.appculture.MissionControl-tvOSTests"; 629 | PRODUCT_NAME = "$(TARGET_NAME)"; 630 | SDKROOT = appletvos; 631 | TVOS_DEPLOYMENT_TARGET = 9.2; 632 | }; 633 | name = Debug; 634 | }; 635 | 8B03C2051CF5E1DD00B09B48 /* Release */ = { 636 | isa = XCBuildConfiguration; 637 | buildSettings = { 638 | INFOPLIST_FILE = Tests/Info.plist; 639 | LD_RUNPATH_SEARCH_PATHS = "$(inherited) @executable_path/Frameworks @loader_path/Frameworks"; 640 | PRODUCT_BUNDLE_IDENTIFIER = "com.appculture.MissionControl-tvOSTests"; 641 | PRODUCT_NAME = "$(TARGET_NAME)"; 642 | SDKROOT = appletvos; 643 | TVOS_DEPLOYMENT_TARGET = 9.2; 644 | }; 645 | name = Release; 646 | }; 647 | 8B03C2201CF5E28C00B09B48 /* Debug */ = { 648 | isa = XCBuildConfiguration; 649 | buildSettings = { 650 | APPLICATION_EXTENSION_API_ONLY = YES; 651 | CODE_SIGN_IDENTITY = "-"; 652 | COMBINE_HIDPI_IMAGES = YES; 653 | DEFINES_MODULE = YES; 654 | DYLIB_COMPATIBILITY_VERSION = 1; 655 | DYLIB_CURRENT_VERSION = 1; 656 | DYLIB_INSTALL_NAME_BASE = "@rpath"; 657 | FRAMEWORK_VERSION = A; 658 | INFOPLIST_FILE = Sources/Info.plist; 659 | INSTALL_PATH = "$(LOCAL_LIBRARY_DIR)/Frameworks"; 660 | LD_RUNPATH_SEARCH_PATHS = "$(inherited) @executable_path/../Frameworks @loader_path/Frameworks"; 661 | MACOSX_DEPLOYMENT_TARGET = 10.10; 662 | PRODUCT_BUNDLE_IDENTIFIER = "com.appculture.MissionControl-OSX"; 663 | PRODUCT_NAME = MissionControl; 664 | SDKROOT = macosx; 665 | SKIP_INSTALL = YES; 666 | }; 667 | name = Debug; 668 | }; 669 | 8B03C2211CF5E28C00B09B48 /* Release */ = { 670 | isa = XCBuildConfiguration; 671 | buildSettings = { 672 | APPLICATION_EXTENSION_API_ONLY = YES; 673 | CODE_SIGN_IDENTITY = "-"; 674 | COMBINE_HIDPI_IMAGES = YES; 675 | DEFINES_MODULE = YES; 676 | DYLIB_COMPATIBILITY_VERSION = 1; 677 | DYLIB_CURRENT_VERSION = 1; 678 | DYLIB_INSTALL_NAME_BASE = "@rpath"; 679 | FRAMEWORK_VERSION = A; 680 | INFOPLIST_FILE = Sources/Info.plist; 681 | INSTALL_PATH = "$(LOCAL_LIBRARY_DIR)/Frameworks"; 682 | LD_RUNPATH_SEARCH_PATHS = "$(inherited) @executable_path/../Frameworks @loader_path/Frameworks"; 683 | MACOSX_DEPLOYMENT_TARGET = 10.10; 684 | PRODUCT_BUNDLE_IDENTIFIER = "com.appculture.MissionControl-OSX"; 685 | PRODUCT_NAME = MissionControl; 686 | SDKROOT = macosx; 687 | SKIP_INSTALL = YES; 688 | }; 689 | name = Release; 690 | }; 691 | 8B03C2231CF5E28C00B09B48 /* Debug */ = { 692 | isa = XCBuildConfiguration; 693 | buildSettings = { 694 | CODE_SIGN_IDENTITY = "-"; 695 | COMBINE_HIDPI_IMAGES = YES; 696 | INFOPLIST_FILE = Tests/Info.plist; 697 | LD_RUNPATH_SEARCH_PATHS = "$(inherited) @executable_path/../Frameworks @loader_path/../Frameworks"; 698 | MACOSX_DEPLOYMENT_TARGET = 10.11; 699 | PRODUCT_BUNDLE_IDENTIFIER = "com.appculture.MissionControl-OSXTests"; 700 | PRODUCT_NAME = "$(TARGET_NAME)"; 701 | SDKROOT = macosx; 702 | }; 703 | name = Debug; 704 | }; 705 | 8B03C2241CF5E28C00B09B48 /* Release */ = { 706 | isa = XCBuildConfiguration; 707 | buildSettings = { 708 | CODE_SIGN_IDENTITY = "-"; 709 | COMBINE_HIDPI_IMAGES = YES; 710 | INFOPLIST_FILE = Tests/Info.plist; 711 | LD_RUNPATH_SEARCH_PATHS = "$(inherited) @executable_path/../Frameworks @loader_path/../Frameworks"; 712 | MACOSX_DEPLOYMENT_TARGET = 10.11; 713 | PRODUCT_BUNDLE_IDENTIFIER = "com.appculture.MissionControl-OSXTests"; 714 | PRODUCT_NAME = "$(TARGET_NAME)"; 715 | SDKROOT = macosx; 716 | }; 717 | name = Release; 718 | }; 719 | 8B63138D1CE5F9A10029DC98 /* Debug */ = { 720 | isa = XCBuildConfiguration; 721 | buildSettings = { 722 | ALWAYS_SEARCH_USER_PATHS = NO; 723 | CLANG_ANALYZER_NONNULL = YES; 724 | CLANG_CXX_LANGUAGE_STANDARD = "gnu++0x"; 725 | CLANG_CXX_LIBRARY = "libc++"; 726 | CLANG_ENABLE_MODULES = YES; 727 | CLANG_ENABLE_OBJC_ARC = YES; 728 | CLANG_WARN_BOOL_CONVERSION = YES; 729 | CLANG_WARN_CONSTANT_CONVERSION = YES; 730 | CLANG_WARN_DIRECT_OBJC_ISA_USAGE = YES_ERROR; 731 | CLANG_WARN_EMPTY_BODY = YES; 732 | CLANG_WARN_ENUM_CONVERSION = YES; 733 | CLANG_WARN_INT_CONVERSION = YES; 734 | CLANG_WARN_OBJC_ROOT_CLASS = YES_ERROR; 735 | CLANG_WARN_UNREACHABLE_CODE = YES; 736 | CLANG_WARN__DUPLICATE_METHOD_MATCH = YES; 737 | "CODE_SIGN_IDENTITY[sdk=iphoneos*]" = "iPhone Developer"; 738 | COPY_PHASE_STRIP = NO; 739 | CURRENT_PROJECT_VERSION = 1; 740 | DEBUG_INFORMATION_FORMAT = dwarf; 741 | ENABLE_STRICT_OBJC_MSGSEND = YES; 742 | ENABLE_TESTABILITY = YES; 743 | GCC_C_LANGUAGE_STANDARD = gnu99; 744 | GCC_DYNAMIC_NO_PIC = NO; 745 | GCC_NO_COMMON_BLOCKS = YES; 746 | GCC_OPTIMIZATION_LEVEL = 0; 747 | GCC_PREPROCESSOR_DEFINITIONS = ( 748 | "DEBUG=1", 749 | "$(inherited)", 750 | ); 751 | GCC_WARN_64_TO_32_BIT_CONVERSION = YES; 752 | GCC_WARN_ABOUT_RETURN_TYPE = YES_ERROR; 753 | GCC_WARN_UNDECLARED_SELECTOR = YES; 754 | GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE; 755 | GCC_WARN_UNUSED_FUNCTION = YES; 756 | GCC_WARN_UNUSED_VARIABLE = YES; 757 | IPHONEOS_DEPLOYMENT_TARGET = 8.0; 758 | MTL_ENABLE_DEBUG_INFO = YES; 759 | ONLY_ACTIVE_ARCH = YES; 760 | SDKROOT = iphoneos; 761 | SWIFT_OPTIMIZATION_LEVEL = "-Onone"; 762 | TARGETED_DEVICE_FAMILY = "1,2"; 763 | TVOS_DEPLOYMENT_TARGET = 9.0; 764 | VERSIONING_SYSTEM = "apple-generic"; 765 | VERSION_INFO_PREFIX = ""; 766 | WATCHOS_DEPLOYMENT_TARGET = 2.0; 767 | }; 768 | name = Debug; 769 | }; 770 | 8B63138E1CE5F9A10029DC98 /* Release */ = { 771 | isa = XCBuildConfiguration; 772 | buildSettings = { 773 | ALWAYS_SEARCH_USER_PATHS = NO; 774 | CLANG_ANALYZER_NONNULL = YES; 775 | CLANG_CXX_LANGUAGE_STANDARD = "gnu++0x"; 776 | CLANG_CXX_LIBRARY = "libc++"; 777 | CLANG_ENABLE_MODULES = YES; 778 | CLANG_ENABLE_OBJC_ARC = YES; 779 | CLANG_WARN_BOOL_CONVERSION = YES; 780 | CLANG_WARN_CONSTANT_CONVERSION = YES; 781 | CLANG_WARN_DIRECT_OBJC_ISA_USAGE = YES_ERROR; 782 | CLANG_WARN_EMPTY_BODY = YES; 783 | CLANG_WARN_ENUM_CONVERSION = YES; 784 | CLANG_WARN_INT_CONVERSION = YES; 785 | CLANG_WARN_OBJC_ROOT_CLASS = YES_ERROR; 786 | CLANG_WARN_UNREACHABLE_CODE = YES; 787 | CLANG_WARN__DUPLICATE_METHOD_MATCH = YES; 788 | "CODE_SIGN_IDENTITY[sdk=iphoneos*]" = "iPhone Developer"; 789 | COPY_PHASE_STRIP = NO; 790 | CURRENT_PROJECT_VERSION = 1; 791 | DEBUG_INFORMATION_FORMAT = "dwarf-with-dsym"; 792 | ENABLE_NS_ASSERTIONS = NO; 793 | ENABLE_STRICT_OBJC_MSGSEND = YES; 794 | GCC_C_LANGUAGE_STANDARD = gnu99; 795 | GCC_NO_COMMON_BLOCKS = YES; 796 | GCC_WARN_64_TO_32_BIT_CONVERSION = YES; 797 | GCC_WARN_ABOUT_RETURN_TYPE = YES_ERROR; 798 | GCC_WARN_UNDECLARED_SELECTOR = YES; 799 | GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE; 800 | GCC_WARN_UNUSED_FUNCTION = YES; 801 | GCC_WARN_UNUSED_VARIABLE = YES; 802 | IPHONEOS_DEPLOYMENT_TARGET = 8.0; 803 | MTL_ENABLE_DEBUG_INFO = NO; 804 | SDKROOT = iphoneos; 805 | TARGETED_DEVICE_FAMILY = "1,2"; 806 | TVOS_DEPLOYMENT_TARGET = 9.0; 807 | VALIDATE_PRODUCT = YES; 808 | VERSIONING_SYSTEM = "apple-generic"; 809 | VERSION_INFO_PREFIX = ""; 810 | WATCHOS_DEPLOYMENT_TARGET = 2.0; 811 | }; 812 | name = Release; 813 | }; 814 | 8B6313901CE5F9A10029DC98 /* Debug */ = { 815 | isa = XCBuildConfiguration; 816 | buildSettings = { 817 | APPLICATION_EXTENSION_API_ONLY = YES; 818 | CLANG_ENABLE_MODULES = YES; 819 | DEFINES_MODULE = YES; 820 | DYLIB_COMPATIBILITY_VERSION = 1; 821 | DYLIB_CURRENT_VERSION = 1; 822 | DYLIB_INSTALL_NAME_BASE = "@rpath"; 823 | INFOPLIST_FILE = Sources/Info.plist; 824 | INSTALL_PATH = "$(LOCAL_LIBRARY_DIR)/Frameworks"; 825 | LD_RUNPATH_SEARCH_PATHS = "$(inherited) @executable_path/Frameworks @loader_path/Frameworks"; 826 | PRODUCT_BUNDLE_IDENTIFIER = "com.appculture.MissionControl-iOSTests"; 827 | PRODUCT_NAME = MissionControl; 828 | SKIP_INSTALL = YES; 829 | SWIFT_OPTIMIZATION_LEVEL = "-Onone"; 830 | }; 831 | name = Debug; 832 | }; 833 | 8B6313911CE5F9A10029DC98 /* Release */ = { 834 | isa = XCBuildConfiguration; 835 | buildSettings = { 836 | APPLICATION_EXTENSION_API_ONLY = YES; 837 | CLANG_ENABLE_MODULES = YES; 838 | DEFINES_MODULE = YES; 839 | DYLIB_COMPATIBILITY_VERSION = 1; 840 | DYLIB_CURRENT_VERSION = 1; 841 | DYLIB_INSTALL_NAME_BASE = "@rpath"; 842 | INFOPLIST_FILE = Sources/Info.plist; 843 | INSTALL_PATH = "$(LOCAL_LIBRARY_DIR)/Frameworks"; 844 | LD_RUNPATH_SEARCH_PATHS = "$(inherited) @executable_path/Frameworks @loader_path/Frameworks"; 845 | PRODUCT_BUNDLE_IDENTIFIER = "com.appculture.MissionControl-iOSTests"; 846 | PRODUCT_NAME = MissionControl; 847 | SKIP_INSTALL = YES; 848 | }; 849 | name = Release; 850 | }; 851 | 8B6313931CE5F9A10029DC98 /* Debug */ = { 852 | isa = XCBuildConfiguration; 853 | buildSettings = { 854 | INFOPLIST_FILE = Tests/Info.plist; 855 | LD_RUNPATH_SEARCH_PATHS = "$(inherited) @executable_path/Frameworks @loader_path/Frameworks"; 856 | PRODUCT_BUNDLE_IDENTIFIER = com.appculture.MissionControlTests; 857 | PRODUCT_NAME = "$(TARGET_NAME)"; 858 | }; 859 | name = Debug; 860 | }; 861 | 8B6313941CE5F9A10029DC98 /* Release */ = { 862 | isa = XCBuildConfiguration; 863 | buildSettings = { 864 | INFOPLIST_FILE = Tests/Info.plist; 865 | LD_RUNPATH_SEARCH_PATHS = "$(inherited) @executable_path/Frameworks @loader_path/Frameworks"; 866 | PRODUCT_BUNDLE_IDENTIFIER = com.appculture.MissionControlTests; 867 | PRODUCT_NAME = "$(TARGET_NAME)"; 868 | }; 869 | name = Release; 870 | }; 871 | /* End XCBuildConfiguration section */ 872 | 873 | /* Begin XCConfigurationList section */ 874 | 8B03C1E71CF5E10500B09B48 /* Build configuration list for PBXNativeTarget "MissionControl watchOS" */ = { 875 | isa = XCConfigurationList; 876 | buildConfigurations = ( 877 | 8B03C1E51CF5E10500B09B48 /* Debug */, 878 | 8B03C1E61CF5E10500B09B48 /* Release */, 879 | ); 880 | defaultConfigurationIsVisible = 0; 881 | }; 882 | 8B03C2001CF5E1DD00B09B48 /* Build configuration list for PBXNativeTarget "MissionControl tvOS" */ = { 883 | isa = XCConfigurationList; 884 | buildConfigurations = ( 885 | 8B03C2011CF5E1DD00B09B48 /* Debug */, 886 | 8B03C2021CF5E1DD00B09B48 /* Release */, 887 | ); 888 | defaultConfigurationIsVisible = 0; 889 | }; 890 | 8B03C2031CF5E1DD00B09B48 /* Build configuration list for PBXNativeTarget "MissionControl tvOS Tests" */ = { 891 | isa = XCConfigurationList; 892 | buildConfigurations = ( 893 | 8B03C2041CF5E1DD00B09B48 /* Debug */, 894 | 8B03C2051CF5E1DD00B09B48 /* Release */, 895 | ); 896 | defaultConfigurationIsVisible = 0; 897 | }; 898 | 8B03C21F1CF5E28C00B09B48 /* Build configuration list for PBXNativeTarget "MissionControl OSX" */ = { 899 | isa = XCConfigurationList; 900 | buildConfigurations = ( 901 | 8B03C2201CF5E28C00B09B48 /* Debug */, 902 | 8B03C2211CF5E28C00B09B48 /* Release */, 903 | ); 904 | defaultConfigurationIsVisible = 0; 905 | }; 906 | 8B03C2221CF5E28C00B09B48 /* Build configuration list for PBXNativeTarget "MissionControl OSX Tests" */ = { 907 | isa = XCConfigurationList; 908 | buildConfigurations = ( 909 | 8B03C2231CF5E28C00B09B48 /* Debug */, 910 | 8B03C2241CF5E28C00B09B48 /* Release */, 911 | ); 912 | defaultConfigurationIsVisible = 0; 913 | }; 914 | 8B6313751CE5F9A10029DC98 /* Build configuration list for PBXProject "MissionControl" */ = { 915 | isa = XCConfigurationList; 916 | buildConfigurations = ( 917 | 8B63138D1CE5F9A10029DC98 /* Debug */, 918 | 8B63138E1CE5F9A10029DC98 /* Release */, 919 | ); 920 | defaultConfigurationIsVisible = 0; 921 | defaultConfigurationName = Release; 922 | }; 923 | 8B63138F1CE5F9A10029DC98 /* Build configuration list for PBXNativeTarget "MissionControl iOS" */ = { 924 | isa = XCConfigurationList; 925 | buildConfigurations = ( 926 | 8B6313901CE5F9A10029DC98 /* Debug */, 927 | 8B6313911CE5F9A10029DC98 /* Release */, 928 | ); 929 | defaultConfigurationIsVisible = 0; 930 | defaultConfigurationName = Release; 931 | }; 932 | 8B6313921CE5F9A10029DC98 /* Build configuration list for PBXNativeTarget "MissionControl iOS Tests" */ = { 933 | isa = XCConfigurationList; 934 | buildConfigurations = ( 935 | 8B6313931CE5F9A10029DC98 /* Debug */, 936 | 8B6313941CE5F9A10029DC98 /* Release */, 937 | ); 938 | defaultConfigurationIsVisible = 0; 939 | defaultConfigurationName = Release; 940 | }; 941 | /* End XCConfigurationList section */ 942 | }; 943 | rootObject = 8B6313721CE5F9A10029DC98 /* Project object */; 944 | } 945 | -------------------------------------------------------------------------------- /MissionControl.xcodeproj/project.xcworkspace/contents.xcworkspacedata: -------------------------------------------------------------------------------- 1 | 2 | 4 | 6 | 7 | 8 | -------------------------------------------------------------------------------- /MissionControl.xcodeproj/xcshareddata/xcschemes/MissionControl OSX.xcscheme: -------------------------------------------------------------------------------- 1 | 2 | 5 | 8 | 9 | 15 | 21 | 22 | 23 | 24 | 25 | 30 | 31 | 33 | 39 | 40 | 41 | 42 | 43 | 49 | 50 | 51 | 52 | 53 | 54 | 64 | 65 | 71 | 72 | 73 | 74 | 75 | 76 | 82 | 83 | 89 | 90 | 91 | 92 | 94 | 95 | 98 | 99 | 100 | -------------------------------------------------------------------------------- /MissionControl.xcodeproj/xcshareddata/xcschemes/MissionControl iOS.xcscheme: -------------------------------------------------------------------------------- 1 | 2 | 5 | 8 | 9 | 15 | 21 | 22 | 23 | 24 | 25 | 31 | 32 | 34 | 40 | 41 | 42 | 43 | 44 | 50 | 51 | 52 | 53 | 54 | 55 | 65 | 66 | 72 | 73 | 74 | 75 | 76 | 77 | 83 | 84 | 90 | 91 | 92 | 93 | 95 | 96 | 99 | 100 | 101 | -------------------------------------------------------------------------------- /MissionControl.xcodeproj/xcshareddata/xcschemes/MissionControl tvOS.xcscheme: -------------------------------------------------------------------------------- 1 | 2 | 5 | 8 | 9 | 15 | 21 | 22 | 23 | 24 | 25 | 30 | 31 | 33 | 39 | 40 | 41 | 42 | 43 | 49 | 50 | 51 | 52 | 53 | 54 | 64 | 65 | 71 | 72 | 73 | 74 | 75 | 76 | 82 | 83 | 89 | 90 | 91 | 92 | 94 | 95 | 98 | 99 | 100 | -------------------------------------------------------------------------------- /MissionControl.xcodeproj/xcshareddata/xcschemes/MissionControl watchOS.xcscheme: -------------------------------------------------------------------------------- 1 | 2 | 5 | 8 | 9 | 15 | 21 | 22 | 23 | 24 | 25 | 30 | 31 | 32 | 33 | 34 | 35 | 45 | 46 | 52 | 53 | 54 | 55 | 56 | 57 | 63 | 64 | 70 | 71 | 72 | 73 | 75 | 76 | 79 | 80 | 81 | -------------------------------------------------------------------------------- /Package.swift: -------------------------------------------------------------------------------- 1 | // 2 | // Package.swift 3 | // 4 | // Copyright (c) 2016 appculture http://appculture.com 5 | // 6 | // Permission is hereby granted, free of charge, to any person obtaining a copy 7 | // of this software and associated documentation files (the "Software"), to deal 8 | // in the Software without restriction, including without limitation the rights 9 | // to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 10 | // copies of the Software, and to permit persons to whom the Software is 11 | // furnished to do so, subject to the following conditions: 12 | // 13 | // The above copyright notice and this permission notice shall be included in all 14 | // copies or substantial portions of the Software. 15 | // 16 | // THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 17 | // IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 18 | // FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 19 | // AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 20 | // LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 21 | // OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 22 | // SOFTWARE. 23 | // 24 | 25 | import PackageDescription 26 | 27 | let package = Package( 28 | name: "MissionControl" 29 | ) -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Mission Control 2 | **Super powerful remote config utility written in Swift (iOS, watchOS, tvOS, OSX)** 3 | 4 | [![Language Swift 2.2](https://img.shields.io/badge/Language-Swift%202.2-orange.svg?style=flat)](https://swift.org) 5 | [![Platforms iOS | watchOS | tvOS | OSX](https://img.shields.io/badge/Platforms-iOS%20%7C%20watchOS%20%7C%20tvOS%20%7C%20OS%20X-lightgray.svg?style=flat)](http://www.apple.com) 6 | [![License MIT](https://img.shields.io/badge/License-MIT-lightgrey.svg?style=flat)](https://github.com/appculture/MissionControl-iOS/blob/master/LICENSE) 7 | 8 | [![CocoaPods Version](https://img.shields.io/cocoapods/v/MissionControl.svg?style=flat)](https://cocoapods.org/pods/MissionControl) 9 | [![Carthage compatible](https://img.shields.io/badge/Carthage-compatible-brightgreen.svg?style=flat)](https://github.com/Carthage/Carthage) 10 | [![Swift Package Manager compatible](https://img.shields.io/badge/Swift%20Package%20Manager-compatible-brightgreen.svg)](https://github.com/apple/swift-package-manager) 11 | 12 | > **Brought to you by** 13 | > 14 | > 15 | 16 | > Have you ever wished you could change some config parameter for your app without deploying a new version? Of course you have! Wouldn't it be great if you had whole config for your app in the cloud and change it as you see fit? Of course it would! Well, go ahead, just put some config somewhere in the cloud and **MissionControl** will take care of the rest for you. 17 | 18 | ## Index 19 | - [Features](#features) 20 | - [Usage](#usage) 21 | - [Initial Configuration](#initial-configuration) 22 | - [**Phase 1** - No Config](#phase-1---no-config) 23 | - [**Phase 2** - Local Config](#phase-2---local-config) 24 | - [**Phase 3** - Remote Config](#phase-3---remote-config) 25 | - [Force Load Remote Setting](#force-load-remote-setting) 26 | - [Listen for changes](#listen-for-changes) 27 | - [Demo](#demo) 28 | - [Requirements](#requirements) 29 | - [Installation](#installation) 30 | - [License](#license) 31 | 32 | ## Features 33 | - Easily take [advantages of using remote (cloud) config](https://library.launchkit.io/every-app-developer-should-move-their-config-to-the-cloud-here-s-why-1efedc8f893f#.3tumo5yfg) for your app 34 | - Simple and flexible API let's you gradually move from no config, via local config to remote config 35 | - Automatic caching of the latest remote settings for offline usage (fail-safe) 36 | - Force load remote setting when you really need the latest config (like NOW) 37 | - Covered with **unit tests** 38 | - Covered with [docs](http://cocoadocs.org/docsets/MissionControl) 39 | 40 | ## Usage 41 | 42 | ### Initial Configuration 43 | 44 | ```swift 45 | /// You should just launch shared instance of MissionControl on your app start. 46 | /// Good place to do this is in your's AppDelegate's didFinishLaunchingWithOptions: 47 | 48 | MissionControl.launch() 49 | ``` 50 | 51 | ### Phase 1 - No Config 52 | 53 | ```swift 54 | /// If you're starting from scratch, you could just start using MissionControl right away. 55 | /// 56 | /// For anything that you find "configurable" (colors, fonts, alignment, values etc.), 57 | /// instead of just hard-coding it, use helper accessors with setting key and fallback value. 58 | /// 59 | /// Here are some examples: 60 | 61 | let ready = ConfigBool("Ready", fallback: false) 62 | let numberOfSeconds = ConfigInt("CountdownDuration", fallback: 10) 63 | let launchForce = ConfigDouble("LaunchForce", fallback: 0.5) 64 | let color = ConfigString("ReadyColor", fallback: "#7ED321") 65 | ``` 66 | 67 | ### Phase 2 - Local Config 68 | 69 | ```swift 70 | /// After some time, this adds up and you're probably ready to create some local config. 71 | /// You should just define dictionary with setting keys and values and pass it on launch. 72 | /// 73 | /// These settings will override whatever you put before in fallback value of accessors. 74 | /// It doesn't need to contain all the stuff, the rest will just continue to use fallback values. 75 | 76 | let config: [String : AnyObject] = [ 77 | "Ready" : true, 78 | "LaunchForce" : 0.21 79 | ] 80 | 81 | MissionControl.launch(localConfig: config) 82 | ``` 83 | 84 | ### Phase 3 - Remote Config 85 | 86 | ```swift 87 | /// After some time, you decide to have more influence on these settings, 88 | /// Yes, even if the app is already deployed. We get it! 89 | /// 90 | /// But, you should create that backend part yourself (sorry). 91 | /// Just make sure that you return JSON formatted key-value dictionary in response body. 92 | /// Then, all you need to do is pass your's backend URL on launch. 93 | /// 94 | /// After the first refresh (done automatically on launch) remote settings will be cached to disk. 95 | /// These remote settings will override whatever you put in local config dictionary. 96 | /// 97 | /// All helper accessors will respect these priority levels: 98 | /// 1. Remote setting from memory (received in the most recent refresh). 99 | /// 2. Remote setting from disk cache (if never refreshed in current app session (ex. offline)). 100 | /// 3. Local setting from disk (defaults provided in `localConfig` on `launch`). 101 | /// 4. Inline provided fallback value 102 | 103 | let remoteURL = NSURL(string: "http://appculture.com/mission-control")! 104 | MissionControl.launch(localConfig: config, remoteConfigURL: remoteURL) 105 | ``` 106 | 107 | ### Force Load Remote Setting 108 | 109 | ```swift 110 | /// If you need, you can always call `refresh` manually to get the latest settings. 111 | /// Good place to call this is in your AppDelegate's applicationWillEnterForeground: or applicationDidBecomeActive: 112 | 113 | MissionControl.refresh() 114 | 115 | /// There are also "async force remote" helper accessors which you can use 116 | /// when it's really important to have the latest setting or abort everything. 117 | 118 | ConfigBoolForce("Abort", fallback: true) { (forced) in 119 | if forced { 120 | self.stopCountdown() 121 | self.state = .Aborted 122 | } 123 | } 124 | ``` 125 | 126 | ### Listen for changes 127 | 128 | ```swift 129 | /// MissionControl can inform you whenever remote config is refreshed or failed to do so. 130 | /// You can observe for these notifications, or become a MissionControl's delegate, whatever you prefer. 131 | 132 | // MARK: - Notifications 133 | 134 | let center = NSNotificationCenter.defaultCenter() 135 | center.addObserver(self, selector: #selector(handleRefresh(_:)), 136 | name: MissionControl.Notification.DidRefreshConfig, object: nil) 137 | center.addObserver(self, selector: #selector(handleFail(_:)), 138 | name: MissionControl.Notification.DidFailRefreshingConfig, object: nil) 139 | 140 | // MARK: - MissionControlDelegate 141 | 142 | MissionControl.delegate = self 143 | 144 | func missionControlDidRefreshConfig(old old: [String : AnyObject]?, new: [String : AnyObject]) { 145 | /// do whatever you need to do 146 | } 147 | 148 | func missionControlDidFailRefreshingConfig(error error: ErrorType) { 149 | /// ignore or not, it's up to you 150 | } 151 | ``` 152 | 153 | ## Demo 154 | 155 | Be sure to check out our example demo project from this repo. 156 | It's kind of a "**Rocket Launcher**" which doesn't really launch rockets, 157 | but it demonstrates power of using **MissionControl**. 158 | 159 | 160 | 161 | 162 | 163 | Let me explain: 164 | 165 | 1. First screen is initial "**Offline**" state in which you need to "**Connect**" to the base (remote config). 166 | 2. When you press "**Connect**" button it will **force load** remote config asking for the "**Ready**" Bool flag. 167 | 3. If remote config returns that **"Ready" = true** it will go to the **Launch** screen, otherwise **Failure** screen. 168 | 4. From the **Launch** screen you can initiate the **Countdown**. Number of seconds is also provided via remote config. 169 | 5. During the **Countdown**, on each second, app checks if launch should be aborted by force loading **"Abort"** Bool flag from remote. Yes, you can **abort the launch remotely** from MissionControl. 170 | 6. After Countdown is finished, you can see some nice animation and **that's all folks**. 171 | 172 | **P.S.** Some colors and other values were also provided via remote config. 173 | Here's what settings are used in this demo, so you can try to abort launch from your server. 174 | Just remember to pass your URL to **MissionControl** `launch:` method. 175 | 176 | ```json 177 | { 178 | "TopColor": "#000000", 179 | "BottomColor": "#4A90E2", 180 | "Ready": true, 181 | "CountdownDuration": 10, 182 | "Abort": false, 183 | "LaunchForce": 0.5, 184 | "OfflineColor": "#F8E71C", 185 | "ReadyColor": "#7ED321", 186 | "CountdownColor": "#F5A623", 187 | "LaunchedColor": "#BD10E0", 188 | "FailedColor": "#D0021B", 189 | "AbortedColor": "#D0021B" 190 | } 191 | ``` 192 | 193 | ### So, are you ready for the "Real Time" apps?! [We are](http://appculture.com). 194 | 195 | ## Requirements 196 | - Xcode 7.3+ 197 | - iOS 8.0+ 198 | 199 | ## Installation 200 | 201 | - Using [CocoaPods](http://cocoapods.org/): 202 | 203 | ```ruby 204 | pod 'MissionControl' 205 | ``` 206 | 207 | - [Carthage](https://github.com/Carthage/Carthage): 208 | 209 | ```ogdl 210 | github "appculture/MissionControl-iOS" 211 | ``` 212 | 213 | - Manually: 214 | 215 | Just drag **MissionControl.swift** into your project and start using it. 216 | 217 | ## License 218 | MissionControl is released under the MIT license. See [LICENSE](LICENSE) for details. 219 | -------------------------------------------------------------------------------- /Sources/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.0 19 | CFBundleSignature 20 | ???? 21 | CFBundleVersion 22 | $(CURRENT_PROJECT_VERSION) 23 | NSPrincipalClass 24 | 25 | 26 | 27 | -------------------------------------------------------------------------------- /Sources/MissionControl.h: -------------------------------------------------------------------------------- 1 | // 2 | // MissionControl.h 3 | // 4 | // Copyright (c) 2016 appculture http://appculture.com 5 | // 6 | // Permission is hereby granted, free of charge, to any person obtaining a copy 7 | // of this software and associated documentation files (the "Software"), to deal 8 | // in the Software without restriction, including without limitation the rights 9 | // to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 10 | // copies of the Software, and to permit persons to whom the Software is 11 | // furnished to do so, subject to the following conditions: 12 | // 13 | // The above copyright notice and this permission notice shall be included in all 14 | // copies or substantial portions of the Software. 15 | // 16 | // THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 17 | // IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 18 | // FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 19 | // AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 20 | // LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 21 | // OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 22 | // SOFTWARE. 23 | // 24 | 25 | #import 26 | 27 | FOUNDATION_EXPORT double MissionControlVersionNumber; 28 | FOUNDATION_EXPORT const unsigned char MissionControlVersionString[]; -------------------------------------------------------------------------------- /Sources/MissionControl.swift: -------------------------------------------------------------------------------- 1 | // 2 | // MissionControl.swift 3 | // 4 | // Copyright (c) 2016 appculture http://appculture.com 5 | // 6 | // Permission is hereby granted, free of charge, to any person obtaining a copy 7 | // of this software and associated documentation files (the "Software"), to deal 8 | // in the Software without restriction, including without limitation the rights 9 | // to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 10 | // copies of the Software, and to permit persons to whom the Software is 11 | // furnished to do so, subject to the following conditions: 12 | // 13 | // The above copyright notice and this permission notice shall be included in all 14 | // copies or substantial portions of the Software. 15 | // 16 | // THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 17 | // IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 18 | // FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 19 | // AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 20 | // LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 21 | // OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 22 | // SOFTWARE. 23 | // 24 | 25 | import Foundation 26 | 27 | // MARK: - MissionControl 28 | 29 | /// Facade class for using MissionControl. 30 | public class MissionControl { 31 | 32 | // MARK: Types 33 | 34 | /// Errors types which can be throwed when refreshing local config from remote. 35 | public enum Error: ErrorType { 36 | /// Property `remoteConfigURL` is not set on launch. 37 | case NoRemoteURL 38 | /// Server returned response code other then 200 OK. 39 | case BadResponseCode 40 | /// Server returned data with invalid format. 41 | case InvalidData 42 | } 43 | 44 | /// Constants for keys of sent NSNotification objects. 45 | public struct Notification { 46 | /// This notification is sent each time when config is refreshed from remote. 47 | public static let DidRefreshConfig = "MissionControl.DidRefreshConfig" 48 | /// This notification is sent when refreshing config from remote fails. 49 | public static let DidFailRefreshingConfig = "MissionControl.DidFailRefreshingConfig" 50 | 51 | /// Constants for keys of `userInfo` dictionary inside sent `ConfigRefreshed` NSNotification objects. 52 | public struct UserInfo { 53 | /// Previous value of `config` property (before refreshing config from remote) 54 | public static let OldConfigKey = "MissionControl.OldConfig" 55 | /// Current value of `config` property (after refreshing config from remote) 56 | public static let NewConfigKey = "MissionControl.NewConfig" 57 | } 58 | } 59 | 60 | // MARK: Properties 61 | 62 | /// Delegate for Mission Control. 63 | public class var delegate: MissionControlDelegate? { 64 | get { return ACMissionControl.sharedInstance.delegate } 65 | set { ACMissionControl.sharedInstance.delegate = newValue } 66 | } 67 | 68 | /// The latest version of config dictionary, directly accessible, if needed. 69 | public class var config: [String : AnyObject] { 70 | let remoteConfig = ACMissionControl.sharedInstance.remoteConfig 71 | let cachedConfig = ACMissionControl.sharedInstance.cachedConfig 72 | let localConfig = ACMissionControl.sharedInstance.localConfig 73 | let emptyConfig = [String : AnyObject]() 74 | let resolvedConfig = remoteConfig ?? cachedConfig ?? localConfig ?? emptyConfig 75 | return resolvedConfig 76 | } 77 | 78 | /// Date of last successful refresh from remote. 79 | public class var refreshDate: NSDate? { 80 | return ACMissionControl.sharedInstance.refreshDate 81 | } 82 | 83 | /// Date of last cached remote config. 84 | public class var cacheDate: NSDate? { 85 | return ACMissionControl.sharedInstance.cacheDate 86 | } 87 | 88 | // MARK: API 89 | 90 | /** 91 | This should be called on your app start to initialize and/or refresh remote config. 92 | All parameters are optional but this is the only way you can set them. 93 | Good place to call this is in your AppDelegate's `didFinishLaunchingWithOptions:`. 94 | 95 | - parameter localConfig: Default local config which can be used until remote config is fetched. 96 | - parameter remoteConfigURL: If this parameter is set then `refresh` will be called, otherwise not. 97 | */ 98 | public class func launch(localConfig localConfig: [String : AnyObject]? = nil, remoteConfigURL url: NSURL? = nil) { 99 | ACMissionControl.sharedInstance.localConfig = localConfig 100 | ACMissionControl.sharedInstance.remoteURL = url 101 | } 102 | 103 | /** 104 | Manually initiates refreshing of local config from remote config if needed. 105 | If `remoteConfigURL` is not set when this is called an error will be thrown inside inner block. 106 | Good place to call this is in your AppDelegate's `applicationDidBecomeActive:`. 107 | 108 | - parameter completion: Completion handler (SEE: `ThrowWithInnerBlock`). 109 | */ 110 | public class func refresh(completion: ThrowWithInnerBlock? = nil) { 111 | ACMissionControl.sharedInstance.refresh(completion) 112 | } 113 | 114 | } 115 | 116 | // MARK: - MissionControlDelegate 117 | 118 | /** 119 | Delegate for Mission Control. 120 | 121 | All NSNotification events are also sent via this delegate. 122 | */ 123 | public protocol MissionControlDelegate: class { 124 | /** 125 | Called each time when config is refreshed from remote. 126 | 127 | - parameter old: Previous config (nil if it's the first refresh) 128 | - parameter new: Current config 129 | */ 130 | func missionControlDidRefreshConfig(old old: [String : AnyObject]?, new: [String : AnyObject]) 131 | 132 | /** 133 | Called when refreshing config from remote fails. 134 | 135 | - parameter error: Error which happened during config refresh from remote. 136 | */ 137 | func missionControlDidFailRefreshingConfig(error error: ErrorType) 138 | } 139 | 140 | // MARK: - Custom Types 141 | 142 | /// Block which throws via inner block. 143 | public typealias ThrowWithInnerBlock = (() throws -> Void) -> Void 144 | 145 | /// Block which throws dictionary via inner block. 146 | public typealias ThrowJSONWithInnerBlock = (block: () throws -> [String : AnyObject]) -> Void 147 | 148 | // MARK: - Accessors 149 | 150 | /** 151 | Accessor for retreiving setting of generic type `T` for given key. 152 | 153 | This method will resolve to proper setting by following this priority order: 154 | 1. Remote setting from memory (received in the last refresh). 155 | 2. Remote setting from disk cache (if never refreshed in current app session (ex. offline)). 156 | 3. Local setting from disk (defaults provided in `localConfig` on MissionControl `launch`). 157 | 4. Provided fallback value (if provided) 158 | 159 | - parameter key: Key for the setting. 160 | - parameter fallback: Fallback value if setting is not available in any config. 161 | 162 | - returns: Resolved setting of generic type `T` for given key. 163 | */ 164 | public func ConfigGeneric(key: String, fallback: T) -> T { 165 | if let remoteValue = ACMissionControl.sharedInstance.remoteConfig?[key] as? T { 166 | return remoteValue 167 | } else if let cachedValue = ACMissionControl.sharedInstance.cachedConfig?[key] as? T { 168 | return cachedValue 169 | } else if let localValue = ACMissionControl.sharedInstance.localConfig?[key] as? T { 170 | return localValue 171 | } else { 172 | return fallback 173 | } 174 | } 175 | 176 | /** 177 | Async "Force Remote" Accessor for retreiving the latest setting of generic type `T` for given key. 178 | 179 | This method will first call `refresh` method after which it will evaluate its success. 180 | 181 | If `refresh` was successful, it will call normal accessor of generic type `T` for given key, 182 | which will by its priority order resolve to the latest remote value as a parameter inside `completion` handler. 183 | 184 | If `refresh` fails, it will return provided `fallback` value as a parameter inside `completion` block. 185 | 186 | - parameter key: Key for the setting. 187 | - parameter fallback: Fallback value of generic type `T` if refresh is not successful. 188 | */ 189 | public func ConfigGenericForce(key: String, fallback: T, completion: ((forced: T) -> Void)) { 190 | MissionControl.refresh({ (innerBlock) in 191 | do { 192 | let _ = try innerBlock() 193 | completion(forced: ConfigGeneric(key, fallback: fallback)) 194 | } catch { 195 | completion(forced: fallback) 196 | } 197 | }) 198 | } 199 | 200 | /** 201 | Accessor helper for retreiving setting of type `Bool` for given key. 202 | It will call `ConfigGeneric` with `Bool` type. 203 | 204 | - parameter key: Key for the setting. 205 | - parameter fallback: Fallback value if setting not available in any config. Defaults to `Bool()`. 206 | 207 | - returns: Resolved setting of type `Bool` for given key. 208 | */ 209 | public func ConfigBool(key: String, fallback: Bool = Bool()) -> Bool { 210 | return ConfigGeneric(key, fallback: fallback) 211 | } 212 | 213 | /** 214 | Async "Force Remote" Accessor helper for retreiving the latest setting of type `Bool` for given key. 215 | It will call `ConfigGenericForce` with `Bool` type. 216 | 217 | - parameter key: Key for the setting. 218 | - parameter fallback: Fallback value if refresh was not successful. 219 | */ 220 | public func ConfigBoolForce(key: String, fallback: Bool, completion: ((forced: Bool) -> Void)) { 221 | ConfigGenericForce(key, fallback: fallback, completion: completion) 222 | } 223 | 224 | /** 225 | Accessor helper for retreiving setting of type `Int` for given key. 226 | It will call `ConfigGeneric` with `Int` type. 227 | 228 | - parameter key: Key for the setting. 229 | - parameter fallback: Fallback value if setting not available in any config. Defaults to `Int()`. 230 | 231 | - returns: Resolved setting of type `Int` for given key. 232 | */ 233 | public func ConfigInt(key: String, fallback: Int = Int()) -> Int { 234 | return ConfigGeneric(key, fallback: fallback) 235 | } 236 | 237 | /** 238 | Async "Force Remote" Accessor helper for retreiving the latest setting of type `Int` for given key. 239 | It will call `ConfigGenericForce` with `Int` type. 240 | 241 | - parameter key: Key for the setting. 242 | - parameter fallback: Fallback value if refresh was not successful. 243 | */ 244 | public func ConfigIntForce(key: String, fallback: Int, completion: ((forced: Int) -> Void)) { 245 | ConfigGenericForce(key, fallback: fallback, completion: completion) 246 | } 247 | 248 | /** 249 | Accessor helper for retreiving setting of type `Double` for given key. 250 | It will call `ConfigGeneric` with `Double` type. 251 | 252 | - parameter key: Key for the setting. 253 | - parameter fallback: Fallback value if setting not available in any config. Defaults to `Double()`. 254 | 255 | - returns: Resolved setting of type `Double` for given key. 256 | */ 257 | public func ConfigDouble(key: String, fallback: Double = Double()) -> Double { 258 | return ConfigGeneric(key, fallback: fallback) 259 | } 260 | 261 | /** 262 | Async "Force Remote" Accessor helper for retreiving the latest setting of type `Double` for given key. 263 | It will call `ConfigGenericForce` with `Double` type. 264 | 265 | - parameter key: Key for the setting. 266 | - parameter fallback: Fallback value if refresh was not successful. 267 | */ 268 | public func ConfigDoubleForce(key: String, fallback: Double, completion: ((forced: Double) -> Void)) { 269 | ConfigGenericForce(key, fallback: fallback, completion: completion) 270 | } 271 | 272 | /** 273 | Accessor helper for retreiving setting of type `String` for given key. 274 | It will call `ConfigGeneric` with `String` type. 275 | 276 | - parameter key: Key for the setting. 277 | - parameter fallback: Fallback value if setting not available in any config. Defaults to `String()`. 278 | 279 | - returns: Resolved setting of type `String` for given key. 280 | */ 281 | public func ConfigString(key: String, fallback: String = String()) -> String { 282 | return ConfigGeneric(key, fallback: fallback) 283 | } 284 | 285 | /** 286 | Async "Force Remote" Accessor helper for retreiving the latest setting of type `String` for given key. 287 | It will call `ConfigGenericForce` with `String` type. 288 | 289 | - parameter key: Key for the setting. 290 | - parameter fallback: Fallback value if refresh was not successful. 291 | */ 292 | public func ConfigStringForce(key: String, fallback: String, completion: ((forced: String) -> Void)) { 293 | ConfigGenericForce(key, fallback: fallback, completion: completion) 294 | } 295 | 296 | // MARK: - ACMissionControl 297 | 298 | class ACMissionControl { 299 | 300 | // MARK: Singleton 301 | 302 | static let sharedInstance = ACMissionControl() 303 | 304 | // MARK: Properties 305 | 306 | weak var delegate: MissionControlDelegate? 307 | 308 | var localConfig: [String : AnyObject]? 309 | 310 | var remoteURL: NSURL? { 311 | didSet { 312 | if let _ = remoteURL { 313 | refresh({ (block) in 314 | do { 315 | _ = try block() 316 | } catch { 317 | print(error) 318 | } 319 | }) 320 | } 321 | } 322 | } 323 | 324 | var remoteConfig: [String : AnyObject]? { 325 | didSet { 326 | if let newConfig = remoteConfig { 327 | refreshDate = NSDate() 328 | 329 | cachedConfig = newConfig 330 | cacheDate = refreshDate 331 | 332 | informListeners(oldConfig: oldValue, newConfig: newConfig) 333 | } 334 | } 335 | } 336 | 337 | private func informListeners(oldConfig oldConfig: [String : AnyObject]?, newConfig: [String : AnyObject]) { 338 | let userInfo = userInfoWithConfig(old: oldConfig, new: newConfig) 339 | delegate?.missionControlDidRefreshConfig(old: oldConfig, new: newConfig) 340 | sendNotification(MissionControl.Notification.DidRefreshConfig, userInfo: userInfo) 341 | } 342 | 343 | var refreshDate: NSDate? 344 | 345 | private struct Cache { 346 | static let Config = "ACMissionControl.CachedConfig" 347 | static let Date = "ACMissionControl.CacheDate" 348 | } 349 | 350 | var cachedConfig: [String : AnyObject]? { 351 | get { 352 | let userDefaults = NSUserDefaults.standardUserDefaults() 353 | let config = userDefaults.objectForKey(Cache.Config) as? [String : AnyObject] 354 | return config 355 | } 356 | set { 357 | let userDefaults = NSUserDefaults.standardUserDefaults() 358 | userDefaults.setObject(newValue, forKey: Cache.Config) 359 | userDefaults.synchronize() 360 | } 361 | } 362 | 363 | var cacheDate: NSDate? { 364 | get { 365 | let userDefaults = NSUserDefaults.standardUserDefaults() 366 | let config = userDefaults.objectForKey(Cache.Date) as? NSDate 367 | return config 368 | } 369 | set { 370 | let userDefaults = NSUserDefaults.standardUserDefaults() 371 | userDefaults.setObject(newValue, forKey: Cache.Date) 372 | userDefaults.synchronize() 373 | } 374 | } 375 | 376 | // MARK: API 377 | 378 | func refresh(completion: ThrowWithInnerBlock? = nil) { 379 | getRemoteConfig { [unowned self] (block) in 380 | dispatch_async(dispatch_get_main_queue()) { [unowned self] in 381 | do { 382 | let remoteConfig = try block() 383 | self.remoteConfig = remoteConfig 384 | completion?({ }) 385 | } catch { 386 | self.informListeners(error) 387 | completion?({ throw error }) 388 | } 389 | } 390 | } 391 | } 392 | 393 | private func informListeners(error: ErrorType) { 394 | delegate?.missionControlDidFailRefreshingConfig(error: error) 395 | let userInfo = ["Error" : "\(error)"] 396 | sendNotification(MissionControl.Notification.DidFailRefreshingConfig, userInfo: userInfo) 397 | } 398 | 399 | // MARK: Helpers 400 | 401 | func resetAll() { 402 | localConfig = nil 403 | cachedConfig = nil 404 | remoteConfig = nil 405 | refreshDate = nil 406 | remoteURL = nil 407 | delegate = nil 408 | } 409 | 410 | func resetRemote() { 411 | remoteConfig = nil 412 | refreshDate = nil 413 | } 414 | 415 | private func userInfoWithConfig(old old: [String : AnyObject]?, new: [String : AnyObject]?) -> [NSObject : AnyObject]? { 416 | if old == nil && new == nil { 417 | return nil 418 | } else { 419 | var userInfo = [NSObject : AnyObject]() 420 | if let oldConfig = old { 421 | userInfo[MissionControl.Notification.UserInfo.OldConfigKey] = oldConfig 422 | } 423 | if let newConfig = new { 424 | userInfo[MissionControl.Notification.UserInfo.NewConfigKey] = newConfig 425 | } 426 | return userInfo 427 | } 428 | } 429 | 430 | private func sendNotification(name: String, userInfo: [NSObject : AnyObject]? = nil) { 431 | let center = NSNotificationCenter.defaultCenter() 432 | center.postNotificationName(name, object: self, userInfo: userInfo) 433 | } 434 | 435 | private func getRemoteConfig(completion: ThrowJSONWithInnerBlock) { 436 | guard let url = remoteURL 437 | else { completion(block: { throw MissionControl.Error.NoRemoteURL }); return } 438 | 439 | let request = NSURLRequest(URL: url) 440 | let session = NSURLSession.sharedSession() 441 | 442 | let task = session.dataTaskWithRequest(request) { [unowned self] (data, response, error) in 443 | guard let httpResponse = response as? NSHTTPURLResponse where httpResponse.statusCode == 200 444 | else { completion(block: { throw MissionControl.Error.BadResponseCode }); return } 445 | self.parseRemoteConfigFromData(data, completion: completion) 446 | } 447 | 448 | task.resume() 449 | } 450 | 451 | private func parseRemoteConfigFromData(data: NSData?, completion: ThrowJSONWithInnerBlock) { 452 | guard let configData = data 453 | else { completion(block: { throw MissionControl.Error.InvalidData }); return } 454 | 455 | do { 456 | let json = try NSJSONSerialization.JSONObjectWithData(configData, options: .AllowFragments) 457 | guard let config = json as? [String : AnyObject] 458 | else { completion(block: { throw MissionControl.Error.InvalidData }); return } 459 | completion(block: { return config }) 460 | } catch { 461 | completion(block: { throw MissionControl.Error.InvalidData }) 462 | } 463 | } 464 | 465 | } 466 | -------------------------------------------------------------------------------- /Tests/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 | CFBundleSignature 20 | ???? 21 | CFBundleVersion 22 | 1 23 | 24 | 25 | -------------------------------------------------------------------------------- /Tests/MissionControlTests.swift: -------------------------------------------------------------------------------- 1 | // 2 | // MissionControlTests.swift 3 | // 4 | // Copyright (c) 2016 appculture http://appculture.com 5 | // 6 | // Permission is hereby granted, free of charge, to any person obtaining a copy 7 | // of this software and associated documentation files (the "Software"), to deal 8 | // in the Software without restriction, including without limitation the rights 9 | // to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 10 | // copies of the Software, and to permit persons to whom the Software is 11 | // furnished to do so, subject to the following conditions: 12 | // 13 | // The above copyright notice and this permission notice shall be included in all 14 | // copies or substantial portions of the Software. 15 | // 16 | // THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 17 | // IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 18 | // FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 19 | // AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 20 | // LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 21 | // OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 22 | // SOFTWARE. 23 | // 24 | 25 | import XCTest 26 | @testable import MissionControl 27 | 28 | class MissionControlTests: XCTestCase, MissionControlDelegate { 29 | 30 | // MARK: - Lifecycle 31 | 32 | override func setUp() { 33 | super.setUp() 34 | // Put setup code here. This method is called before the invocation of each test method in the class. 35 | } 36 | 37 | override func tearDown() { 38 | // Put teardown code here. This method is called after the invocation of each test method in the class. 39 | ACMissionControl.sharedInstance.resetAll() 40 | 41 | super.tearDown() 42 | } 43 | 44 | // MARK: - Helper Properties 45 | 46 | struct URL { 47 | static let BadResponseConfig = NSURL(string: "http://appculture.com/mission-control/not-existing-config.json")! 48 | static let EmptyDataConfig = NSURL(string: "http://private-83024-missioncontrol5.apiary-mock.com/mission-control/empty-config")! 49 | static let InvalidDataConfig = NSURL(string: "http://private-83024-missioncontrol5.apiary-mock.com/mission-control/invalid-config")! 50 | static let RemoteTestConfig = NSURL(string: "http://private-83024-missioncontrol5.apiary-mock.com/mission-control/test-config")! 51 | } 52 | 53 | struct Key { 54 | static let Bool = "BoolSetting" 55 | static let Int = "IntSetting" 56 | static let Double = "DoubleSetting" 57 | static let String = "StringSetting" 58 | } 59 | 60 | let localTestConfig: [String : AnyObject] = [ 61 | Key.Bool : false, 62 | Key.Int : 21, 63 | Key.Double : 0.8, 64 | Key.String : "Local" 65 | ] 66 | 67 | let remoteTestConfig: [String : AnyObject] = [ 68 | Key.Bool : true, 69 | Key.Int : 8, 70 | Key.Double : 2.1, 71 | Key.String : "Remote" 72 | ] 73 | 74 | let fallbackTestConfig: [String : AnyObject] = [ 75 | Key.Bool : false, 76 | Key.Int : 1984, 77 | Key.Double : 21.08, 78 | Key.String : "Fallback" 79 | ] 80 | 81 | var didRefreshConfigExpectation: XCTestExpectation? 82 | var didFailRefreshingConfigExpectation: XCTestExpectation? 83 | 84 | // MARK: - MissionControlDelegate 85 | 86 | func missionControlDidRefreshConfig(old old: [String : AnyObject]?, new: [String : AnyObject]) { 87 | didRefreshConfigExpectation?.fulfill() 88 | } 89 | 90 | func missionControlDidFailRefreshingConfig(error error: ErrorType) { 91 | didFailRefreshingConfigExpectation?.fulfill() 92 | } 93 | 94 | // MARK: - Test Initial State 95 | 96 | func testInitialConfig() { 97 | let config = MissionControl.config 98 | XCTAssertEqual(config.count, 0, "Initial config should be empty but not nil.") 99 | } 100 | 101 | func testInitialRefreshDate() { 102 | let date = MissionControl.refreshDate 103 | XCTAssertNil(date, "Initial refresh date should be nil.") 104 | } 105 | 106 | func testInitialAccessorsWithoutFallbackValues() { 107 | let bool = ConfigBool(Key.Bool) 108 | XCTAssertEqual(bool, false, "Should default to false.") 109 | 110 | let int = ConfigInt(Key.Int) 111 | XCTAssertEqual(int, 0, "Should default to 0.") 112 | 113 | let double = ConfigDouble(Key.Double) 114 | XCTAssertEqual(double, 0.0, "Should default to 0.0.") 115 | 116 | let string = ConfigString(Key.String) 117 | XCTAssertEqual(string, String(), "Should default to empty string.") 118 | } 119 | 120 | func testInitialAccessorsWithFallbackValues() { 121 | let fallbackBool = fallbackTestConfig[Key.Bool] as! Bool 122 | let fallbackInt = fallbackTestConfig[Key.Int] as! Int 123 | let fallbackDouble = fallbackTestConfig[Key.Double] as! Double 124 | let fallbackString = fallbackTestConfig[Key.String] as! String 125 | 126 | let bool = ConfigBool(Key.Bool, fallback: fallbackBool) 127 | XCTAssertEqual(bool, fallbackBool, "Should resolve to fallback value.") 128 | 129 | let int = ConfigInt(Key.Int, fallback: fallbackInt) 130 | XCTAssertEqual(int, fallbackInt, "Should resolve to fallback value.") 131 | 132 | let double = ConfigDouble(Key.Double, fallback: fallbackDouble) 133 | XCTAssertEqual(double, fallbackDouble, "Should resolve to fallback value.") 134 | 135 | let string = ConfigString(Key.String, fallback: fallbackString) 136 | XCTAssertEqual(string, fallbackString, "Should resolve to fallback value.") 137 | } 138 | 139 | // MARK: - Test Launch Without Parameters 140 | 141 | func testLaunchWithoutParameters() { 142 | MissionControl.launch() 143 | confirmInitialState() 144 | } 145 | 146 | func confirmInitialState() { 147 | testInitialConfig() 148 | testInitialRefreshDate() 149 | 150 | testInitialAccessorsWithFallbackValues() 151 | testInitialAccessorsWithoutFallbackValues() 152 | } 153 | 154 | // MARK: - Test Launch With Local Config 155 | 156 | func testLaunchWithLocalConfig() { 157 | MissionControl.launch(localConfig: localTestConfig) 158 | confirmLocalConfigState() 159 | } 160 | 161 | func confirmLocalConfigState() { 162 | let config = MissionControl.config 163 | XCTAssertEqual(config.count, localTestConfig.count, "Initial config should contain given local config.") 164 | 165 | let date = MissionControl.refreshDate 166 | XCTAssertNil(date, "Initial refresh date should be nil.") 167 | 168 | confirmLocalConfigAccessorsWithoutDefaultValues() 169 | confirmLocalConfigAccessorsWithDefaultValues() 170 | } 171 | 172 | func confirmLocalConfigAccessorsWithDefaultValues() { 173 | let bool = ConfigBool(Key.Bool, fallback: true) 174 | let expectedBool = localTestConfig[Key.Bool] as! Bool 175 | XCTAssertEqual(bool, expectedBool, "Should resolve to value in local test config.") 176 | 177 | let int = ConfigInt(Key.Int, fallback: 1984) 178 | let expectedInt = localTestConfig[Key.Int] as! Int 179 | XCTAssertEqual(int, expectedInt, "Should resolve to value in local test config.") 180 | 181 | let double = ConfigDouble(Key.Double, fallback: 21.08) 182 | let expectedDouble = localTestConfig[Key.Double] as! Double 183 | XCTAssertEqual(double, expectedDouble, "Should resolve to value in local test config.") 184 | 185 | let string = ConfigString(Key.String, fallback: "Default") 186 | let expectedString = localTestConfig[Key.String] as! String 187 | XCTAssertEqual(string, expectedString, "Should resolve to value in local test config.") 188 | } 189 | 190 | func confirmLocalConfigAccessorsWithoutDefaultValues() { 191 | let bool = ConfigBool(Key.Bool) 192 | let expectedBool = localTestConfig[Key.Bool] as! Bool 193 | XCTAssertEqual(bool, expectedBool, "Should resolve to value in local test config.") 194 | 195 | let int = ConfigInt(Key.Int) 196 | let expectedInt = localTestConfig[Key.Int] as! Int 197 | XCTAssertEqual(int, expectedInt, "Should resolve to value in local test config.") 198 | 199 | let double = ConfigDouble(Key.Double) 200 | let expectedDouble = localTestConfig[Key.Double] as! Double 201 | XCTAssertEqual(double, expectedDouble, "Should resolve to value in local test config.") 202 | 203 | let string = ConfigString(Key.String) 204 | let expectedString = localTestConfig[Key.String] as! String 205 | XCTAssertEqual(string, expectedString, "Should resolve to value in local test config.") 206 | } 207 | 208 | // MARK: - Test Launch With Remote Config 209 | 210 | func testLaunchWithRemoteConfig() { 211 | MissionControl.launch(remoteConfigURL: URL.RemoteTestConfig) 212 | confirmInitialState() 213 | confirmRemoteConfigStateAfterNotification(MissionControl.Notification.DidRefreshConfig) 214 | } 215 | 216 | // MARK: - Test Launch With Local & Remote Config 217 | 218 | func testLaunchWithLocalAndRemoteConfig() { 219 | MissionControl.launch(localConfig: localTestConfig, remoteConfigURL: URL.RemoteTestConfig) 220 | confirmLocalConfigState() 221 | confirmRemoteConfigStateAfterNotification(MissionControl.Notification.DidRefreshConfig) 222 | } 223 | 224 | // MARK: - Test Refresh 225 | 226 | func testAutomaticRefresh() { 227 | /// - NOTE: refresh is called automatically during launch 228 | MissionControl.launch(remoteConfigURL: URL.RemoteTestConfig) 229 | confirmRemoteConfigStateAfterNotification(MissionControl.Notification.DidRefreshConfig) 230 | } 231 | 232 | func testManualRefresh() { 233 | testAutomaticRefresh() 234 | 235 | MissionControl.refresh() 236 | confirmRemoteConfigStateAfterNotification(MissionControl.Notification.DidRefreshConfig) 237 | } 238 | 239 | // MARK: - Test Remote Accessors 240 | 241 | func confirmRemoteConfigStateAfterNotification(notification: String) { 242 | confirmDidRefreshConfigDelegateCallback() 243 | 244 | let _ = expectationForNotification(notification, object: nil) { (notification) -> Bool in 245 | self.confirmRemoteConfigState() 246 | return true 247 | } 248 | waitForExpectationsWithTimeout(5, handler: nil) 249 | } 250 | 251 | func confirmDidRefreshConfigDelegateCallback() { 252 | MissionControl.delegate = self 253 | didRefreshConfigExpectation = expectationWithDescription("Should call MissionControlDelegate.") 254 | } 255 | 256 | func confirmRemoteConfigState() { 257 | let config = MissionControl.config 258 | XCTAssertEqual(config.count, remoteTestConfig.count, "Config should contain all settings from remote config.") 259 | 260 | let date = MissionControl.refreshDate 261 | XCTAssertNotNil(date, "Initial refresh date should not be nil.") 262 | 263 | confirmRemoteConfigAccessorsWithDefaultValues() 264 | confirmRemoteConfigAccessorsWithoutDefaultValues() 265 | } 266 | 267 | func confirmRemoteConfigAccessorsWithDefaultValues() { 268 | let bool = ConfigBool(Key.Bool) 269 | let expectedBool = remoteTestConfig[Key.Bool] as! Bool 270 | XCTAssertEqual(bool, expectedBool, "Should resolve to value in remote test config.") 271 | 272 | let int = ConfigInt(Key.Int) 273 | let expectedInt = remoteTestConfig[Key.Int] as! Int 274 | XCTAssertEqual(int, expectedInt, "Should resolve to value in remote test config.") 275 | 276 | let double = ConfigDouble(Key.Double) 277 | let expectedDouble = remoteTestConfig[Key.Double] as! Double 278 | XCTAssertEqual(double, expectedDouble, "Should resolve to value in remote test config.") 279 | 280 | let string = ConfigString(Key.String) 281 | let expectedString = remoteTestConfig[Key.String] as! String 282 | XCTAssertEqual(string, expectedString, "Should resolve to value in remote test config.") 283 | } 284 | 285 | func confirmRemoteConfigAccessorsWithoutDefaultValues() { 286 | let bool = ConfigBool(Key.Bool) 287 | let expectedBool = remoteTestConfig[Key.Bool] as! Bool 288 | XCTAssertEqual(bool, expectedBool, "Should resolve to value in remote test config.") 289 | 290 | let int = ConfigInt(Key.Int) 291 | let expectedInt = remoteTestConfig[Key.Int] as! Int 292 | XCTAssertEqual(int, expectedInt, "Should resolve to value in remote test config.") 293 | 294 | let double = ConfigDouble(Key.Double) 295 | let expectedDouble = remoteTestConfig[Key.Double] as! Double 296 | XCTAssertEqual(double, expectedDouble, "Should resolve to value in remote test config.") 297 | 298 | let string = ConfigString(Key.String) 299 | let expectedString = remoteTestConfig[Key.String] as! String 300 | XCTAssertEqual(string, expectedString, "Should resolve to value in remote test config.") 301 | } 302 | 303 | // MARK: - Test Force Remote Accessors 304 | 305 | func testForceRemoteAccessors() { 306 | MissionControl.launch(remoteConfigURL: URL.RemoteTestConfig) 307 | 308 | let boolExpectation = expectationWithDescription("ConfigBoolForce") 309 | let intExpectation = expectationWithDescription("ConfigIntForce") 310 | let doubleExpectation = expectationWithDescription("ConfigDoubleForce") 311 | let stringExpectation = expectationWithDescription("ConfigStringForce") 312 | 313 | let fallbackBool = fallbackTestConfig[Key.Bool] as! Bool 314 | let fallbackInt = fallbackTestConfig[Key.Int] as! Int 315 | let fallbackDouble = fallbackTestConfig[Key.Double] as! Double 316 | let fallbackString = fallbackTestConfig[Key.String] as! String 317 | 318 | ConfigBoolForce(Key.Bool, fallback: fallbackBool) { (forced) in 319 | let expectedBool = self.remoteTestConfig[Key.Bool] as! Bool 320 | XCTAssertEqual(forced, expectedBool, "Should resolve to value in remote test config.") 321 | boolExpectation.fulfill() 322 | } 323 | ConfigIntForce(Key.Int, fallback: fallbackInt) { (forced) in 324 | let expectedInt = self.remoteTestConfig[Key.Int] as! Int 325 | XCTAssertEqual(forced, expectedInt, "Should resolve to value in remote test config.") 326 | intExpectation.fulfill() 327 | } 328 | ConfigDoubleForce(Key.Double, fallback: fallbackDouble) { (forced) in 329 | let expectedDouble = self.remoteTestConfig[Key.Double] as! Double 330 | XCTAssertEqual(forced, expectedDouble, "Should resolve to value in remote test config.") 331 | doubleExpectation.fulfill() 332 | } 333 | ConfigStringForce(Key.String, fallback: fallbackString) { (forced) in 334 | let expectedString = self.remoteTestConfig[Key.String] as! String 335 | XCTAssertEqual(forced, expectedString, "Should resolve to value in remote test config.") 336 | stringExpectation.fulfill() 337 | } 338 | 339 | waitForExpectationsWithTimeout(10.0, handler: nil) 340 | } 341 | 342 | func testForceRemoteAccessorsFallback() { 343 | MissionControl.launch(remoteConfigURL: URL.BadResponseConfig) 344 | 345 | let boolExpectation = expectationWithDescription("ConfigBoolForceFallback") 346 | let intExpectation = expectationWithDescription("ConfigIntForceFallback") 347 | let doubleExpectation = expectationWithDescription("ConfigDoubleForceFallback") 348 | let stringExpectation = expectationWithDescription("ConfigStringForceFallback") 349 | 350 | let fallbackBool = fallbackTestConfig[Key.Bool] as! Bool 351 | let fallbackInt = fallbackTestConfig[Key.Int] as! Int 352 | let fallbackDouble = fallbackTestConfig[Key.Double] as! Double 353 | let fallbackString = fallbackTestConfig[Key.String] as! String 354 | 355 | ConfigBoolForce(Key.Bool, fallback: fallbackBool) { (forced) in 356 | XCTAssertEqual(forced, fallbackBool, "Should resolve to fallback value.") 357 | boolExpectation.fulfill() 358 | } 359 | ConfigIntForce(Key.Int, fallback: fallbackInt) { (forced) in 360 | XCTAssertEqual(forced, fallbackInt, "Should resolve to fallback value.") 361 | intExpectation.fulfill() 362 | } 363 | ConfigDoubleForce(Key.Double, fallback: fallbackDouble) { (forced) in 364 | XCTAssertEqual(forced, fallbackDouble, "Should resolve to fallback value.") 365 | doubleExpectation.fulfill() 366 | } 367 | ConfigStringForce(Key.String, fallback: fallbackString) { (forced) in 368 | XCTAssertEqual(forced, fallbackString, "Should resolve to fallback value.") 369 | stringExpectation.fulfill() 370 | } 371 | 372 | waitForExpectationsWithTimeout(10.0, handler: nil) 373 | } 374 | 375 | // MARK: - Test Cache 376 | 377 | func testCache() { 378 | MissionControl.launch(remoteConfigURL: URL.RemoteTestConfig) 379 | 380 | let notification = MissionControl.Notification.DidRefreshConfig 381 | let _ = expectationForNotification(notification, object: nil) { (notification) -> Bool in 382 | ACMissionControl.sharedInstance.resetRemote() 383 | self.confirmCachedConfigState() 384 | return true 385 | } 386 | waitForExpectationsWithTimeout(5, handler: nil) 387 | } 388 | 389 | func confirmCachedConfigState() { 390 | let config = MissionControl.config 391 | XCTAssertEqual(config.count, remoteTestConfig.count, "Cached config should contain all settings from Remote config.") 392 | 393 | let date = MissionControl.cacheDate 394 | XCTAssertNotNil(date, "Cache date should not be nil.") 395 | 396 | confirmRemoteConfigAccessorsWithDefaultValues() 397 | confirmRemoteConfigAccessorsWithoutDefaultValues() 398 | } 399 | 400 | // MARK: - Test Refresh Errors 401 | 402 | func testRefreshErrorNoRemoteURL() { 403 | MissionControl.launch() 404 | 405 | /// - NOTE: refresh is NOT called automatically during launch (remote URL missing) 406 | let asyncExpectation = expectationWithDescription("ManualRefreshWithoutURL") 407 | MissionControl.refresh { (block) in 408 | do { 409 | let _ = try block() 410 | XCTAssert(false, "Should fall through to catch block!") 411 | asyncExpectation.fulfill() 412 | } catch { 413 | let message = "Should return NoRemoteURL error whene remoteURL is not set." 414 | XCTAssertEqual("\(error)", "\(MissionControl.Error.NoRemoteURL)", message) 415 | asyncExpectation.fulfill() 416 | } 417 | } 418 | waitForExpectationsWithTimeout(5, handler: nil) 419 | } 420 | 421 | func testRefreshErrorBadResponseCode() { 422 | MissionControl.launch(remoteConfigURL: URL.BadResponseConfig) 423 | /// - NOTE: refresh is called automatically during launch 424 | 425 | let message = "Should return BadResponseCode error when response is not 200 OK." 426 | confirmConfigRefreshFailedNotification(MissionControl.Error.BadResponseCode, message: message) 427 | } 428 | 429 | func testRefreshErrorInvalidDataEmpty() { 430 | MissionControl.launch(remoteConfigURL: URL.EmptyDataConfig) 431 | /// - NOTE: refresh is called automatically during launch 432 | 433 | let message = "Should return InvalidData error when response data is empty." 434 | confirmConfigRefreshFailedNotification(MissionControl.Error.InvalidData, message: message) 435 | } 436 | 437 | func testRefreshErrorInvalidData() { 438 | MissionControl.launch(remoteConfigURL: URL.InvalidDataConfig) 439 | /// - NOTE: refresh is called automatically during launch 440 | 441 | let message = "Should return InvalidData error when response data is not valid JSON." 442 | confirmConfigRefreshFailedNotification(MissionControl.Error.InvalidData, message: message) 443 | } 444 | 445 | func confirmConfigRefreshFailedNotification(error: MissionControl.Error, message: String) { 446 | confirmDidFailRefreshingConfigDelegateCallback() 447 | 448 | let notification = MissionControl.Notification.DidFailRefreshingConfig 449 | let _ = expectationForNotification(notification, object: nil) { (notification) -> Bool in 450 | guard let errorInfo = notification.userInfo?["Error"] as? String else { return false } 451 | XCTAssertEqual("\(errorInfo)", "\(error)", message) 452 | self.confirmInitialState() 453 | return true 454 | } 455 | waitForExpectationsWithTimeout(5, handler: nil) 456 | } 457 | 458 | func confirmDidFailRefreshingConfigDelegateCallback() { 459 | MissionControl.delegate = self 460 | didFailRefreshingConfigExpectation = expectationWithDescription("Should call MissionControlDelegate.") 461 | } 462 | 463 | } 464 | --------------------------------------------------------------------------------