diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..6b20294 --- /dev/null +++ b/.gitignore @@ -0,0 +1,81 @@ +# Xcode +# +# gitignore contributors: remember to update Global/Xcode.gitignore, Objective-C.gitignore & Swift.gitignore + +## Build generated +build/ +DerivedData/ + +## Various settings +*.pbxuser +!default.pbxuser +*.mode1v3 +!default.mode1v3 +*.mode2v3 +!default.mode2v3 +*.perspectivev3 +!default.perspectivev3 +xcuserdata/ + +## Other +*.moved-aside +*.xccheckout +*.xcscmblueprint + +## Obj-C/Swift specific +*.hmap +*.ipa +*.dSYM.zip +*.dSYM + +## Playgrounds +timeline.xctimeline +playground.xcworkspace + +# Swift Package Manager +# +# Add this line if you want to avoid checking in source code from Swift Package Manager dependencies. +# Packages/ +# Package.pins +# Package.resolved +.build/ + +# CocoaPods +# +# We recommend against adding the Pods directory to your .gitignore. However +# you should judge for yourself, the pros and cons are mentioned at: +# https://guides.cocoapods.org/using/using-cocoapods.html#should-i-check-the-pods-directory-into-source-control +# +# Pods/ +# +# Add this line if you want to avoid checking in source code from the Xcode workspace +# *.xcworkspace + +# Carthage +# +# Add this line if you want to avoid checking in source code from Carthage dependencies. + +Carthage/ + +# fastlane +# +# It is recommended to not store the screenshots in the git repo. Instead, use fastlane to re-generate the +# screenshots whenever they are needed. +# For more information about the recommended setup visit: +# https://docs.fastlane.tools/best-practices/source-control/#source-control + +fastlane/report.xml +fastlane/Preview.html +fastlane/screenshots/**/*.png +fastlane/test_output + +# Code Injection +# +# After new code Injection tools there's a generated folder /iOSInjectionProject +# https://github.com/johnno1962/injectionforxcode + +iOSInjectionProject/ + +# macOS + +*.DS_Store diff --git a/Example/MissionControlDemo.xcodeproj/project.pbxproj b/Example/MissionControlDemo.xcodeproj/project.pbxproj index 136f5ca..27e4a2a 100644 --- a/Example/MissionControlDemo.xcodeproj/project.pbxproj +++ b/Example/MissionControlDemo.xcodeproj/project.pbxproj @@ -42,6 +42,41 @@ remoteGlobalIDString = 8B63137A1CE5F9A10029DC98; remoteInfo = "MissionControl iOS"; }; + 8BA444261D4F6EAA00E99FAE /* PBXContainerItemProxy */ = { + isa = PBXContainerItemProxy; + containerPortal = 8B2A29C61CEA27DF00FAE67F /* MissionControl.xcodeproj */; + proxyType = 2; + remoteGlobalIDString = 8B03C1E01CF5E10500B09B48; + remoteInfo = "MissionControl watchOS"; + }; + 8BA444281D4F6EAA00E99FAE /* PBXContainerItemProxy */ = { + isa = PBXContainerItemProxy; + containerPortal = 8B2A29C61CEA27DF00FAE67F /* MissionControl.xcodeproj */; + proxyType = 2; + remoteGlobalIDString = 8B03C1EF1CF5E1DD00B09B48; + remoteInfo = "MissionControl tvOS"; + }; + 8BA4442A1D4F6EAA00E99FAE /* PBXContainerItemProxy */ = { + isa = PBXContainerItemProxy; + containerPortal = 8B2A29C61CEA27DF00FAE67F /* MissionControl.xcodeproj */; + proxyType = 2; + remoteGlobalIDString = 8B03C1F81CF5E1DD00B09B48; + remoteInfo = "MissionControl tvOS Tests"; + }; + 8BA4442C1D4F6EAA00E99FAE /* PBXContainerItemProxy */ = { + isa = PBXContainerItemProxy; + containerPortal = 8B2A29C61CEA27DF00FAE67F /* MissionControl.xcodeproj */; + proxyType = 2; + remoteGlobalIDString = 8B03C20E1CF5E28C00B09B48; + remoteInfo = "MissionControl OSX"; + }; + 8BA4442E1D4F6EAA00E99FAE /* PBXContainerItemProxy */ = { + isa = PBXContainerItemProxy; + containerPortal = 8B2A29C61CEA27DF00FAE67F /* MissionControl.xcodeproj */; + proxyType = 2; + remoteGlobalIDString = 8B03C2171CF5E28C00B09B48; + remoteInfo = "MissionControl OSX Tests"; + }; /* End PBXContainerItemProxy section */ /* Begin PBXCopyFilesBuildPhase section */ @@ -89,7 +124,12 @@ isa = PBXGroup; children = ( 8B2A29CC1CEA27DF00FAE67F /* MissionControl.framework */, - 8B2A29CE1CEA27DF00FAE67F /* MissionControlTests.xctest */, + 8B2A29CE1CEA27DF00FAE67F /* MissionControl iOS Tests.xctest */, + 8BA444271D4F6EAA00E99FAE /* MissionControl.framework */, + 8BA444291D4F6EAA00E99FAE /* MissionControl.framework */, + 8BA4442B1D4F6EAA00E99FAE /* MissionControl tvOS Tests.xctest */, + 8BA4442D1D4F6EAA00E99FAE /* MissionControl.framework */, + 8BA4442F1D4F6EAA00E99FAE /* MissionControl OSX Tests.xctest */, ); name = Products; sourceTree = ""; @@ -189,11 +229,12 @@ isa = PBXProject; attributes = { LastSwiftUpdateCheck = 0730; - LastUpgradeCheck = 0730; + LastUpgradeCheck = 0810; ORGANIZATIONNAME = appculture; TargetAttributes = { 8B464DCB1CE3742F00BAE834 = { CreatedOnToolsVersion = 7.3.1; + LastSwiftMigration = 0800; }; }; }; @@ -229,13 +270,48 @@ remoteRef = 8B2A29CB1CEA27DF00FAE67F /* PBXContainerItemProxy */; sourceTree = BUILT_PRODUCTS_DIR; }; - 8B2A29CE1CEA27DF00FAE67F /* MissionControlTests.xctest */ = { + 8B2A29CE1CEA27DF00FAE67F /* MissionControl iOS Tests.xctest */ = { isa = PBXReferenceProxy; fileType = wrapper.cfbundle; - path = MissionControlTests.xctest; + path = "MissionControl iOS Tests.xctest"; remoteRef = 8B2A29CD1CEA27DF00FAE67F /* PBXContainerItemProxy */; sourceTree = BUILT_PRODUCTS_DIR; }; + 8BA444271D4F6EAA00E99FAE /* MissionControl.framework */ = { + isa = PBXReferenceProxy; + fileType = wrapper.framework; + path = MissionControl.framework; + remoteRef = 8BA444261D4F6EAA00E99FAE /* PBXContainerItemProxy */; + sourceTree = BUILT_PRODUCTS_DIR; + }; + 8BA444291D4F6EAA00E99FAE /* MissionControl.framework */ = { + isa = PBXReferenceProxy; + fileType = wrapper.framework; + path = MissionControl.framework; + remoteRef = 8BA444281D4F6EAA00E99FAE /* PBXContainerItemProxy */; + sourceTree = BUILT_PRODUCTS_DIR; + }; + 8BA4442B1D4F6EAA00E99FAE /* MissionControl tvOS Tests.xctest */ = { + isa = PBXReferenceProxy; + fileType = wrapper.cfbundle; + path = "MissionControl tvOS Tests.xctest"; + remoteRef = 8BA4442A1D4F6EAA00E99FAE /* PBXContainerItemProxy */; + sourceTree = BUILT_PRODUCTS_DIR; + }; + 8BA4442D1D4F6EAA00E99FAE /* MissionControl.framework */ = { + isa = PBXReferenceProxy; + fileType = wrapper.framework; + path = MissionControl.framework; + remoteRef = 8BA4442C1D4F6EAA00E99FAE /* PBXContainerItemProxy */; + sourceTree = BUILT_PRODUCTS_DIR; + }; + 8BA4442F1D4F6EAA00E99FAE /* MissionControl OSX Tests.xctest */ = { + isa = PBXReferenceProxy; + fileType = wrapper.cfbundle; + path = "MissionControl OSX Tests.xctest"; + remoteRef = 8BA4442E1D4F6EAA00E99FAE /* PBXContainerItemProxy */; + sourceTree = BUILT_PRODUCTS_DIR; + }; /* End PBXReferenceProxy section */ /* Begin PBXResourcesBuildPhase section */ @@ -309,8 +385,10 @@ CLANG_WARN_DIRECT_OBJC_ISA_USAGE = YES_ERROR; CLANG_WARN_EMPTY_BODY = YES; CLANG_WARN_ENUM_CONVERSION = YES; + CLANG_WARN_INFINITE_RECURSION = YES; CLANG_WARN_INT_CONVERSION = YES; CLANG_WARN_OBJC_ROOT_CLASS = YES_ERROR; + CLANG_WARN_SUSPICIOUS_MOVE = YES; CLANG_WARN_UNREACHABLE_CODE = YES; CLANG_WARN__DUPLICATE_METHOD_MATCH = YES; "CODE_SIGN_IDENTITY[sdk=iphoneos*]" = "iPhone Developer"; @@ -355,8 +433,10 @@ CLANG_WARN_DIRECT_OBJC_ISA_USAGE = YES_ERROR; CLANG_WARN_EMPTY_BODY = YES; CLANG_WARN_ENUM_CONVERSION = YES; + CLANG_WARN_INFINITE_RECURSION = YES; CLANG_WARN_INT_CONVERSION = YES; CLANG_WARN_OBJC_ROOT_CLASS = YES_ERROR; + CLANG_WARN_SUSPICIOUS_MOVE = YES; CLANG_WARN_UNREACHABLE_CODE = YES; CLANG_WARN__DUPLICATE_METHOD_MATCH = YES; "CODE_SIGN_IDENTITY[sdk=iphoneos*]" = "iPhone Developer"; @@ -383,24 +463,27 @@ 8B464DDF1CE3742F00BAE834 /* Debug */ = { isa = XCBuildConfiguration; buildSettings = { + ALWAYS_EMBED_SWIFT_STANDARD_LIBRARIES = YES; ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon; - EMBEDDED_CONTENT_CONTAINS_SWIFT = YES; INFOPLIST_FILE = MissionControlDemo/Info.plist; LD_RUNPATH_SEARCH_PATHS = "$(inherited) @executable_path/Frameworks"; PRODUCT_BUNDLE_IDENTIFIER = com.appculture.MissionControlDemo; PRODUCT_NAME = MissionControlDemo; + SWIFT_VERSION = 3.0; }; name = Debug; }; 8B464DE01CE3742F00BAE834 /* Release */ = { isa = XCBuildConfiguration; buildSettings = { + ALWAYS_EMBED_SWIFT_STANDARD_LIBRARIES = YES; ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon; - EMBEDDED_CONTENT_CONTAINS_SWIFT = YES; INFOPLIST_FILE = MissionControlDemo/Info.plist; LD_RUNPATH_SEARCH_PATHS = "$(inherited) @executable_path/Frameworks"; PRODUCT_BUNDLE_IDENTIFIER = com.appculture.MissionControlDemo; PRODUCT_NAME = MissionControlDemo; + SWIFT_OPTIMIZATION_LEVEL = "-Owholemodule"; + SWIFT_VERSION = 3.0; }; name = Release; }; diff --git a/Example/MissionControlDemo.xcodeproj/xcshareddata/xcschemes/MissionControlDemo.xcscheme b/Example/MissionControlDemo.xcodeproj/xcshareddata/xcschemes/MissionControlDemo.xcscheme index c1a3774..6c3d035 100644 --- a/Example/MissionControlDemo.xcodeproj/xcshareddata/xcschemes/MissionControlDemo.xcscheme +++ b/Example/MissionControlDemo.xcodeproj/xcshareddata/xcschemes/MissionControlDemo.xcscheme @@ -1,6 +1,6 @@ Bool { - - let url = NSURL(string: "http://private-83024-missioncontrol5.apiary-mock.com/mission-control/launch-config")! + + func application(_ application: UIApplication, + didFinishLaunchingWithOptions launchOptions: [UIApplicationLaunchOptionsKey : Any]? = nil) -> Bool { + let url = URL(string: "http://private-83024-missioncontrol5.apiary-mock.com/mission-control/launch-config")! MissionControl.launch(remoteConfigURL: url) return true } - func applicationWillEnterForeground(application: UIApplication) { + func applicationWillEnterForeground(_ application: UIApplication) { MissionControl.refresh() } - func applicationDidBecomeActive(application: UIApplication) { + func applicationDidBecomeActive(_ application: UIApplication) { MissionControl.refresh() } diff --git a/Example/MissionControlDemo/BaseLaunchView.swift b/Example/MissionControlDemo/BaseLaunchView.swift index 627c750..b25320b 100644 --- a/Example/MissionControlDemo/BaseLaunchView.swift +++ b/Example/MissionControlDemo/BaseLaunchView.swift @@ -44,35 +44,35 @@ class BaseLaunchView: UIView { // MARK: - Properties - var didTapButtonAction: ((sender: AnyObject) -> Void)? + var didTapButtonAction: ((_ sender: AnyObject) -> Void)? var padding: CGFloat = 24.0 - var buttonHighlightColor = UIColor.lightGrayColor() - var buttonColor = UIColor.whiteColor() { + var buttonHighlightColor = UIColor.lightGray + var buttonColor = UIColor.white { didSet { button.backgroundColor = buttonColor } } - var buttonTitleColor = UIColor.darkGrayColor() { + var buttonTitleColor = UIColor.darkGray { didSet { buttonTitle.textColor = buttonTitleColor } } - var statusLightColor = UIColor.darkGrayColor() { + var statusLightColor = UIColor.darkGray { didSet { statusLight.backgroundColor = statusLightColor } } - var statusTitleColor = UIColor.whiteColor() { + var statusTitleColor = UIColor.white { didSet { statusTitle.textColor = statusTitleColor - statusLight.layer.borderColor = statusTitleColor.CGColor + statusLight.layer.borderColor = statusTitleColor.cgColor } } - var countdownColor = UIColor.whiteColor() { + var countdownColor = UIColor.white { didSet { countdown.textColor = countdownColor } @@ -91,7 +91,7 @@ class BaseLaunchView: UIView { } init() { - super.init(frame: CGRectZero) + super.init(frame: CGRect.zero) commonInit() } @@ -103,56 +103,56 @@ class BaseLaunchView: UIView { // MARK: - Override - override func layoutSublayersOfLayer(layer: CALayer) { - super.layoutSublayersOfLayer(layer) + override func layoutSublayers(of layer: CALayer) { + super.layoutSublayers(of: layer) gradientLayer.frame = gradient.bounds } - override func touchesBegan(touches: Set, withEvent event: UIEvent?) { - super.touchesBegan(touches, withEvent: event) + override func touchesBegan(_ touches: Set, with event: UIEvent?) { + super.touchesBegan(touches, with: event) if touchesInsideView(touches, view: button) { highlightButton() } } - override func touchesMoved(touches: Set, withEvent event: UIEvent?) { - super.touchesMoved(touches, withEvent: event) + override func touchesMoved(_ touches: Set, with event: UIEvent?) { + super.touchesMoved(touches, with: event) if !touchesInsideView(touches, view: button) { restoreButton() } } - override func touchesEnded(touches: Set, withEvent event: UIEvent?) { - super.touchesEnded(touches, withEvent: event) + override func touchesEnded(_ touches: Set, with event: UIEvent?) { + super.touchesEnded(touches, with: event) if touchesInsideView(touches, view: button) { restoreButton() if let action = didTapButtonAction { - action(sender: button) + action(button) } } } - private func touchesInsideView(touches: Set, view: UIView) -> Bool { + private func touchesInsideView(_ touches: Set, view: UIView) -> Bool { guard let touch = touches.first else { return false } - let location = touch.locationInView(view) - let insideView = CGRectContainsPoint(view.bounds, location) + let location = touch.location(in: view) + let insideView = view.bounds.contains(location) return insideView } private func highlightButton() { - UIView.animateWithDuration(0.2, animations: { [unowned self] in + UIView.animate(withDuration: 0.2, animations: { [unowned self] in self.button.backgroundColor = self.buttonHighlightColor - self.buttonImage.transform = CGAffineTransformMakeRotation(CGFloat(M_PI_2)) + self.buttonImage.transform = CGAffineTransform(rotationAngle: CGFloat(M_PI_2)) }) } private func restoreButton() { - UIView.animateWithDuration(0.2, animations: { [unowned self] in + UIView.animate(withDuration: 0.2, animations: { [unowned self] in self.button.backgroundColor = self.buttonColor - self.buttonImage.transform = CGAffineTransformIdentity + self.buttonImage.transform = CGAffineTransform.identity }) } @@ -167,10 +167,10 @@ class BaseLaunchView: UIView { private func configureGradient() { gradient.translatesAutoresizingMaskIntoConstraints = false - gradient.layer.insertSublayer(gradientLayer, atIndex: 0) + gradient.layer.insertSublayer(gradientLayer, at: 0) - gradientLayer.colors = [UIColor.orangeColor().CGColor, UIColor.blueColor().CGColor] - gradientLayer.contentsScale = UIScreen.mainScreen().scale + gradientLayer.colors = [UIColor.orange.cgColor, UIColor.blue.cgColor] + gradientLayer.contentsScale = UIScreen.main.scale gradientLayer.drawsAsynchronously = true gradientLayer.needsDisplayOnBoundsChange = true gradientLayer.setNeedsDisplay() @@ -179,33 +179,33 @@ class BaseLaunchView: UIView { private func configureButton() { button.translatesAutoresizingMaskIntoConstraints = false button.backgroundColor = buttonColor - button.layer.borderColor = statusLightColor.CGColor + button.layer.borderColor = statusLightColor.cgColor button.layer.borderWidth = 10.0 button.layer.cornerRadius = 10.0 button.clipsToBounds = true buttonImage.translatesAutoresizingMaskIntoConstraints = false - buttonImage.contentMode = .ScaleAspectFill + buttonImage.contentMode = .scaleAspectFill buttonImage.image = UIImage(named: "appculture") buttonTitle.translatesAutoresizingMaskIntoConstraints = false buttonTitle.adjustsFontSizeToFitWidth = true - buttonTitle.textAlignment = .Center + buttonTitle.textAlignment = .center buttonTitle.textColor = buttonTitleColor buttonTitle.text = "BUTTON" } private func configureStatus() { statusTitle.translatesAutoresizingMaskIntoConstraints = false - statusTitle.setContentHuggingPriority(251.0, forAxis: .Vertical) + statusTitle.setContentHuggingPriority(251.0, for: .vertical) statusTitle.adjustsFontSizeToFitWidth = true - statusTitle.textAlignment = .Center + statusTitle.textAlignment = .center statusTitle.textColor = statusTitleColor statusTitle.text = "STATUS" statusLight.translatesAutoresizingMaskIntoConstraints = false statusLight.backgroundColor = statusLightColor - statusLight.layer.borderColor = statusTitleColor.CGColor + statusLight.layer.borderColor = statusTitleColor.cgColor statusLight.layer.borderWidth = 2.0 statusLight.layer.cornerRadius = 16.0 } @@ -213,7 +213,7 @@ class BaseLaunchView: UIView { private func configureCountdown() { countdown.translatesAutoresizingMaskIntoConstraints = false countdown.adjustsFontSizeToFitWidth = true - countdown.textAlignment = .Center + countdown.textAlignment = .center countdown.textColor = countdownColor countdown.text = "00" } @@ -249,64 +249,64 @@ class BaseLaunchView: UIView { } private var gradientConstraints: [NSLayoutConstraint] { - let leading = gradient.leadingAnchor.constraintEqualToAnchor(leadingAnchor) - let trailing = gradient.trailingAnchor.constraintEqualToAnchor(trailingAnchor) - let top = gradient.topAnchor.constraintEqualToAnchor(topAnchor) - let bottom = gradient.bottomAnchor.constraintEqualToAnchor(bottomAnchor) + let leading = gradient.leadingAnchor.constraint(equalTo: leadingAnchor) + let trailing = gradient.trailingAnchor.constraint(equalTo: trailingAnchor) + let top = gradient.topAnchor.constraint(equalTo: topAnchor) + let bottom = gradient.bottomAnchor.constraint(equalTo: bottomAnchor) return [leading, trailing, top, bottom] } private var buttonConstraints: [NSLayoutConstraint] { - let leading = button.leadingAnchor.constraintEqualToAnchor(leadingAnchor, constant: padding) - let trailing = button.trailingAnchor.constraintEqualToAnchor(trailingAnchor, constant: -padding) - let bottom = button.bottomAnchor.constraintEqualToAnchor(bottomAnchor, constant: -padding) - let height = button.heightAnchor.constraintEqualToConstant(90.0) + let leading = button.leadingAnchor.constraint(equalTo: leadingAnchor, constant: padding) + let trailing = button.trailingAnchor.constraint(equalTo: trailingAnchor, constant: -padding) + let bottom = button.bottomAnchor.constraint(equalTo: bottomAnchor, constant: -padding) + let height = button.heightAnchor.constraint(equalToConstant: 90.0) return [leading, trailing, bottom, height] } private var buttonImageConstraints: [NSLayoutConstraint] { - let leading = buttonImage.leadingAnchor.constraintEqualToAnchor(button.leadingAnchor, constant: 20.0) - let top = buttonImage.topAnchor.constraintEqualToAnchor(button.topAnchor, constant: 22.0) - let bottom = buttonImage.bottomAnchor.constraintEqualToAnchor(button.bottomAnchor, constant: -22.0) - let width = buttonImage.widthAnchor.constraintEqualToAnchor(buttonImage.heightAnchor) + let leading = buttonImage.leadingAnchor.constraint(equalTo: button.leadingAnchor, constant: 20.0) + let top = buttonImage.topAnchor.constraint(equalTo: button.topAnchor, constant: 22.0) + let bottom = buttonImage.bottomAnchor.constraint(equalTo: button.bottomAnchor, constant: -22.0) + let width = buttonImage.widthAnchor.constraint(equalTo: buttonImage.heightAnchor) return [leading, top, bottom, width] } private var buttonTitleConstraints: [NSLayoutConstraint] { - let leading = buttonTitle.leadingAnchor.constraintEqualToAnchor(buttonImage.trailingAnchor, constant: 12.0) - let trailing = buttonTitle.trailingAnchor.constraintEqualToAnchor(button.trailingAnchor, constant: -22.0) - let centerY = buttonTitle.centerYAnchor.constraintEqualToAnchor(button.centerYAnchor) + let leading = buttonTitle.leadingAnchor.constraint(equalTo: buttonImage.trailingAnchor, constant: 12.0) + let trailing = buttonTitle.trailingAnchor.constraint(equalTo: button.trailingAnchor, constant: -22.0) + let centerY = buttonTitle.centerYAnchor.constraint(equalTo: button.centerYAnchor) return [leading, trailing, centerY] } private var statusTitleConstraints: [NSLayoutConstraint] { - let leading = statusTitle.leadingAnchor.constraintEqualToAnchor(button.leadingAnchor) - let trailing = statusTitle.trailingAnchor.constraintEqualToAnchor(button.trailingAnchor) - let bottom = statusTitle.bottomAnchor.constraintEqualToAnchor(button.topAnchor, constant: -padding) + let leading = statusTitle.leadingAnchor.constraint(equalTo: button.leadingAnchor) + let trailing = statusTitle.trailingAnchor.constraint(equalTo: button.trailingAnchor) + let bottom = statusTitle.bottomAnchor.constraint(equalTo: button.topAnchor, constant: -padding) return [leading, trailing, bottom] } private var statusLightConstraints: [NSLayoutConstraint] { - let centerX = statusLight.centerXAnchor.constraintEqualToAnchor(centerXAnchor) - let bottom = statusLight.bottomAnchor.constraintEqualToAnchor(statusTitle.topAnchor, constant: -padding) - let width = statusLight.widthAnchor.constraintEqualToConstant(32.0) - let height = statusLight.heightAnchor.constraintEqualToConstant(32.0) + let centerX = statusLight.centerXAnchor.constraint(equalTo: centerXAnchor) + let bottom = statusLight.bottomAnchor.constraint(equalTo: statusTitle.topAnchor, constant: -padding) + let width = statusLight.widthAnchor.constraint(equalToConstant: 32.0) + let height = statusLight.heightAnchor.constraint(equalToConstant: 32.0) return [centerX, bottom, width, height] } private var countdownConstraints: [NSLayoutConstraint] { - let leading = countdown.leadingAnchor.constraintEqualToAnchor(leadingAnchor) - let trailing = countdown.trailingAnchor.constraintEqualToAnchor(trailingAnchor) - let top = countdown.topAnchor.constraintEqualToAnchor(topAnchor) - let bottom = countdown.bottomAnchor.constraintEqualToAnchor(statusLight.topAnchor) + let leading = countdown.leadingAnchor.constraint(equalTo: leadingAnchor) + let trailing = countdown.trailingAnchor.constraint(equalTo: trailingAnchor) + let top = countdown.topAnchor.constraint(equalTo: topAnchor) + let bottom = countdown.bottomAnchor.constraint(equalTo: statusLight.topAnchor) return [leading, trailing, top, bottom] } // MARK: - Interface Builder override func prepareForInterfaceBuilder() { - let bundle = NSBundle(forClass: self.dynamicType) - let image = UIImage(named: "appculture", inBundle: bundle, compatibleWithTraitCollection: traitCollection) + let bundle = Bundle(for: type(of: self)) + let image = UIImage(named: "appculture", in: bundle, compatibleWith: traitCollection) buttonImage.image = image } @@ -319,12 +319,12 @@ extension UIColor { convenience init (hex: String) { var colorString: String = hex if (hex.hasPrefix("#")) { - let index = hex.startIndex.advancedBy(1) - colorString = colorString.substringFromIndex(index) + let index = hex.characters.index(hex.startIndex, offsetBy: 1) + colorString = colorString.substring(from: index) } var rgbValue:UInt32 = 0 - NSScanner(string: colorString).scanHexInt(&rgbValue) + Scanner(string: colorString).scanHexInt32(&rgbValue) self.init( red: CGFloat((rgbValue & 0xFF0000) >> 16) / 255.0, diff --git a/Example/MissionControlDemo/LaunchBrain.swift b/Example/MissionControlDemo/LaunchBrain.swift index 7c1b5f5..64e53be 100644 --- a/Example/MissionControlDemo/LaunchBrain.swift +++ b/Example/MissionControlDemo/LaunchBrain.swift @@ -58,7 +58,7 @@ class LaunchBrain: MissionControlDelegate { } } - var timer: NSTimer? + var timer: Timer? private var launchForce: Double { return 1.0 - ConfigDouble("LaunchForce", fallback: 0.5) @@ -81,12 +81,12 @@ class LaunchBrain: MissionControlDelegate { // MARK: - MissionControlDelegate - func missionControlDidRefreshConfig(old old: [String : AnyObject]?, new: [String : AnyObject]) { + func missionControlDidRefreshConfig(old: [String : Any]?, new: [String : Any]) { print("missionControlDidRefreshConfig") updateUIForState(state) } - func missionControlDidFailRefreshingConfig(error error: ErrorType) { + func missionControlDidFailRefreshingConfig(error: Error) { print("missionControlDidFailRefreshingConfig") stopCountdown() @@ -101,7 +101,7 @@ class LaunchBrain: MissionControlDelegate { // MARK: - Actions - func didTapButton(sender: AnyObject) { + func didTapButton(_ sender: AnyObject) { switch state { case .Offline: ConfigBoolForce("Ready", fallback: false, completion: { (forced) in @@ -126,7 +126,7 @@ class LaunchBrain: MissionControlDelegate { updateUIForState(state) } - private func updateUIForState(state: LaunchState) { + private func updateUIForState(_ state: LaunchState) { updateUIForAnyState(state) switch state { @@ -147,16 +147,16 @@ class LaunchBrain: MissionControlDelegate { } } - private func updateUIForAnyState(state: LaunchState) { + private func updateUIForAnyState(_ state: LaunchState) { let color1 = UIColor(hex: ConfigString("TopColor", fallback: "#000000")) let color2 = UIColor(hex: ConfigString("BottomColor", fallback: "#4A90E2")) - view.gradientLayer.colors = [color1.CGColor, color2.CGColor] + view.gradientLayer.colors = [color1.cgColor, color2.cgColor] - view.button.layer.borderColor = colorForState(state).CGColor + view.button.layer.borderColor = colorForState(state).cgColor view.buttonTitle.text = commandForState(state) view.stopBlinkingStatusLight() - view.statusTitle.text = "STATUS: \(state.rawValue.capitalizedString)" + view.statusTitle.text = "STATUS: \(state.rawValue.capitalized)" view.statusLightOnColor = colorForState(state) view.statusLightOn = true @@ -167,7 +167,7 @@ class LaunchBrain: MissionControlDelegate { view.stopAnimatingGradient() view.stopRotatingButtonImage() - view.button.layer.borderColor = view.statusLightOffColor.CGColor + view.button.layer.borderColor = view.statusLightOffColor.cgColor view.countdown.alpha = 0.1 seconds = 0 view.startBlinkingStatusLight(timeInterval: 0.5) @@ -204,7 +204,7 @@ class LaunchBrain: MissionControlDelegate { view.startBlinkingStatusLight(timeInterval: 0.25) } - private func commandForState(state: LaunchState) -> String { + private func commandForState(_ state: LaunchState) -> String { switch state { case .Offline: return "CONNECT" @@ -217,7 +217,7 @@ class LaunchBrain: MissionControlDelegate { } } - private func colorForState(state: LaunchState) -> UIColor { + private func colorForState(_ state: LaunchState) -> UIColor { switch state { case .Offline: return UIColor(hex: ConfigString("OfflineColor", fallback: "#F8E71C")) @@ -238,7 +238,7 @@ class LaunchBrain: MissionControlDelegate { private func startCountdown() { if timer == nil { - timer = NSTimer.scheduledTimerWithTimeInterval(1.0, + timer = Timer.scheduledTimer(timeInterval: 1.0, target: self, selector: #selector(timerTick(_:)), userInfo: nil, repeats: true) @@ -250,7 +250,7 @@ class LaunchBrain: MissionControlDelegate { timer = nil } - @objc func timerTick(sender: NSTimer) { + @objc func timerTick(_ sender: Timer) { ConfigBoolForce("Abort", fallback: true) { (forced) in if forced { self.stopCountdown() diff --git a/Example/MissionControlDemo/LaunchView.swift b/Example/MissionControlDemo/LaunchView.swift index 9a8de04..93cb596 100644 --- a/Example/MissionControlDemo/LaunchView.swift +++ b/Example/MissionControlDemo/LaunchView.swift @@ -29,17 +29,17 @@ class LaunchView: BaseLaunchView { // MARK: - Properties - var blinkTimer: NSTimer? + var blinkTimer: Timer? var statusLightOffColor = UIColor(hex: "#4A4A4A") - var statusLightOnColor = UIColor.whiteColor() + var statusLightOnColor = UIColor.white var statusLightOn = false { didSet { if statusLightOn { - UIView.animateWithDuration(0.2) { + UIView.animate(withDuration: 0.2) { self.turnStatusLightOn() } } else { - UIView.animateWithDuration(0.2) { + UIView.animate(withDuration: 0.2) { self.turnStatusLightOff() } } @@ -59,13 +59,13 @@ class LaunchView: BaseLaunchView { private func configureDefaultUI() { padding = 24.0 - gradientLayer.colors = [UIColor(hex: "#000000").CGColor, UIColor(hex: "#4A90E2").CGColor] + gradientLayer.colors = [UIColor(hex: "#000000").cgColor, UIColor(hex: "#4A90E2").cgColor] gradientLayer.locations = [0.0, 1.0] - buttonColor = UIColor.whiteColor() + buttonColor = UIColor.white buttonHighlightColor = UIColor(hex: "#E4F6F6") - statusTitleColor = UIColor.whiteColor() - countdownColor = UIColor.whiteColor() + statusTitleColor = UIColor.white + countdownColor = UIColor.white buttonTitle.font = UIFont(name: "AvenirNext-Heavy", size: 36.0) statusTitle.font = UIFont(name: "Nasa-Display", size: 40.0) @@ -74,8 +74,8 @@ class LaunchView: BaseLaunchView { // MARK: - Blink - func startBlinkingStatusLight(timeInterval timeInterval: NSTimeInterval) { - blinkTimer = NSTimer.scheduledTimerWithTimeInterval(timeInterval, + func startBlinkingStatusLight(timeInterval: TimeInterval) { + blinkTimer = Timer.scheduledTimer(timeInterval: timeInterval, target: self, selector: #selector(blinkStatusLight), userInfo: nil, repeats: true) @@ -93,7 +93,7 @@ class LaunchView: BaseLaunchView { func turnStatusLightOn() { statusLightColor = statusLightOnColor - statusLight.layer.shadowColor = statusLightOnColor.CGColor + statusLight.layer.shadowColor = statusLightOnColor.cgColor statusLight.layer.shadowOffset = CGSize(width: 0.0, height: 0.0) statusLight.layer.shadowOpacity = 1.0 statusLight.layer.shadowRadius = 5.0 @@ -106,7 +106,7 @@ class LaunchView: BaseLaunchView { // MARK: - Button Image Rotation - func rotateButtonImageWithDuration(duration: Double) { + func rotateButtonImageWithDuration(_ duration: Double) { buttonImage.rotate(withDuration: duration) } @@ -116,7 +116,7 @@ class LaunchView: BaseLaunchView { // MARK: - Gradient Animation - func animateGradientWithDuration(duration: Double) { + func animateGradientWithDuration(_ duration: Double) { animateGradientLayer(gradientLayer, withDuration: duration) } @@ -131,7 +131,7 @@ private extension UIView { @nonobjc static let rotationKey = "AERotation" func rotate(withDuration duration: Double = 1.0) { - if layer.animationForKey(UIView.rotationKey) == nil { + if layer.animation(forKey: UIView.rotationKey) == nil { let rotationAnimation = CABasicAnimation(keyPath: "transform.rotation") rotationAnimation.fromValue = 0.0 @@ -139,18 +139,18 @@ private extension UIView { rotationAnimation.duration = duration rotationAnimation.repeatCount = Float.infinity - layer.addAnimation(rotationAnimation, forKey: UIView.rotationKey) + layer.add(rotationAnimation, forKey: UIView.rotationKey) } } func stopRotation() { - layer.removeAnimationForKey(UIView.rotationKey) + layer.removeAnimation(forKey: UIView.rotationKey) } @nonobjc static let gradientKey = "AEGradientAnimation" - func animateGradientLayer(gradientLayer: CAGradientLayer, withDuration duration: Double = 2.0) { - if gradientLayer.animationForKey(UIView.gradientKey) == nil { + func animateGradientLayer(_ gradientLayer: CAGradientLayer, withDuration duration: Double = 2.0) { + if gradientLayer.animation(forKey: UIView.gradientKey) == nil { let sequenceDuration = duration / 4.0 let currentLocations = [0.0, 1.0] @@ -171,9 +171,9 @@ private extension UIView { let colorAnimation1 = CABasicAnimation(keyPath: "colors") colorAnimation1.fromValue = [color1, color1] - colorAnimation1.toValue = gradientLayer.colors?.reverse() + colorAnimation1.toValue = gradientLayer.colors?.reversed() colorAnimation1.duration = sequenceDuration - colorAnimation1.removedOnCompletion = false + colorAnimation1.isRemovedOnCompletion = false colorAnimation1.fillMode = kCAFillModeForwards colorAnimation1.beginTime = sequenceDuration @@ -191,7 +191,7 @@ private extension UIView { colorAnimation2.fromValue = [color2, color2] colorAnimation2.toValue = gradientLayer.colors colorAnimation2.duration = sequenceDuration - colorAnimation2.removedOnCompletion = false + colorAnimation2.isRemovedOnCompletion = false colorAnimation2.fillMode = kCAFillModeForwards colorAnimation2.beginTime = 3 * sequenceDuration @@ -202,12 +202,12 @@ private extension UIView { group.animations = [locationAnimation1, colorAnimation1, locationAnimation2, colorAnimation2] group.repeatCount = Float.infinity - gradientLayer.addAnimation(group, forKey: UIView.gradientKey) + gradientLayer.add(group, forKey: UIView.gradientKey) } } - func stopGradientAnimation(gradientLayer: CAGradientLayer) { - gradientLayer.removeAnimationForKey(UIView.gradientKey) + func stopGradientAnimation(_ gradientLayer: CAGradientLayer) { + gradientLayer.removeAnimation(forKey: UIView.gradientKey) } } diff --git a/Example/MissionControlDemo/LaunchViewController.swift b/Example/MissionControlDemo/LaunchViewController.swift index 9883266..e17910d 100644 --- a/Example/MissionControlDemo/LaunchViewController.swift +++ b/Example/MissionControlDemo/LaunchViewController.swift @@ -39,8 +39,8 @@ class LaunchViewController: UIViewController, LaunchDelegate { launch = LaunchBrain(view: launchView, delegate: self) } - - override func prefersStatusBarHidden() -> Bool { + + override var prefersStatusBarHidden: Bool { return true } diff --git a/MissionControl.xcodeproj/project.pbxproj b/MissionControl.xcodeproj/project.pbxproj index 7204d49..f94af5c 100644 --- a/MissionControl.xcodeproj/project.pbxproj +++ b/MissionControl.xcodeproj/project.pbxproj @@ -21,6 +21,12 @@ 8B6313861CE5F9A10029DC98 /* MissionControl.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 8B63137B1CE5F9A10029DC98 /* MissionControl.framework */; }; 8B63138B1CE5F9A10029DC98 /* MissionControlTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 8B63138A1CE5F9A10029DC98 /* MissionControlTests.swift */; }; 8B6313961CE5FA2B0029DC98 /* MissionControl.swift in Sources */ = {isa = PBXBuildFile; fileRef = 8B6313951CE5FA2B0029DC98 /* MissionControl.swift */; }; + 8BFA21AE1DB66DBB00CAB201 /* Brain.swift in Sources */ = {isa = PBXBuildFile; fileRef = 8BFA21AD1DB66DBB00CAB201 /* Brain.swift */; }; + 8BFA21AF1DB66DBB00CAB201 /* Brain.swift in Sources */ = {isa = PBXBuildFile; fileRef = 8BFA21AD1DB66DBB00CAB201 /* Brain.swift */; }; + 8BFA21B01DB66DBB00CAB201 /* Brain.swift in Sources */ = {isa = PBXBuildFile; fileRef = 8BFA21AD1DB66DBB00CAB201 /* Brain.swift */; }; + 8BFA21B21DB66DC700CAB201 /* Accessors.swift in Sources */ = {isa = PBXBuildFile; fileRef = 8BFA21B11DB66DC700CAB201 /* Accessors.swift */; }; + 8BFA21B31DB66DC700CAB201 /* Accessors.swift in Sources */ = {isa = PBXBuildFile; fileRef = 8BFA21B11DB66DC700CAB201 /* Accessors.swift */; }; + 8BFA21B41DB66DC700CAB201 /* Accessors.swift in Sources */ = {isa = PBXBuildFile; fileRef = 8BFA21B11DB66DC700CAB201 /* Accessors.swift */; }; /* End PBXBuildFile section */ /* Begin PBXContainerItemProxy section */ @@ -65,6 +71,8 @@ 8B63138A1CE5F9A10029DC98 /* MissionControlTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MissionControlTests.swift; sourceTree = ""; }; 8B63138C1CE5F9A10029DC98 /* Info.plist */ = {isa = PBXFileReference; lastKnownFileType = text.plist.xml; path = Info.plist; sourceTree = ""; }; 8B6313951CE5FA2B0029DC98 /* MissionControl.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = MissionControl.swift; sourceTree = ""; }; + 8BFA21AD1DB66DBB00CAB201 /* Brain.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = Brain.swift; sourceTree = ""; }; + 8BFA21B11DB66DC700CAB201 /* Accessors.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = Accessors.swift; sourceTree = ""; }; /* End PBXFileReference section */ /* Begin PBXFrameworksBuildPhase section */ @@ -163,6 +171,8 @@ isa = PBXGroup; children = ( 8B6313951CE5FA2B0029DC98 /* MissionControl.swift */, + 8BFA21B11DB66DC700CAB201 /* Accessors.swift */, + 8BFA21AD1DB66DBB00CAB201 /* Brain.swift */, 8B6313971CE5FA3C0029DC98 /* Supporting Files */, ); path = Sources; @@ -365,29 +375,36 @@ isa = PBXProject; attributes = { LastSwiftUpdateCheck = 0730; - LastUpgradeCheck = 0730; + LastUpgradeCheck = 0810; ORGANIZATIONNAME = appculture; TargetAttributes = { 8B03C1DF1CF5E10500B09B48 = { CreatedOnToolsVersion = 7.3.1; + LastSwiftMigration = 0800; }; 8B03C1EE1CF5E1DD00B09B48 = { CreatedOnToolsVersion = 7.3.1; + LastSwiftMigration = 0800; }; 8B03C1F71CF5E1DD00B09B48 = { CreatedOnToolsVersion = 7.3.1; + LastSwiftMigration = 0800; }; 8B03C20D1CF5E28C00B09B48 = { CreatedOnToolsVersion = 7.3.1; + LastSwiftMigration = 0800; }; 8B03C2161CF5E28C00B09B48 = { CreatedOnToolsVersion = 7.3.1; + LastSwiftMigration = 0800; }; 8B63137A1CE5F9A10029DC98 = { CreatedOnToolsVersion = 7.3.1; + LastSwiftMigration = 1020; }; 8B6313841CE5F9A10029DC98 = { CreatedOnToolsVersion = 7.3.1; + LastSwiftMigration = 1020; }; }; }; @@ -396,6 +413,7 @@ developmentRegion = English; hasScannedForEncodings = 0; knownRegions = ( + English, en, ); mainGroup = 8B6313711CE5F9A10029DC98; @@ -471,6 +489,8 @@ isa = PBXSourcesBuildPhase; buildActionMask = 2147483647; files = ( + 8BFA21B31DB66DC700CAB201 /* Accessors.swift in Sources */, + 8BFA21AF1DB66DBB00CAB201 /* Brain.swift in Sources */, 8B03C1E81CF5E17F00B09B48 /* MissionControl.swift in Sources */, ); runOnlyForDeploymentPostprocessing = 0; @@ -479,6 +499,8 @@ isa = PBXSourcesBuildPhase; buildActionMask = 2147483647; files = ( + 8BFA21B41DB66DC700CAB201 /* Accessors.swift in Sources */, + 8BFA21B01DB66DBB00CAB201 /* Brain.swift in Sources */, 8B03C2061CF5E22E00B09B48 /* MissionControl.swift in Sources */, ); runOnlyForDeploymentPostprocessing = 0; @@ -511,6 +533,8 @@ isa = PBXSourcesBuildPhase; buildActionMask = 2147483647; files = ( + 8BFA21B21DB66DC700CAB201 /* Accessors.swift in Sources */, + 8BFA21AE1DB66DBB00CAB201 /* Brain.swift in Sources */, 8B6313961CE5FA2B0029DC98 /* MissionControl.swift in Sources */, ); runOnlyForDeploymentPostprocessing = 0; @@ -548,6 +572,7 @@ isa = XCBuildConfiguration; buildSettings = { APPLICATION_EXTENSION_API_ONLY = YES; + "CODE_SIGN_IDENTITY[sdk=watchos*]" = ""; DEFINES_MODULE = YES; DYLIB_COMPATIBILITY_VERSION = 1; DYLIB_CURRENT_VERSION = 1; @@ -559,6 +584,7 @@ PRODUCT_NAME = MissionControl; SDKROOT = watchos; SKIP_INSTALL = YES; + SWIFT_VERSION = 3.0; TARGETED_DEVICE_FAMILY = 4; }; name = Debug; @@ -567,6 +593,7 @@ isa = XCBuildConfiguration; buildSettings = { APPLICATION_EXTENSION_API_ONLY = YES; + "CODE_SIGN_IDENTITY[sdk=watchos*]" = ""; DEFINES_MODULE = YES; DYLIB_COMPATIBILITY_VERSION = 1; DYLIB_CURRENT_VERSION = 1; @@ -578,6 +605,8 @@ PRODUCT_NAME = MissionControl; SDKROOT = watchos; SKIP_INSTALL = YES; + SWIFT_OPTIMIZATION_LEVEL = "-Owholemodule"; + SWIFT_VERSION = 3.0; TARGETED_DEVICE_FAMILY = 4; }; name = Release; @@ -586,6 +615,7 @@ isa = XCBuildConfiguration; buildSettings = { APPLICATION_EXTENSION_API_ONLY = YES; + "CODE_SIGN_IDENTITY[sdk=appletvos*]" = ""; DEFINES_MODULE = YES; DYLIB_COMPATIBILITY_VERSION = 1; DYLIB_CURRENT_VERSION = 1; @@ -597,6 +627,7 @@ PRODUCT_NAME = MissionControl; SDKROOT = appletvos; SKIP_INSTALL = YES; + SWIFT_VERSION = 3.0; TARGETED_DEVICE_FAMILY = 3; }; name = Debug; @@ -605,6 +636,7 @@ isa = XCBuildConfiguration; buildSettings = { APPLICATION_EXTENSION_API_ONLY = YES; + "CODE_SIGN_IDENTITY[sdk=appletvos*]" = ""; DEFINES_MODULE = YES; DYLIB_COMPATIBILITY_VERSION = 1; DYLIB_CURRENT_VERSION = 1; @@ -616,6 +648,8 @@ PRODUCT_NAME = MissionControl; SDKROOT = appletvos; SKIP_INSTALL = YES; + SWIFT_OPTIMIZATION_LEVEL = "-Owholemodule"; + SWIFT_VERSION = 3.0; TARGETED_DEVICE_FAMILY = 3; }; name = Release; @@ -628,6 +662,7 @@ PRODUCT_BUNDLE_IDENTIFIER = "com.appculture.MissionControl-tvOSTests"; PRODUCT_NAME = "$(TARGET_NAME)"; SDKROOT = appletvos; + SWIFT_VERSION = 3.0; TVOS_DEPLOYMENT_TARGET = 9.2; }; name = Debug; @@ -640,6 +675,8 @@ PRODUCT_BUNDLE_IDENTIFIER = "com.appculture.MissionControl-tvOSTests"; PRODUCT_NAME = "$(TARGET_NAME)"; SDKROOT = appletvos; + SWIFT_OPTIMIZATION_LEVEL = "-Owholemodule"; + SWIFT_VERSION = 3.0; TVOS_DEPLOYMENT_TARGET = 9.2; }; name = Release; @@ -648,7 +685,7 @@ isa = XCBuildConfiguration; buildSettings = { APPLICATION_EXTENSION_API_ONLY = YES; - CODE_SIGN_IDENTITY = "-"; + CODE_SIGN_IDENTITY = ""; COMBINE_HIDPI_IMAGES = YES; DEFINES_MODULE = YES; DYLIB_COMPATIBILITY_VERSION = 1; @@ -663,6 +700,7 @@ PRODUCT_NAME = MissionControl; SDKROOT = macosx; SKIP_INSTALL = YES; + SWIFT_VERSION = 3.0; }; name = Debug; }; @@ -670,7 +708,7 @@ isa = XCBuildConfiguration; buildSettings = { APPLICATION_EXTENSION_API_ONLY = YES; - CODE_SIGN_IDENTITY = "-"; + CODE_SIGN_IDENTITY = ""; COMBINE_HIDPI_IMAGES = YES; DEFINES_MODULE = YES; DYLIB_COMPATIBILITY_VERSION = 1; @@ -685,6 +723,8 @@ PRODUCT_NAME = MissionControl; SDKROOT = macosx; SKIP_INSTALL = YES; + SWIFT_OPTIMIZATION_LEVEL = "-Owholemodule"; + SWIFT_VERSION = 3.0; }; name = Release; }; @@ -699,6 +739,7 @@ PRODUCT_BUNDLE_IDENTIFIER = "com.appculture.MissionControl-OSXTests"; PRODUCT_NAME = "$(TARGET_NAME)"; SDKROOT = macosx; + SWIFT_VERSION = 3.0; }; name = Debug; }; @@ -713,6 +754,8 @@ PRODUCT_BUNDLE_IDENTIFIER = "com.appculture.MissionControl-OSXTests"; PRODUCT_NAME = "$(TARGET_NAME)"; SDKROOT = macosx; + SWIFT_OPTIMIZATION_LEVEL = "-Owholemodule"; + SWIFT_VERSION = 3.0; }; name = Release; }; @@ -730,8 +773,10 @@ CLANG_WARN_DIRECT_OBJC_ISA_USAGE = YES_ERROR; CLANG_WARN_EMPTY_BODY = YES; CLANG_WARN_ENUM_CONVERSION = YES; + CLANG_WARN_INFINITE_RECURSION = YES; CLANG_WARN_INT_CONVERSION = YES; CLANG_WARN_OBJC_ROOT_CLASS = YES_ERROR; + CLANG_WARN_SUSPICIOUS_MOVE = YES; CLANG_WARN_UNREACHABLE_CODE = YES; CLANG_WARN__DUPLICATE_METHOD_MATCH = YES; "CODE_SIGN_IDENTITY[sdk=iphoneos*]" = "iPhone Developer"; @@ -781,8 +826,10 @@ CLANG_WARN_DIRECT_OBJC_ISA_USAGE = YES_ERROR; CLANG_WARN_EMPTY_BODY = YES; CLANG_WARN_ENUM_CONVERSION = YES; + CLANG_WARN_INFINITE_RECURSION = YES; CLANG_WARN_INT_CONVERSION = YES; CLANG_WARN_OBJC_ROOT_CLASS = YES_ERROR; + CLANG_WARN_SUSPICIOUS_MOVE = YES; CLANG_WARN_UNREACHABLE_CODE = YES; CLANG_WARN__DUPLICATE_METHOD_MATCH = YES; "CODE_SIGN_IDENTITY[sdk=iphoneos*]" = "iPhone Developer"; @@ -802,6 +849,7 @@ IPHONEOS_DEPLOYMENT_TARGET = 8.0; MTL_ENABLE_DEBUG_INFO = NO; SDKROOT = iphoneos; + SWIFT_OPTIMIZATION_LEVEL = "-Owholemodule"; TARGETED_DEVICE_FAMILY = "1,2"; TVOS_DEPLOYMENT_TARGET = 9.0; VALIDATE_PRODUCT = YES; @@ -816,6 +864,7 @@ buildSettings = { APPLICATION_EXTENSION_API_ONLY = YES; CLANG_ENABLE_MODULES = YES; + "CODE_SIGN_IDENTITY[sdk=iphoneos*]" = ""; DEFINES_MODULE = YES; DYLIB_COMPATIBILITY_VERSION = 1; DYLIB_CURRENT_VERSION = 1; @@ -827,6 +876,7 @@ PRODUCT_NAME = MissionControl; SKIP_INSTALL = YES; SWIFT_OPTIMIZATION_LEVEL = "-Onone"; + SWIFT_VERSION = 5.0; }; name = Debug; }; @@ -835,6 +885,7 @@ buildSettings = { APPLICATION_EXTENSION_API_ONLY = YES; CLANG_ENABLE_MODULES = YES; + "CODE_SIGN_IDENTITY[sdk=iphoneos*]" = ""; DEFINES_MODULE = YES; DYLIB_COMPATIBILITY_VERSION = 1; DYLIB_CURRENT_VERSION = 1; @@ -845,6 +896,8 @@ PRODUCT_BUNDLE_IDENTIFIER = "com.appculture.MissionControl-iOSTests"; PRODUCT_NAME = MissionControl; SKIP_INSTALL = YES; + SWIFT_OPTIMIZATION_LEVEL = "-Owholemodule"; + SWIFT_VERSION = 5.0; }; name = Release; }; @@ -855,6 +908,7 @@ LD_RUNPATH_SEARCH_PATHS = "$(inherited) @executable_path/Frameworks @loader_path/Frameworks"; PRODUCT_BUNDLE_IDENTIFIER = com.appculture.MissionControlTests; PRODUCT_NAME = "$(TARGET_NAME)"; + SWIFT_VERSION = 5.0; }; name = Debug; }; @@ -865,6 +919,8 @@ LD_RUNPATH_SEARCH_PATHS = "$(inherited) @executable_path/Frameworks @loader_path/Frameworks"; PRODUCT_BUNDLE_IDENTIFIER = com.appculture.MissionControlTests; PRODUCT_NAME = "$(TARGET_NAME)"; + SWIFT_OPTIMIZATION_LEVEL = "-Owholemodule"; + SWIFT_VERSION = 5.0; }; name = Release; }; @@ -878,6 +934,7 @@ 8B03C1E61CF5E10500B09B48 /* Release */, ); defaultConfigurationIsVisible = 0; + defaultConfigurationName = Release; }; 8B03C2001CF5E1DD00B09B48 /* Build configuration list for PBXNativeTarget "MissionControl tvOS" */ = { isa = XCConfigurationList; @@ -886,6 +943,7 @@ 8B03C2021CF5E1DD00B09B48 /* Release */, ); defaultConfigurationIsVisible = 0; + defaultConfigurationName = Release; }; 8B03C2031CF5E1DD00B09B48 /* Build configuration list for PBXNativeTarget "MissionControl tvOS Tests" */ = { isa = XCConfigurationList; @@ -894,6 +952,7 @@ 8B03C2051CF5E1DD00B09B48 /* Release */, ); defaultConfigurationIsVisible = 0; + defaultConfigurationName = Release; }; 8B03C21F1CF5E28C00B09B48 /* Build configuration list for PBXNativeTarget "MissionControl OSX" */ = { isa = XCConfigurationList; @@ -902,6 +961,7 @@ 8B03C2211CF5E28C00B09B48 /* Release */, ); defaultConfigurationIsVisible = 0; + defaultConfigurationName = Release; }; 8B03C2221CF5E28C00B09B48 /* Build configuration list for PBXNativeTarget "MissionControl OSX Tests" */ = { isa = XCConfigurationList; @@ -910,6 +970,7 @@ 8B03C2241CF5E28C00B09B48 /* Release */, ); defaultConfigurationIsVisible = 0; + defaultConfigurationName = Release; }; 8B6313751CE5F9A10029DC98 /* Build configuration list for PBXProject "MissionControl" */ = { isa = XCConfigurationList; diff --git a/MissionControl.xcodeproj/project.xcworkspace/xcshareddata/IDEWorkspaceChecks.plist b/MissionControl.xcodeproj/project.xcworkspace/xcshareddata/IDEWorkspaceChecks.plist new file mode 100644 index 0000000..18d9810 --- /dev/null +++ b/MissionControl.xcodeproj/project.xcworkspace/xcshareddata/IDEWorkspaceChecks.plist @@ -0,0 +1,8 @@ + + + + + IDEDidComputeMac32BitWarning + + + diff --git a/MissionControl.xcodeproj/project.xcworkspace/xcuserdata/lex.xcuserdatad/UserInterfaceState.xcuserstate b/MissionControl.xcodeproj/project.xcworkspace/xcuserdata/lex.xcuserdatad/UserInterfaceState.xcuserstate new file mode 100644 index 0000000..ed436ce Binary files /dev/null and b/MissionControl.xcodeproj/project.xcworkspace/xcuserdata/lex.xcuserdatad/UserInterfaceState.xcuserstate differ diff --git a/MissionControl.xcodeproj/xcshareddata/xcschemes/MissionControl OSX.xcscheme b/MissionControl.xcodeproj/xcshareddata/xcschemes/MissionControl OSX.xcscheme index 1a31318..2ddd393 100644 --- a/MissionControl.xcodeproj/xcshareddata/xcschemes/MissionControl OSX.xcscheme +++ b/MissionControl.xcodeproj/xcshareddata/xcschemes/MissionControl OSX.xcscheme @@ -1,6 +1,6 @@ + + + + SuppressBuildableAutocreation + + 8B03C1DF1CF5E10500B09B48 + + primary + + + 8B03C1EE1CF5E1DD00B09B48 + + primary + + + 8B03C1F71CF5E1DD00B09B48 + + primary + + + 8B03C20D1CF5E28C00B09B48 + + primary + + + 8B03C2161CF5E28C00B09B48 + + primary + + + 8B63137A1CE5F9A10029DC98 + + primary + + + 8B6313841CE5F9A10029DC98 + + primary + + + + + diff --git a/README.md b/README.md index f6ab8bb..5a0e0be 100644 --- a/README.md +++ b/README.md @@ -1,7 +1,7 @@ # Mission Control **Super powerful remote config utility written in Swift (iOS, watchOS, tvOS, OSX)** -[![Language Swift 2.2](https://img.shields.io/badge/Language-Swift%202.2-orange.svg?style=flat)](https://swift.org) +[![Language Swift 2.3](https://img.shields.io/badge/Language-Swift%202.3-orange.svg?style=flat)](https://swift.org) [![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) [![License MIT](https://img.shields.io/badge/License-MIT-lightgrey.svg?style=flat)](https://github.com/appculture/MissionControl-iOS/blob/master/LICENSE) @@ -193,7 +193,7 @@ Just remember to pass your URL to **MissionControl** `launch:` method. ### So, are you ready for the "Real Time" apps?! [We are](http://appculture.com). ## Requirements -- Xcode 7.3+ +- Xcode 8.0+ - iOS 8.0+ ## Installation diff --git a/Sources/Accessors.swift b/Sources/Accessors.swift new file mode 100644 index 0000000..7f6c577 --- /dev/null +++ b/Sources/Accessors.swift @@ -0,0 +1,171 @@ +// +// Accessors.swift +// +// Copyright (c) 2016 appculture http://appculture.com +// +// Permission is hereby granted, free of charge, to any person obtaining a copy +// of this software and associated documentation files (the "Software"), to deal +// in the Software without restriction, including without limitation the rights +// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +// copies of the Software, and to permit persons to whom the Software is +// furnished to do so, subject to the following conditions: +// +// The above copyright notice and this permission notice shall be included in all +// copies or substantial portions of the Software. +// +// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +// SOFTWARE. +// + +import Foundation + +/** + Accessor for retreiving setting of generic type `T` for given key. + + This method will resolve to proper setting by following this priority order: + 1. Remote setting from memory (received in the last refresh). + 2. Remote setting from disk cache (if never refreshed in current app session (ex. offline)). + 3. Local setting from disk (defaults provided in `localConfig` on MissionControl `launch`). + 4. Provided fallback value (if provided) + + - parameter key: Key for the setting. + - parameter fallback: Fallback value if setting is not available in any config. + + - returns: Resolved setting of generic type `T` for given key. +*/ +public func ConfigGeneric(_ key: String, fallback: T) -> T { + if let remoteValue = ACMissionControl.shared.remoteConfig?[key] as? T { + return remoteValue + } else if let cachedValue = ACMissionControl.shared.cachedConfig?[key] as? T { + return cachedValue + } else if let localValue = ACMissionControl.shared.localConfig?[key] as? T { + return localValue + } else { + return fallback + } +} + +/** + Async "Force Remote" Accessor for retreiving the latest setting of generic type `T` for given key. + + This method will first call `refresh` method after which it will evaluate its success. + + If `refresh` was successful, it will call normal accessor of generic type `T` for given key, + which will by its priority order resolve to the latest remote value as a parameter inside `completion` handler. + + If `refresh` fails, it will return provided `fallback` value as a parameter inside `completion` block. + + - parameter key: Key for the setting. + - parameter fallback: Fallback value of generic type `T` if refresh is not successful. +*/ +public func ConfigGenericForce(_ key: String, fallback: T, completion: @escaping ((_ forced: T) -> Void)) { + MissionControl.refresh({ (innerBlock) in + do { + let _ = try innerBlock() + completion(ConfigGeneric(key, fallback: fallback)) + } catch { + completion(fallback) + } + }) +} + +/** + Accessor helper for retreiving setting of type `Bool` for given key. + It will call `ConfigGeneric` with `Bool` type. + + - parameter key: Key for the setting. + - parameter fallback: Fallback value if setting not available in any config. Defaults to `Bool()`. + + - returns: Resolved setting of type `Bool` for given key. +*/ +public func ConfigBool(_ key: String, fallback: Bool = Bool()) -> Bool { + return ConfigGeneric(key, fallback: fallback) +} + +/** + Async "Force Remote" Accessor helper for retreiving the latest setting of type `Bool` for given key. + It will call `ConfigGenericForce` with `Bool` type. + + - parameter key: Key for the setting. + - parameter fallback: Fallback value if refresh was not successful. +*/ +public func ConfigBoolForce(_ key: String, fallback: Bool, completion: @escaping ((_ forced: Bool) -> Void)) { + ConfigGenericForce(key, fallback: fallback, completion: completion) +} + +/** + Accessor helper for retreiving setting of type `Int` for given key. + It will call `ConfigGeneric` with `Int` type. + + - parameter key: Key for the setting. + - parameter fallback: Fallback value if setting not available in any config. Defaults to `Int()`. + + - returns: Resolved setting of type `Int` for given key. +*/ +public func ConfigInt(_ key: String, fallback: Int = Int()) -> Int { + return ConfigGeneric(key, fallback: fallback) +} + +/** + Async "Force Remote" Accessor helper for retreiving the latest setting of type `Int` for given key. + It will call `ConfigGenericForce` with `Int` type. + + - parameter key: Key for the setting. + - parameter fallback: Fallback value if refresh was not successful. +*/ +public func ConfigIntForce(_ key: String, fallback: Int, completion: @escaping ((_ forced: Int) -> Void)) { + ConfigGenericForce(key, fallback: fallback, completion: completion) +} + +/** + Accessor helper for retreiving setting of type `Double` for given key. + It will call `ConfigGeneric` with `Double` type. + + - parameter key: Key for the setting. + - parameter fallback: Fallback value if setting not available in any config. Defaults to `Double()`. + + - returns: Resolved setting of type `Double` for given key. +*/ +public func ConfigDouble(_ key: String, fallback: Double = Double()) -> Double { + return ConfigGeneric(key, fallback: fallback) +} + +/** + Async "Force Remote" Accessor helper for retreiving the latest setting of type `Double` for given key. + It will call `ConfigGenericForce` with `Double` type. + + - parameter key: Key for the setting. + - parameter fallback: Fallback value if refresh was not successful. +*/ +public func ConfigDoubleForce(_ key: String, fallback: Double, completion: @escaping ((_ forced: Double) -> Void)) { + ConfigGenericForce(key, fallback: fallback, completion: completion) +} + +/** + Accessor helper for retreiving setting of type `String` for given key. + It will call `ConfigGeneric` with `String` type. + + - parameter key: Key for the setting. + - parameter fallback: Fallback value if setting not available in any config. Defaults to `String()`. + + - returns: Resolved setting of type `String` for given key. +*/ +public func ConfigString(_ key: String, fallback: String = String()) -> String { + return ConfigGeneric(key, fallback: fallback) +} + +/** + Async "Force Remote" Accessor helper for retreiving the latest setting of type `String` for given key. + It will call `ConfigGenericForce` with `String` type. + + - parameter key: Key for the setting. + - parameter fallback: Fallback value if refresh was not successful. +*/ +public func ConfigStringForce(_ key: String, fallback: String, completion: @escaping ((_ forced: String) -> Void)) { + ConfigGenericForce(key, fallback: fallback, completion: completion) +} diff --git a/Sources/Brain.swift b/Sources/Brain.swift new file mode 100644 index 0000000..849c619 --- /dev/null +++ b/Sources/Brain.swift @@ -0,0 +1,200 @@ +// +// Brain.swift +// +// Copyright (c) 2016 appculture http://appculture.com +// +// Permission is hereby granted, free of charge, to any person obtaining a copy +// of this software and associated documentation files (the "Software"), to deal +// in the Software without restriction, including without limitation the rights +// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +// copies of the Software, and to permit persons to whom the Software is +// furnished to do so, subject to the following conditions: +// +// The above copyright notice and this permission notice shall be included in all +// copies or substantial portions of the Software. +// +// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +// SOFTWARE. +// + +import Foundation + +/// Block which throws via inner block. +public typealias ThrowWithInnerBlock = (() throws -> Void) -> Void + +/// Block which throws dictionary via inner block. +public typealias ThrowJSONWithInnerBlock = (_ block: @escaping () throws -> [String : AnyObject]) -> Void + +class ACMissionControl { + + // MARK: - Singleton + + static let shared = ACMissionControl() + + // MARK: - Properties + + weak var delegate: MissionControlDelegate? + + var localConfig: [String : Any]? + + var remoteURL: URL? { + didSet { + if let _ = remoteURL { + refresh({ (block) in + do { + _ = try block() + } catch { + print(error) + } + }) + } + } + } + + var remoteConfig: [String : Any]? { + didSet { + if let newConfig = remoteConfig { + refreshDate = Date() + + cachedConfig = newConfig + cacheDate = refreshDate + + informListeners(oldConfig: oldValue, newConfig: newConfig) + } + } + } + + private func informListeners(oldConfig: [String : Any]?, newConfig: [String : Any]) { + let userInfo = userInfoWithConfig(old: oldConfig, new: newConfig) + delegate?.missionControlDidRefreshConfig(old: oldConfig, new: newConfig) + sendNotification(MissionControl.Notification.DidRefreshConfig, userInfo: userInfo) + } + + var refreshDate: Date? + + private struct Cache { + static let Config = "ACMissionControl.CachedConfig" + static let Date = "ACMissionControl.CacheDate" + } + + var cachedConfig: [String : Any]? { + get { + let userDefaults = UserDefaults.standard + let config = userDefaults.object(forKey: Cache.Config) as? [String : AnyObject] + return config + } + set { + let userDefaults = UserDefaults.standard + userDefaults.set(newValue, forKey: Cache.Config) + userDefaults.synchronize() + } + } + + var cacheDate: Date? { + get { + let userDefaults = UserDefaults.standard + let config = userDefaults.object(forKey: Cache.Date) as? Date + return config + } + set { + let userDefaults = UserDefaults.standard + userDefaults.set(newValue, forKey: Cache.Date) + userDefaults.synchronize() + } + } + + // MARK: - API + + func refresh(_ completion: ThrowWithInnerBlock? = nil) { + getRemoteConfig { [unowned self] (block) in + DispatchQueue.main.async { [unowned self] in + do { + let remoteConfig = try block() + self.remoteConfig = remoteConfig + completion?({ }) + } catch { + self.informListeners(error) + completion?({ throw error }) + } + } + } + } + + private func informListeners(_ error: Error) { + delegate?.missionControlDidFailRefreshingConfig(error: error) + let userInfo: [AnyHashable : Any] = ["Error" : "\(error)"] + sendNotification(MissionControl.Notification.DidFailRefreshingConfig, userInfo: userInfo) + } + + // MARK: - Helpers + + func resetAll() { + localConfig = nil + cachedConfig = nil + remoteConfig = nil + refreshDate = nil + remoteURL = nil + delegate = nil + } + + func resetRemote() { + remoteConfig = nil + refreshDate = nil + } + + private func userInfoWithConfig(old: [AnyHashable : Any]?, new: [AnyHashable : Any]?) -> [AnyHashable : Any]? { + if old == nil && new == nil { + return nil + } else { + var userInfo = [AnyHashable : Any]() + if let oldConfig = old { + userInfo[MissionControl.Notification.UserInfo.OldConfigKey] = oldConfig + } + if let newConfig = new { + userInfo[MissionControl.Notification.UserInfo.NewConfigKey] = newConfig + } + return userInfo + } + } + + private func sendNotification(_ name: String, userInfo: [AnyHashable : Any]? = nil) { + let center = NotificationCenter.default + center.post(name: Notification.Name(rawValue: name), object: self, userInfo: userInfo) + } + + private func getRemoteConfig(_ completion: @escaping ThrowJSONWithInnerBlock) { + guard let url = remoteURL + else { completion({ throw MissionControl.ServerError.noRemoteURL }); return } + + let request = URLRequest(url: url) + let session = URLSession.shared + + let task = session.dataTask(with: request) { [unowned self] (data, response, error) in + guard let httpResponse = response as? HTTPURLResponse, httpResponse.statusCode == 200 + else { completion({ throw MissionControl.ServerError.badResponseCode }); return } + self.parseRemoteConfigFromData(data, completion: completion) + } + + task.resume() + } + + private func parseRemoteConfigFromData(_ data: Data?, completion: ThrowJSONWithInnerBlock) { + guard let configData = data + else { completion({ throw MissionControl.ServerError.invalidData }); return } + + do { + let json = try JSONSerialization.jsonObject(with: configData, options: .allowFragments) + guard let config = json as? [String : AnyObject] + else { completion({ throw MissionControl.ServerError.invalidData }); return } + completion({ return config }) + } catch { + completion({ throw MissionControl.ServerError.invalidData }) + } + } + +} diff --git a/Sources/MissionControl.swift b/Sources/MissionControl.swift index 09f943a..651996e 100644 --- a/Sources/MissionControl.swift +++ b/Sources/MissionControl.swift @@ -24,21 +24,19 @@ import Foundation -// MARK: - MissionControl - /// Facade class for using MissionControl. public class MissionControl { - // MARK: Types + // MARK: - Types /// Errors types which can be throwed when refreshing local config from remote. - public enum Error: ErrorType { + public enum ServerError: Error { /// Property `remoteConfigURL` is not set on launch. - case NoRemoteURL + case noRemoteURL /// Server returned response code other then 200 OK. - case BadResponseCode + case badResponseCode /// Server returned data with invalid format. - case InvalidData + case invalidData } /// Constants for keys of sent NSNotification objects. @@ -57,35 +55,35 @@ public class MissionControl { } } - // MARK: Properties + // MARK: - Properties /// Delegate for Mission Control. public class var delegate: MissionControlDelegate? { - get { return ACMissionControl.sharedInstance.delegate } - set { ACMissionControl.sharedInstance.delegate = newValue } + get { return ACMissionControl.shared.delegate } + set { ACMissionControl.shared.delegate = newValue } } /// The latest version of config dictionary, directly accessible, if needed. - public class var config: [String : AnyObject] { - let remoteConfig = ACMissionControl.sharedInstance.remoteConfig - let cachedConfig = ACMissionControl.sharedInstance.cachedConfig - let localConfig = ACMissionControl.sharedInstance.localConfig - let emptyConfig = [String : AnyObject]() + public class var config: [String : Any] { + let remoteConfig = ACMissionControl.shared.remoteConfig + let cachedConfig = ACMissionControl.shared.cachedConfig + let localConfig = ACMissionControl.shared.localConfig + let emptyConfig = [String : Any]() let resolvedConfig = remoteConfig ?? cachedConfig ?? localConfig ?? emptyConfig return resolvedConfig } /// Date of last successful refresh from remote. - public class var refreshDate: NSDate? { - return ACMissionControl.sharedInstance.refreshDate + public class var refreshDate: Date? { + return ACMissionControl.shared.refreshDate } /// Date of last cached remote config. - public class var cacheDate: NSDate? { - return ACMissionControl.sharedInstance.cacheDate + public class var cacheDate: Date? { + return ACMissionControl.shared.cacheDate } - // MARK: API + // MARK: - API /** This should be called on your app start to initialize and/or refresh remote config. @@ -95,9 +93,9 @@ public class MissionControl { - parameter localConfig: Default local config which can be used until remote config is fetched. - parameter remoteConfigURL: If this parameter is set then `refresh` will be called, otherwise not. */ - public class func launch(localConfig localConfig: [String : AnyObject]? = nil, remoteConfigURL url: NSURL? = nil) { - ACMissionControl.sharedInstance.localConfig = localConfig - ACMissionControl.sharedInstance.remoteURL = url + public class func launch(localConfig: [String : Any]? = nil, remoteConfigURL url: URL? = nil) { + ACMissionControl.shared.localConfig = localConfig + ACMissionControl.shared.remoteURL = url } /** @@ -107,14 +105,12 @@ public class MissionControl { - parameter completion: Completion handler (SEE: `ThrowWithInnerBlock`). */ - public class func refresh(completion: ThrowWithInnerBlock? = nil) { - ACMissionControl.sharedInstance.refresh(completion) + public class func refresh(_ completion: ThrowWithInnerBlock? = nil) { + ACMissionControl.shared.refresh(completion) } } -// MARK: - MissionControlDelegate - /** Delegate for Mission Control. @@ -127,339 +123,12 @@ public protocol MissionControlDelegate: class { - parameter old: Previous config (nil if it's the first refresh) - parameter new: Current config */ - func missionControlDidRefreshConfig(old old: [String : AnyObject]?, new: [String : AnyObject]) + func missionControlDidRefreshConfig(old: [String : Any]?, new: [String : Any]) /** Called when refreshing config from remote fails. - parameter error: Error which happened during config refresh from remote. */ - func missionControlDidFailRefreshingConfig(error error: ErrorType) -} - -// MARK: - Custom Types - -/// Block which throws via inner block. -public typealias ThrowWithInnerBlock = (() throws -> Void) -> Void - -/// Block which throws dictionary via inner block. -public typealias ThrowJSONWithInnerBlock = (block: () throws -> [String : AnyObject]) -> Void - -// MARK: - Accessors - -/** - Accessor for retreiving setting of generic type `T` for given key. - - This method will resolve to proper setting by following this priority order: - 1. Remote setting from memory (received in the last refresh). - 2. Remote setting from disk cache (if never refreshed in current app session (ex. offline)). - 3. Local setting from disk (defaults provided in `localConfig` on MissionControl `launch`). - 4. Provided fallback value (if provided) - - - parameter key: Key for the setting. - - parameter fallback: Fallback value if setting is not available in any config. - - - returns: Resolved setting of generic type `T` for given key. -*/ -public func ConfigGeneric(key: String, fallback: T) -> T { - if let remoteValue = ACMissionControl.sharedInstance.remoteConfig?[key] as? T { - return remoteValue - } else if let cachedValue = ACMissionControl.sharedInstance.cachedConfig?[key] as? T { - return cachedValue - } else if let localValue = ACMissionControl.sharedInstance.localConfig?[key] as? T { - return localValue - } else { - return fallback - } -} - -/** - Async "Force Remote" Accessor for retreiving the latest setting of generic type `T` for given key. - - This method will first call `refresh` method after which it will evaluate its success. - - If `refresh` was successful, it will call normal accessor of generic type `T` for given key, - which will by its priority order resolve to the latest remote value as a parameter inside `completion` handler. - - If `refresh` fails, it will return provided `fallback` value as a parameter inside `completion` block. - - - parameter key: Key for the setting. - - parameter fallback: Fallback value of generic type `T` if refresh is not successful. -*/ -public func ConfigGenericForce(key: String, fallback: T, completion: ((forced: T) -> Void)) { - MissionControl.refresh({ (innerBlock) in - do { - let _ = try innerBlock() - completion(forced: ConfigGeneric(key, fallback: fallback)) - } catch { - completion(forced: fallback) - } - }) -} - -/** - Accessor helper for retreiving setting of type `Bool` for given key. - It will call `ConfigGeneric` with `Bool` type. - - - parameter key: Key for the setting. - - parameter fallback: Fallback value if setting not available in any config. Defaults to `Bool()`. - - - returns: Resolved setting of type `Bool` for given key. -*/ -public func ConfigBool(key: String, fallback: Bool = Bool()) -> Bool { - return ConfigGeneric(key, fallback: fallback) -} - -/** - Async "Force Remote" Accessor helper for retreiving the latest setting of type `Bool` for given key. - It will call `ConfigGenericForce` with `Bool` type. - - - parameter key: Key for the setting. - - parameter fallback: Fallback value if refresh was not successful. -*/ -public func ConfigBoolForce(key: String, fallback: Bool, completion: ((forced: Bool) -> Void)) { - ConfigGenericForce(key, fallback: fallback, completion: completion) -} - -/** - Accessor helper for retreiving setting of type `Int` for given key. - It will call `ConfigGeneric` with `Int` type. - - - parameter key: Key for the setting. - - parameter fallback: Fallback value if setting not available in any config. Defaults to `Int()`. - - - returns: Resolved setting of type `Int` for given key. -*/ -public func ConfigInt(key: String, fallback: Int = Int()) -> Int { - return ConfigGeneric(key, fallback: fallback) -} - -/** - Async "Force Remote" Accessor helper for retreiving the latest setting of type `Int` for given key. - It will call `ConfigGenericForce` with `Int` type. - - - parameter key: Key for the setting. - - parameter fallback: Fallback value if refresh was not successful. -*/ -public func ConfigIntForce(key: String, fallback: Int, completion: ((forced: Int) -> Void)) { - ConfigGenericForce(key, fallback: fallback, completion: completion) -} - -/** - Accessor helper for retreiving setting of type `Double` for given key. - It will call `ConfigGeneric` with `Double` type. - - - parameter key: Key for the setting. - - parameter fallback: Fallback value if setting not available in any config. Defaults to `Double()`. - - - returns: Resolved setting of type `Double` for given key. -*/ -public func ConfigDouble(key: String, fallback: Double = Double()) -> Double { - return ConfigGeneric(key, fallback: fallback) -} - -/** - Async "Force Remote" Accessor helper for retreiving the latest setting of type `Double` for given key. - It will call `ConfigGenericForce` with `Double` type. - - - parameter key: Key for the setting. - - parameter fallback: Fallback value if refresh was not successful. -*/ -public func ConfigDoubleForce(key: String, fallback: Double, completion: ((forced: Double) -> Void)) { - ConfigGenericForce(key, fallback: fallback, completion: completion) -} - -/** - Accessor helper for retreiving setting of type `String` for given key. - It will call `ConfigGeneric` with `String` type. - - - parameter key: Key for the setting. - - parameter fallback: Fallback value if setting not available in any config. Defaults to `String()`. - - - returns: Resolved setting of type `String` for given key. -*/ -public func ConfigString(key: String, fallback: String = String()) -> String { - return ConfigGeneric(key, fallback: fallback) -} - -/** - Async "Force Remote" Accessor helper for retreiving the latest setting of type `String` for given key. - It will call `ConfigGenericForce` with `String` type. - - - parameter key: Key for the setting. - - parameter fallback: Fallback value if refresh was not successful. -*/ -public func ConfigStringForce(key: String, fallback: String, completion: ((forced: String) -> Void)) { - ConfigGenericForce(key, fallback: fallback, completion: completion) -} - -// MARK: - ACMissionControl - -class ACMissionControl { - - // MARK: Singleton - - static let sharedInstance = ACMissionControl() - - // MARK: Properties - - weak var delegate: MissionControlDelegate? - - var localConfig: [String : AnyObject]? - - var remoteURL: NSURL? { - didSet { - if let _ = remoteURL { - refresh({ (block) in - do { - _ = try block() - } catch { - print(error) - } - }) - } - } - } - - var remoteConfig: [String : AnyObject]? { - didSet { - if let newConfig = remoteConfig { - refreshDate = NSDate() - - cachedConfig = newConfig - cacheDate = refreshDate - - informListeners(oldConfig: oldValue, newConfig: newConfig) - } - } - } - - private func informListeners(oldConfig oldConfig: [String : AnyObject]?, newConfig: [String : AnyObject]) { - let userInfo = userInfoWithConfig(old: oldConfig, new: newConfig) - delegate?.missionControlDidRefreshConfig(old: oldConfig, new: newConfig) - sendNotification(MissionControl.Notification.DidRefreshConfig, userInfo: userInfo) - } - - var refreshDate: NSDate? - - private struct Cache { - static let Config = "ACMissionControl.CachedConfig" - static let Date = "ACMissionControl.CacheDate" - } - - var cachedConfig: [String : AnyObject]? { - get { - let userDefaults = NSUserDefaults.standardUserDefaults() - let config = userDefaults.objectForKey(Cache.Config) as? [String : AnyObject] - return config - } - set { - let userDefaults = NSUserDefaults.standardUserDefaults() - userDefaults.setObject(newValue, forKey: Cache.Config) - userDefaults.synchronize() - } - } - - var cacheDate: NSDate? { - get { - let userDefaults = NSUserDefaults.standardUserDefaults() - let config = userDefaults.objectForKey(Cache.Date) as? NSDate - return config - } - set { - let userDefaults = NSUserDefaults.standardUserDefaults() - userDefaults.setObject(newValue, forKey: Cache.Date) - userDefaults.synchronize() - } - } - - // MARK: API - - func refresh(completion: ThrowWithInnerBlock? = nil) { - getRemoteConfig { [unowned self] (block) in - dispatch_async(dispatch_get_main_queue()) { [unowned self] in - do { - let remoteConfig = try block() - self.remoteConfig = remoteConfig - completion?({ }) - } catch { - self.informListeners(error) - completion?({ throw error }) - } - } - } - } - - private func informListeners(error: ErrorType) { - delegate?.missionControlDidFailRefreshingConfig(error: error) - let userInfo = ["Error" : "\(error)"] - sendNotification(MissionControl.Notification.DidFailRefreshingConfig, userInfo: userInfo) - } - - // MARK: Helpers - - func resetAll() { - localConfig = nil - cachedConfig = nil - remoteConfig = nil - refreshDate = nil - remoteURL = nil - delegate = nil - } - - func resetRemote() { - remoteConfig = nil - refreshDate = nil - } - - private func userInfoWithConfig(old old: [String : AnyObject]?, new: [String : AnyObject]?) -> [NSObject : AnyObject]? { - if old == nil && new == nil { - return nil - } else { - var userInfo = [NSObject : AnyObject]() - if let oldConfig = old { - userInfo[MissionControl.Notification.UserInfo.OldConfigKey] = oldConfig - } - if let newConfig = new { - userInfo[MissionControl.Notification.UserInfo.NewConfigKey] = newConfig - } - return userInfo - } - } - - private func sendNotification(name: String, userInfo: [NSObject : AnyObject]? = nil) { - let center = NSNotificationCenter.defaultCenter() - center.postNotificationName(name, object: self, userInfo: userInfo) - } - - private func getRemoteConfig(completion: ThrowJSONWithInnerBlock) { - guard let url = remoteURL - else { completion(block: { throw MissionControl.Error.NoRemoteURL }); return } - - let request = NSURLRequest(URL: url) - let session = NSURLSession.sharedSession() - - let task = session.dataTaskWithRequest(request) { [unowned self] (data, response, error) in - guard let httpResponse = response as? NSHTTPURLResponse where httpResponse.statusCode == 200 - else { completion(block: { throw MissionControl.Error.BadResponseCode }); return } - self.parseRemoteConfigFromData(data, completion: completion) - } - - task.resume() - } - - private func parseRemoteConfigFromData(data: NSData?, completion: ThrowJSONWithInnerBlock) { - guard let configData = data - else { completion(block: { throw MissionControl.Error.InvalidData }); return } - - do { - let json = try NSJSONSerialization.JSONObjectWithData(configData, options: .AllowFragments) - guard let config = json as? [String : AnyObject] - else { completion(block: { throw MissionControl.Error.InvalidData }); return } - completion(block: { return config }) - } catch { - completion(block: { throw MissionControl.Error.InvalidData }) - } - } - + func missionControlDidFailRefreshingConfig(error: Error) } diff --git a/Tests/MissionControlTests.swift b/Tests/MissionControlTests.swift index e8db178..0d83bbb 100644 --- a/Tests/MissionControlTests.swift +++ b/Tests/MissionControlTests.swift @@ -36,7 +36,7 @@ class MissionControlTests: XCTestCase, MissionControlDelegate { override func tearDown() { // Put teardown code here. This method is called after the invocation of each test method in the class. - ACMissionControl.sharedInstance.resetAll() + ACMissionControl.shared.resetAll() super.tearDown() } @@ -44,10 +44,10 @@ class MissionControlTests: XCTestCase, MissionControlDelegate { // MARK: - Helper Properties struct URL { - static let BadResponseConfig = NSURL(string: "http://appculture.com/mission-control/not-existing-config.json")! - static let EmptyDataConfig = NSURL(string: "http://private-83024-missioncontrol5.apiary-mock.com/mission-control/empty-config")! - static let InvalidDataConfig = NSURL(string: "http://private-83024-missioncontrol5.apiary-mock.com/mission-control/invalid-config")! - static let RemoteTestConfig = NSURL(string: "http://private-83024-missioncontrol5.apiary-mock.com/mission-control/test-config")! + static let BadResponseConfig = Foundation.URL(string: "http://appculture.com/mission-control/not-existing-config.json")! + static let EmptyDataConfig = Foundation.URL(string: "http://private-83024-missioncontrol5.apiary-mock.com/mission-control/empty-config")! + static let InvalidDataConfig = Foundation.URL(string: "http://private-83024-missioncontrol5.apiary-mock.com/mission-control/invalid-config")! + static let RemoteTestConfig = Foundation.URL(string: "http://private-83024-missioncontrol5.apiary-mock.com/mission-control/test-config")! } struct Key { @@ -57,21 +57,21 @@ class MissionControlTests: XCTestCase, MissionControlDelegate { static let String = "StringSetting" } - let localTestConfig: [String : AnyObject] = [ + let localTestConfig: [String : Any] = [ Key.Bool : false, Key.Int : 21, Key.Double : 0.8, Key.String : "Local" ] - let remoteTestConfig: [String : AnyObject] = [ + let remoteTestConfig: [String : Any] = [ Key.Bool : true, Key.Int : 8, Key.Double : 2.1, Key.String : "Remote" ] - let fallbackTestConfig: [String : AnyObject] = [ + let fallbackTestConfig: [String : Any] = [ Key.Bool : false, Key.Int : 1984, Key.Double : 21.08, @@ -80,14 +80,14 @@ class MissionControlTests: XCTestCase, MissionControlDelegate { var didRefreshConfigExpectation: XCTestExpectation? var didFailRefreshingConfigExpectation: XCTestExpectation? - + // MARK: - MissionControlDelegate - func missionControlDidRefreshConfig(old old: [String : AnyObject]?, new: [String : AnyObject]) { + func missionControlDidRefreshConfig(old: [String : Any]?, new: [String : Any]) { didRefreshConfigExpectation?.fulfill() } - func missionControlDidFailRefreshingConfig(error error: ErrorType) { + func missionControlDidFailRefreshingConfig(error: Error) { didFailRefreshingConfigExpectation?.fulfill() } @@ -238,19 +238,19 @@ class MissionControlTests: XCTestCase, MissionControlDelegate { // MARK: - Test Remote Accessors - func confirmRemoteConfigStateAfterNotification(notification: String) { + func confirmRemoteConfigStateAfterNotification(_ notification: String) { confirmDidRefreshConfigDelegateCallback() - let _ = expectationForNotification(notification, object: nil) { (notification) -> Bool in + let _ = expectation(forNotification: NSNotification.Name(rawValue: notification), object: nil) { (notification) -> Bool in self.confirmRemoteConfigState() return true } - waitForExpectationsWithTimeout(5, handler: nil) + waitForExpectations(timeout: 5, handler: nil) } func confirmDidRefreshConfigDelegateCallback() { MissionControl.delegate = self - didRefreshConfigExpectation = expectationWithDescription("Should call MissionControlDelegate.") + didRefreshConfigExpectation = expectation(description: "Should call MissionControlDelegate.") } func confirmRemoteConfigState() { @@ -305,10 +305,10 @@ class MissionControlTests: XCTestCase, MissionControlDelegate { func testForceRemoteAccessors() { MissionControl.launch(remoteConfigURL: URL.RemoteTestConfig) - let boolExpectation = expectationWithDescription("ConfigBoolForce") - let intExpectation = expectationWithDescription("ConfigIntForce") - let doubleExpectation = expectationWithDescription("ConfigDoubleForce") - let stringExpectation = expectationWithDescription("ConfigStringForce") + let boolExpectation = expectation(description: "ConfigBoolForce") + let intExpectation = expectation(description: "ConfigIntForce") + let doubleExpectation = expectation(description: "ConfigDoubleForce") + let stringExpectation = expectation(description: "ConfigStringForce") let fallbackBool = fallbackTestConfig[Key.Bool] as! Bool let fallbackInt = fallbackTestConfig[Key.Int] as! Int @@ -336,16 +336,16 @@ class MissionControlTests: XCTestCase, MissionControlDelegate { stringExpectation.fulfill() } - waitForExpectationsWithTimeout(10.0, handler: nil) + waitForExpectations(timeout: 10.0, handler: nil) } func testForceRemoteAccessorsFallback() { MissionControl.launch(remoteConfigURL: URL.BadResponseConfig) - let boolExpectation = expectationWithDescription("ConfigBoolForceFallback") - let intExpectation = expectationWithDescription("ConfigIntForceFallback") - let doubleExpectation = expectationWithDescription("ConfigDoubleForceFallback") - let stringExpectation = expectationWithDescription("ConfigStringForceFallback") + let boolExpectation = expectation(description: "ConfigBoolForceFallback") + let intExpectation = expectation(description: "ConfigIntForceFallback") + let doubleExpectation = expectation(description: "ConfigDoubleForceFallback") + let stringExpectation = expectation(description: "ConfigStringForceFallback") let fallbackBool = fallbackTestConfig[Key.Bool] as! Bool let fallbackInt = fallbackTestConfig[Key.Int] as! Int @@ -369,7 +369,7 @@ class MissionControlTests: XCTestCase, MissionControlDelegate { stringExpectation.fulfill() } - waitForExpectationsWithTimeout(10.0, handler: nil) + waitForExpectations(timeout: 10.0, handler: nil) } // MARK: - Test Cache @@ -378,12 +378,12 @@ class MissionControlTests: XCTestCase, MissionControlDelegate { MissionControl.launch(remoteConfigURL: URL.RemoteTestConfig) let notification = MissionControl.Notification.DidRefreshConfig - let _ = expectationForNotification(notification, object: nil) { (notification) -> Bool in - ACMissionControl.sharedInstance.resetRemote() + let _ = expectation(forNotification: NSNotification.Name(rawValue: notification), object: nil) { (notification) -> Bool in + ACMissionControl.shared.resetRemote() self.confirmCachedConfigState() return true } - waitForExpectationsWithTimeout(5, handler: nil) + waitForExpectations(timeout: 5, handler: nil) } func confirmCachedConfigState() { @@ -403,7 +403,7 @@ class MissionControlTests: XCTestCase, MissionControlDelegate { MissionControl.launch() /// - NOTE: refresh is NOT called automatically during launch (remote URL missing) - let asyncExpectation = expectationWithDescription("ManualRefreshWithoutURL") + let asyncExpectation = expectation(description: "ManualRefreshWithoutURL") MissionControl.refresh { (block) in do { let _ = try block() @@ -411,11 +411,11 @@ class MissionControlTests: XCTestCase, MissionControlDelegate { asyncExpectation.fulfill() } catch { let message = "Should return NoRemoteURL error whene remoteURL is not set." - XCTAssertEqual("\(error)", "\(MissionControl.Error.NoRemoteURL)", message) + XCTAssertEqual("\(error)", "\(MissionControl.ServerError.noRemoteURL)", message) asyncExpectation.fulfill() } } - waitForExpectationsWithTimeout(5, handler: nil) + waitForExpectations(timeout: 5, handler: nil) } func testRefreshErrorBadResponseCode() { @@ -423,7 +423,7 @@ class MissionControlTests: XCTestCase, MissionControlDelegate { /// - NOTE: refresh is called automatically during launch let message = "Should return BadResponseCode error when response is not 200 OK." - confirmConfigRefreshFailedNotification(MissionControl.Error.BadResponseCode, message: message) + confirmConfigRefreshFailedNotification(MissionControl.ServerError.badResponseCode, message: message) } func testRefreshErrorInvalidDataEmpty() { @@ -431,7 +431,7 @@ class MissionControlTests: XCTestCase, MissionControlDelegate { /// - NOTE: refresh is called automatically during launch let message = "Should return InvalidData error when response data is empty." - confirmConfigRefreshFailedNotification(MissionControl.Error.InvalidData, message: message) + confirmConfigRefreshFailedNotification(MissionControl.ServerError.invalidData, message: message) } func testRefreshErrorInvalidData() { @@ -439,25 +439,25 @@ class MissionControlTests: XCTestCase, MissionControlDelegate { /// - NOTE: refresh is called automatically during launch let message = "Should return InvalidData error when response data is not valid JSON." - confirmConfigRefreshFailedNotification(MissionControl.Error.InvalidData, message: message) + confirmConfigRefreshFailedNotification(MissionControl.ServerError.invalidData, message: message) } - func confirmConfigRefreshFailedNotification(error: MissionControl.Error, message: String) { + func confirmConfigRefreshFailedNotification(_ error: Error, message: String) { confirmDidFailRefreshingConfigDelegateCallback() let notification = MissionControl.Notification.DidFailRefreshingConfig - let _ = expectationForNotification(notification, object: nil) { (notification) -> Bool in + let _ = expectation(forNotification: NSNotification.Name(rawValue: notification), object: nil) { (notification) -> Bool in guard let errorInfo = notification.userInfo?["Error"] as? String else { return false } XCTAssertEqual("\(errorInfo)", "\(error)", message) self.confirmInitialState() return true } - waitForExpectationsWithTimeout(5, handler: nil) + waitForExpectations(timeout: 5, handler: nil) } func confirmDidFailRefreshingConfigDelegateCallback() { MissionControl.delegate = self - didFailRefreshingConfigExpectation = expectationWithDescription("Should call MissionControlDelegate.") + didFailRefreshingConfigExpectation = expectation(description: "Should call MissionControlDelegate.") } - + }