├── CHANGELOG.md
├── Images
├── MissionControl-02-Ready.png
├── MissionControl-01-Offline.png
└── MissionControl-03-Countdown.png
├── Example
├── MissionControlDemo
│ ├── Assets.xcassets
│ │ ├── Contents.json
│ │ ├── AppIcon.appiconset
│ │ │ ├── Icon-29.png
│ │ │ ├── Icon-40.png
│ │ │ ├── Icon-76.png
│ │ │ ├── Icon-29@2x.png
│ │ │ ├── Icon-29@3x.png
│ │ │ ├── Icon-40@2x.png
│ │ │ ├── Icon-40@3x.png
│ │ │ ├── Icon-60@2x.png
│ │ │ ├── Icon-60@3x.png
│ │ │ ├── Icon-76@2x.png
│ │ │ ├── Icon-29@2x-1.png
│ │ │ ├── Icon-40@2x-1.png
│ │ │ ├── Icon-83.5@2x.png
│ │ │ └── Contents.json
│ │ ├── appculture.imageset
│ │ │ ├── appculture.pdf
│ │ │ └── Contents.json
│ │ └── Launch Image.imageset
│ │ │ ├── Launch Image.pdf
│ │ │ └── Contents.json
│ ├── NAS966.TTF
│ ├── Info.plist
│ ├── LaunchViewController.swift
│ ├── Base.lproj
│ │ ├── Launch.storyboard
│ │ └── LaunchScreen.storyboard
│ ├── AppDelegate.swift
│ ├── LaunchView.swift
│ ├── LaunchBrain.swift
│ └── BaseLaunchView.swift
└── MissionControlDemo.xcodeproj
│ ├── project.xcworkspace
│ └── contents.xcworkspacedata
│ ├── xcshareddata
│ └── xcschemes
│ │ └── MissionControlDemo.xcscheme
│ └── project.pbxproj
├── MissionControl.xcodeproj
├── project.xcworkspace
│ └── contents.xcworkspacedata
├── xcshareddata
│ └── xcschemes
│ │ ├── MissionControl watchOS.xcscheme
│ │ ├── MissionControl OSX.xcscheme
│ │ ├── MissionControl tvOS.xcscheme
│ │ └── MissionControl iOS.xcscheme
└── project.pbxproj
├── MissionControl.podspec
├── Tests
├── Info.plist
└── MissionControlTests.swift
├── Sources
├── Info.plist
├── MissionControl.h
└── MissionControl.swift
├── LICENSE
├── Package.swift
└── README.md
/CHANGELOG.md:
--------------------------------------------------------------------------------
1 | # Changelog
2 |
3 | ## Version 1.0.0
4 |
5 | - Initial version
--------------------------------------------------------------------------------
/Images/MissionControl-02-Ready.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/appculture/MissionControl-iOS/HEAD/Images/MissionControl-02-Ready.png
--------------------------------------------------------------------------------
/Example/MissionControlDemo/Assets.xcassets/Contents.json:
--------------------------------------------------------------------------------
1 | {
2 | "info" : {
3 | "version" : 1,
4 | "author" : "xcode"
5 | }
6 | }
--------------------------------------------------------------------------------
/Images/MissionControl-01-Offline.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/appculture/MissionControl-iOS/HEAD/Images/MissionControl-01-Offline.png
--------------------------------------------------------------------------------
/Example/MissionControlDemo/NAS966.TTF:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/appculture/MissionControl-iOS/HEAD/Example/MissionControlDemo/NAS966.TTF
--------------------------------------------------------------------------------
/Images/MissionControl-03-Countdown.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/appculture/MissionControl-iOS/HEAD/Images/MissionControl-03-Countdown.png
--------------------------------------------------------------------------------
/Example/MissionControlDemo/Assets.xcassets/AppIcon.appiconset/Icon-29.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/appculture/MissionControl-iOS/HEAD/Example/MissionControlDemo/Assets.xcassets/AppIcon.appiconset/Icon-29.png
--------------------------------------------------------------------------------
/Example/MissionControlDemo/Assets.xcassets/AppIcon.appiconset/Icon-40.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/appculture/MissionControl-iOS/HEAD/Example/MissionControlDemo/Assets.xcassets/AppIcon.appiconset/Icon-40.png
--------------------------------------------------------------------------------
/Example/MissionControlDemo/Assets.xcassets/AppIcon.appiconset/Icon-76.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/appculture/MissionControl-iOS/HEAD/Example/MissionControlDemo/Assets.xcassets/AppIcon.appiconset/Icon-76.png
--------------------------------------------------------------------------------
/Example/MissionControlDemo/Assets.xcassets/AppIcon.appiconset/Icon-29@2x.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/appculture/MissionControl-iOS/HEAD/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/HEAD/Example/MissionControlDemo/Assets.xcassets/AppIcon.appiconset/Icon-29@3x.png
--------------------------------------------------------------------------------
/Example/MissionControlDemo/Assets.xcassets/AppIcon.appiconset/Icon-40@2x.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/appculture/MissionControl-iOS/HEAD/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/HEAD/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/HEAD/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/HEAD/Example/MissionControlDemo/Assets.xcassets/AppIcon.appiconset/Icon-60@3x.png
--------------------------------------------------------------------------------
/Example/MissionControlDemo/Assets.xcassets/AppIcon.appiconset/Icon-76@2x.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/appculture/MissionControl-iOS/HEAD/Example/MissionControlDemo/Assets.xcassets/AppIcon.appiconset/Icon-76@2x.png
--------------------------------------------------------------------------------
/Example/MissionControlDemo/Assets.xcassets/AppIcon.appiconset/Icon-29@2x-1.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/appculture/MissionControl-iOS/HEAD/Example/MissionControlDemo/Assets.xcassets/AppIcon.appiconset/Icon-29@2x-1.png
--------------------------------------------------------------------------------
/Example/MissionControlDemo/Assets.xcassets/AppIcon.appiconset/Icon-40@2x-1.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/appculture/MissionControl-iOS/HEAD/Example/MissionControlDemo/Assets.xcassets/AppIcon.appiconset/Icon-40@2x-1.png
--------------------------------------------------------------------------------
/Example/MissionControlDemo/Assets.xcassets/AppIcon.appiconset/Icon-83.5@2x.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/appculture/MissionControl-iOS/HEAD/Example/MissionControlDemo/Assets.xcassets/AppIcon.appiconset/Icon-83.5@2x.png
--------------------------------------------------------------------------------
/Example/MissionControlDemo/Assets.xcassets/appculture.imageset/appculture.pdf:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/appculture/MissionControl-iOS/HEAD/Example/MissionControlDemo/Assets.xcassets/appculture.imageset/appculture.pdf
--------------------------------------------------------------------------------
/Example/MissionControlDemo/Assets.xcassets/Launch Image.imageset/Launch Image.pdf:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/appculture/MissionControl-iOS/HEAD/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/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 | }
--------------------------------------------------------------------------------
/MissionControl.xcodeproj/project.xcworkspace/contents.xcworkspacedata:
--------------------------------------------------------------------------------
1 |
2 |
4 |
6 |
7 |
8 |
--------------------------------------------------------------------------------
/Example/MissionControlDemo.xcodeproj/project.xcworkspace/contents.xcworkspacedata:
--------------------------------------------------------------------------------
1 |
2 |
4 |
6 |
7 |
8 |
--------------------------------------------------------------------------------
/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
--------------------------------------------------------------------------------
/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 |
--------------------------------------------------------------------------------
/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 |
--------------------------------------------------------------------------------
/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.
--------------------------------------------------------------------------------
/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 | )
--------------------------------------------------------------------------------
/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[];
--------------------------------------------------------------------------------
/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/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/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/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/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/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 |
--------------------------------------------------------------------------------
/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 |
--------------------------------------------------------------------------------
/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 |
--------------------------------------------------------------------------------
/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 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 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 |
--------------------------------------------------------------------------------
/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/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 |
--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
1 | # Mission Control
2 | **Super powerful remote config utility written in Swift (iOS, watchOS, tvOS, OSX)**
3 |
4 | [](https://swift.org)
5 | [](http://www.apple.com)
6 | [](https://github.com/appculture/MissionControl-iOS/blob/master/LICENSE)
7 |
8 | [](https://cocoapods.org/pods/MissionControl)
9 | [](https://github.com/Carthage/Carthage)
10 | [](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 |
--------------------------------------------------------------------------------
/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.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 |
--------------------------------------------------------------------------------
/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/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 |
--------------------------------------------------------------------------------
/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 |
--------------------------------------------------------------------------------