├── .gitignore ├── CODE_OF_CONDUCT.md ├── CONTRIBUTING.md ├── Cordux.podspec ├── Cordux.xcodeproj ├── project.pbxproj ├── project.xcworkspace │ ├── contents.xcworkspacedata │ └── xcshareddata │ │ └── IDEWorkspaceChecks.plist └── xcshareddata │ └── xcschemes │ ├── Cordux-iOS.xcscheme │ └── Cordux-tvOS.xcscheme ├── Cordux ├── Cordux.h └── Info.plist ├── CorduxTests ├── Info.plist ├── NavigationControllerMetaCoordinatorTests.swift ├── SimpleCoordinators.swift ├── SimpleState.swift └── SubscriptionsTest.swift ├── Example ├── .ruby-version ├── CREDITS.md ├── CorduxPrototype.xcodeproj │ ├── project.pbxproj │ └── project.xcworkspace │ │ └── contents.xcworkspacedata ├── CorduxPrototype.xcworkspace │ └── contents.xcworkspacedata ├── CorduxPrototype │ ├── AppCoordinator.swift │ ├── AppDelegate.swift │ ├── AppState.swift │ ├── Assets.xcassets │ │ ├── AppIcon.appiconset │ │ │ └── Contents.json │ │ ├── first.imageset │ │ │ ├── Contents.json │ │ │ └── first.pdf │ │ └── second.imageset │ │ │ ├── Contents.json │ │ │ └── second.pdf │ ├── Authentication.storyboard │ ├── AuthenticationCoordinator.swift │ ├── Base.lproj │ │ └── LaunchScreen.storyboard │ ├── Catalog.storyboard │ ├── CatalogCoordinator.swift │ ├── FirstViewController.swift │ ├── ForgotPasswordViewController.swift │ ├── Info.plist │ ├── Main.storyboard │ ├── MainViewController.swift │ ├── SecondViewController.swift │ └── SignInViewController.swift ├── CorduxPrototypeTests │ ├── CorduxPrototypeTests.swift │ └── Info.plist ├── CorduxPrototypeUITests │ ├── CorduxPrototypeUITests.swift │ └── Info.plist ├── Podfile ├── Podfile.lock └── Pods │ ├── Local Podspecs │ └── Cordux.podspec.json │ ├── Manifest.lock │ ├── Pods.xcodeproj │ └── project.pbxproj │ └── Target Support Files │ ├── Cordux │ ├── Cordux-dummy.m │ ├── Cordux-prefix.pch │ ├── Cordux-umbrella.h │ ├── Cordux.modulemap │ ├── Cordux.xcconfig │ └── Info.plist │ └── Pods-CorduxPrototype │ ├── Info.plist │ ├── Pods-CorduxPrototype-acknowledgements.markdown │ ├── Pods-CorduxPrototype-acknowledgements.plist │ ├── Pods-CorduxPrototype-dummy.m │ ├── Pods-CorduxPrototype-frameworks.sh │ ├── Pods-CorduxPrototype-resources.sh │ ├── Pods-CorduxPrototype-umbrella.h │ ├── Pods-CorduxPrototype.debug.xcconfig │ ├── Pods-CorduxPrototype.modulemap │ └── Pods-CorduxPrototype.release.xcconfig ├── LICENSE ├── README.md ├── Sources ├── Context.swift ├── Coordinator.swift ├── Logging.swift ├── Middleware.swift ├── NavigationControllerCoordinator.swift ├── NavigationControllerMetaCoordinator.swift ├── PresentingCoordinator.swift ├── Reducer.swift ├── Routing.swift ├── SceneCoordinator.swift ├── Scenes.swift ├── SimpleCoordinator.swift ├── Store.swift ├── Subscriptions.swift ├── TabBarControllerCoordinator.swift ├── TypeHelpers.swift └── ViewControllerLifecycle.swift ├── diagrams ├── Makefile └── architecture.dot └── yougot.jpg /.gitignore: -------------------------------------------------------------------------------- 1 | # Xcode 2 | # 3 | # gitignore contributors: remember to update Global/Xcode.gitignore, Objective-C.gitignore & Swift.gitignore 4 | 5 | .DS_Store 6 | 7 | ## Build generated 8 | build/ 9 | DerivedData/ 10 | 11 | ## Various settings 12 | *.pbxuser 13 | !default.pbxuser 14 | *.mode1v3 15 | !default.mode1v3 16 | *.mode2v3 17 | !default.mode2v3 18 | *.perspectivev3 19 | !default.perspectivev3 20 | xcuserdata/ 21 | 22 | ## Other 23 | *.moved-aside 24 | *.xccheckout 25 | *.xcscmblueprint 26 | 27 | # Graphviz Output 28 | diagrams/*.png 29 | -------------------------------------------------------------------------------- /CODE_OF_CONDUCT.md: -------------------------------------------------------------------------------- 1 | # Contributor Covenant Code of Conduct 2 | 3 | ## Our Pledge 4 | 5 | In the interest of fostering an open and welcoming environment, we as 6 | contributors and maintainers pledge to making participation in our project and 7 | our community a harassment-free experience for everyone, regardless of age, body 8 | size, disability, ethnicity, gender identity and expression, level of experience, 9 | nationality, personal appearance, race, religion, or sexual identity and 10 | orientation. 11 | 12 | ## Our Standards 13 | 14 | Examples of behavior that contributes to creating a positive environment 15 | include: 16 | 17 | * Using welcoming and inclusive language 18 | * Being respectful of differing viewpoints and experiences 19 | * Gracefully accepting constructive criticism 20 | * Focusing on what is best for the community 21 | * Showing empathy towards other community members 22 | 23 | Examples of unacceptable behavior by participants include: 24 | 25 | * The use of sexualized language or imagery and unwelcome sexual attention or 26 | advances 27 | * Trolling, insulting/derogatory comments, and personal or political attacks 28 | * Public or private harassment 29 | * Publishing others' private information, such as a physical or electronic 30 | address, without explicit permission 31 | * Other conduct which could reasonably be considered inappropriate in a 32 | professional setting 33 | 34 | ## Our Responsibilities 35 | 36 | Project maintainers are responsible for clarifying the standards of acceptable 37 | behavior and are expected to take appropriate and fair corrective action in 38 | response to any instances of unacceptable behavior. 39 | 40 | Project maintainers have the right and responsibility to remove, edit, or 41 | reject comments, commits, code, wiki edits, issues, and other contributions 42 | that are not aligned to this Code of Conduct, or to ban temporarily or 43 | permanently any contributor for other behaviors that they deem inappropriate, 44 | threatening, offensive, or harmful. 45 | 46 | ## Scope 47 | 48 | This Code of Conduct applies both within project spaces and in public spaces 49 | when an individual is representing the project or its community. Examples of 50 | representing a project or community include using an official project e-mail 51 | address, posting via an official social media account, or acting as an appointed 52 | representative at an online or offline event. Representation of a project may be 53 | further defined and clarified by project maintainers. 54 | 55 | ## Enforcement 56 | 57 | Instances of abusive, harassing, or otherwise unacceptable behavior may be 58 | reported by contacting the project team at open.source.conduct@willowtreeapps.com. All 59 | complaints will be reviewed and investigated and will result in a response that 60 | is deemed necessary and appropriate to the circumstances. The project team is 61 | obligated to maintain confidentiality with regard to the reporter of an incident. 62 | Further details of specific enforcement policies may be posted separately. 63 | 64 | Project maintainers who do not follow or enforce the Code of Conduct in good 65 | faith may face temporary or permanent repercussions as determined by other 66 | members of the project's leadership. 67 | 68 | ## Attribution 69 | 70 | This Code of Conduct is adapted from the [Contributor Covenant][homepage], version 1.4, 71 | available at [http://contributor-covenant.org/version/1/4][version] 72 | 73 | [homepage]: http://contributor-covenant.org 74 | [version]: http://contributor-covenant.org/version/1/4/ 75 | -------------------------------------------------------------------------------- /CONTRIBUTING.md: -------------------------------------------------------------------------------- 1 | # Contributing to Cordux 2 | 3 | Looking to contribute something to Cordux? Here's how you can help. 4 | 5 | Please take a moment to review this document in order to make the contribution process easy and effective for everyone involved. 6 | 7 | Following these guidelines helps to communicate that you respect the time of the developers managing and developing this open source project. In return, they should reciprocate that respect in addressing your issue or assessing patches and features. 8 | 9 | ## Code of Conduct 10 | 11 | WillowTree, Inc. has adopted a Code of Conduct that we expect project participants to adhere to. Please read [the full text](https://github.com/willowtreeapps/cordux/blob/develop/CODE_OF_CONDUCT.md) so that you can understand what actions will and will not be tolerated. 12 | 13 | ## Our Development Process 14 | 15 | ### Test Suite 16 | 17 | We run the unit test target in the latest Xcode 8. 18 | 19 | ### Pull Requests 20 | 21 | **Working on your first Pull Request?** You can learn how from this _free_ series [How to Contribute to an Open Source Project on GitHub](https://egghead.io/courses/how-to-contribute-to-an-open-source-project-on-github). 22 | 23 | _Before_ submitting a pull request, please make sure the following is done: 24 | 25 | 1. Fork the repo and create your branch from `develop`. 26 | 2. If you've added code that should be tested, add tests! 27 | 3. If you've changed APIs, update the documentation. 28 | 4. Ensure the test suite passes. 29 | 30 | ## Bugs 31 | 32 | ### Where to Find Known Issues 33 | 34 | We will be using GitHub Issues for our public bugs. We will keep a close eye on this and try to make it clear when we have an internal fix in progress. Before filing a new task, try to make sure your problem doesn't already exist. 35 | 36 | ### Reporting New Issues 37 | 38 | The best way to get your bug fixed is to provide a reduced test case. 39 | 40 | ## Style Guide 41 | 42 | <<< OPTIONAL - Include any code conventions, documentation conventions, linter specifics, etc >>> 43 | 44 | ## License 45 | 46 | By submitting a pull request, you represent that you have the right to license your contribution to WillowTree and the community, and agree by submitting the patch that your contributions are licensed under the [MIT License](https://github.com/willowtreeapps/cordux/blob/develop/LICENSE). 47 | -------------------------------------------------------------------------------- /Cordux.podspec: -------------------------------------------------------------------------------- 1 | Pod::Spec.new do |s| 2 | s.name = "Cordux" 3 | s.version = "0.1.7" 4 | s.summary = "App Coordinators & Redux. A framework for UIKit development." 5 | s.description = <<-DESC 6 | Cordux combines app coordiantors with a redux-like architecture. 7 | 8 | This allows rapid and powerful UI development with full support 9 | for routing and deep linking. 10 | DESC 11 | 12 | s.homepage = "http://github.com/willowtreeapps/cordux" 13 | s.license = "MIT" 14 | s.authors = { 15 | "Ian Terrell" => "ian.terrell@gmail.com", 16 | } 17 | 18 | s.source = { 19 | :git => "https://github.com/willowtreeapps/cordux.git", 20 | :tag => "v0.1.7" 21 | } 22 | 23 | s.platform = :ios, :tvos 24 | s.ios.deployment_target = "9.0" 25 | s.tvos.deployment_target = "9.1" 26 | 27 | s.source_files = "Sources/*.swift" 28 | end 29 | -------------------------------------------------------------------------------- /Cordux.xcodeproj/project.xcworkspace/contents.xcworkspacedata: -------------------------------------------------------------------------------- 1 | 2 | 4 | 6 | 7 | 8 | -------------------------------------------------------------------------------- /Cordux.xcodeproj/project.xcworkspace/xcshareddata/IDEWorkspaceChecks.plist: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | IDEDidComputeMac32BitWarning 6 | 7 | 8 | 9 | -------------------------------------------------------------------------------- /Cordux.xcodeproj/xcshareddata/xcschemes/Cordux-iOS.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 | -------------------------------------------------------------------------------- /Cordux.xcodeproj/xcshareddata/xcschemes/Cordux-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 | -------------------------------------------------------------------------------- /Cordux/Cordux.h: -------------------------------------------------------------------------------- 1 | // 2 | // Cordux.h 3 | // Cordux 4 | // 5 | // Created by Ian Terrell on 7/28/16. 6 | // Copyright © 2016 WillowTree. All rights reserved. 7 | // 8 | 9 | #import 10 | 11 | //! Project version number for Cordux. 12 | FOUNDATION_EXPORT double CorduxVersionNumber; 13 | 14 | //! Project version string for Cordux. 15 | FOUNDATION_EXPORT const unsigned char CorduxVersionString[]; 16 | 17 | // In this header, you should import all the public headers of your framework using statements like #import 18 | 19 | 20 | -------------------------------------------------------------------------------- /Cordux/Info.plist: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | CFBundleDevelopmentRegion 6 | en 7 | CFBundleExecutable 8 | $(EXECUTABLE_NAME) 9 | CFBundleIdentifier 10 | $(PRODUCT_BUNDLE_IDENTIFIER) 11 | CFBundleInfoDictionaryVersion 12 | 6.0 13 | CFBundleName 14 | $(PRODUCT_NAME) 15 | CFBundlePackageType 16 | FMWK 17 | CFBundleShortVersionString 18 | 1.0 19 | CFBundleSignature 20 | ???? 21 | CFBundleVersion 22 | $(CURRENT_PROJECT_VERSION) 23 | NSPrincipalClass 24 | 25 | 26 | 27 | -------------------------------------------------------------------------------- /CorduxTests/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 | -------------------------------------------------------------------------------- /CorduxTests/NavigationControllerMetaCoordinatorTests.swift: -------------------------------------------------------------------------------- 1 | // 2 | // NavigationControllerMetaCoordinatorTests.swift 3 | // Cordux 4 | // 5 | // Created by Ian Terrell on 12/23/16. 6 | // Copyright © 2016 WillowTree. All rights reserved. 7 | // 8 | 9 | import XCTest 10 | @testable import Cordux 11 | 12 | class NavigationControllerMetaCoordinatorTests: XCTestCase { 13 | static let store = Store(initialState: SimpleAppState(), reducer: SimpleAppReducer()) 14 | 15 | final class TestCoordinator: NavigationControllerMetaCoordinator { 16 | var store = NavigationControllerMetaCoordinatorTests.store 17 | var navigationController = UINavigationController() 18 | var coordinators: [AnyCoordinator] = [] 19 | func start(route: Route?) {} 20 | func coordinators(for route: Route) -> [AnyCoordinator] { 21 | return [] 22 | } 23 | 24 | func setCoordinators(for routes: [RouteConvertible]) { 25 | coordinators = routes.map(BasicCoordinator.init) 26 | } 27 | } 28 | 29 | var coordinator: TestCoordinator! 30 | 31 | override func setUp() { 32 | coordinator = TestCoordinator() 33 | coordinator.setCoordinators(for: ["hello", "world", "who", "there?"]) 34 | } 35 | 36 | func testNumberWithAllSharedCoordinators() { 37 | let number = coordinator.numberOfLastExistingCoordinator(for: Route(components: ["hello", "world", "who", "there?"])) 38 | XCTAssertEqual(4, number) 39 | } 40 | 41 | func testNumberWithNoSharedCoordinators() { 42 | let number = coordinator.numberOfLastExistingCoordinator(for: Route(components: ["x", "hello", "world", "who", "there?"])) 43 | XCTAssertEqual(0, number) 44 | } 45 | 46 | func testNumberWithNoSharedCoordinatorsSubset() { 47 | let number = coordinator.numberOfLastExistingCoordinator(for: Route(components: ["world", "who", "there?"])) 48 | XCTAssertEqual(0, number) 49 | } 50 | 51 | func testNumberWith1SharedCoordinators() { 52 | let number = coordinator.numberOfLastExistingCoordinator(for: Route(components: ["hello", "x", "world", "who", "there?"])) 53 | XCTAssertEqual(1, number) 54 | } 55 | 56 | func testNumberWith2SharedCoordinators() { 57 | let number = coordinator.numberOfLastExistingCoordinator(for: Route(components: ["hello", "world", "x", "who", "there?"])) 58 | XCTAssertEqual(2, number) 59 | } 60 | 61 | func testNumberWith3SharedCoordinators() { 62 | let number = coordinator.numberOfLastExistingCoordinator(for: Route(components: ["hello", "world", "who", "x", "there?"])) 63 | XCTAssertEqual(3, number) 64 | } 65 | 66 | func testNumberWithSomeSharedCoordinators() { 67 | let number = coordinator.numberOfLastExistingCoordinator(for: Route(components: ["hello", "world", "dogs", "cats"])) 68 | XCTAssertEqual(2, number) 69 | } 70 | } 71 | -------------------------------------------------------------------------------- /CorduxTests/SimpleCoordinators.swift: -------------------------------------------------------------------------------- 1 | // 2 | // SimpleCoordinators.swift 3 | // Cordux 4 | // 5 | // Created by Ian Terrell on 12/23/16. 6 | // Copyright © 2016 WillowTree. All rights reserved. 7 | // 8 | 9 | import Foundation 10 | import Cordux 11 | 12 | final class BasicCoordinator: SimpleCoordinator { 13 | let store = Store(initialState: SimpleAppState(), reducer: SimpleAppReducer()) 14 | let rootViewController = UIViewController() 15 | let route: Route 16 | 17 | init(route: RouteConvertible) { 18 | self.route = route.route() 19 | } 20 | 21 | func start(route: Route?) {} 22 | } 23 | -------------------------------------------------------------------------------- /CorduxTests/SimpleState.swift: -------------------------------------------------------------------------------- 1 | // 2 | // SimpleState.swift 3 | // Cordux 4 | // 5 | // Created by Ian Terrell on 8/22/16. 6 | // Copyright © 2016 WillowTree. All rights reserved. 7 | // 8 | 9 | import Foundation 10 | import Cordux 11 | 12 | struct SimpleAppState: StateType { 13 | var route: Route = [] 14 | var auth: SimpleAuthState = .signedOut 15 | } 16 | 17 | enum SimpleAuthState { 18 | case signedOut 19 | case signedIn(name: String) 20 | } 21 | 22 | class SimpleAppReducer: Reducer { 23 | func handleAction(_ action: Action, state: SimpleAppState) -> SimpleAppState { 24 | guard let action = action as? SimpleAction else { 25 | return state 26 | } 27 | 28 | var state = state 29 | 30 | switch action { 31 | case .signIn(let name): 32 | state.auth = .signedIn(name: name) 33 | default: 34 | break 35 | } 36 | 37 | return state 38 | } 39 | } 40 | 41 | struct SimpleAuthViewModel { 42 | let name: String 43 | } 44 | 45 | extension SimpleAuthViewModel { 46 | init?(state: SimpleAppState) { 47 | guard case let .signedIn(name) = state.auth else { 48 | return nil 49 | } 50 | 51 | self.name = name 52 | } 53 | } 54 | 55 | enum SimpleAction: Action { 56 | case noop 57 | case signIn(name: String) 58 | } 59 | -------------------------------------------------------------------------------- /CorduxTests/SubscriptionsTest.swift: -------------------------------------------------------------------------------- 1 | // 2 | // SubscriptionsTest.swift 3 | // Cordux 4 | // 5 | // Created by Ian Terrell on 8/22/16. 6 | // Copyright © 2016 WillowTree. All rights reserved. 7 | // 8 | 9 | import XCTest 10 | @testable import Cordux 11 | 12 | class SubscriptionTestsSubscriber: SubscriberType { 13 | var lastModel: SimpleAuthViewModel? 14 | func newState(_ viewModel: SimpleAuthViewModel?) { 15 | lastModel = viewModel 16 | } 17 | } 18 | 19 | class SubscriptionsTest: XCTestCase { 20 | 21 | var store: Store! 22 | 23 | override func setUp() { 24 | super.setUp() 25 | 26 | store = Store(initialState: SimpleAppState(), reducer: SimpleAppReducer()) 27 | } 28 | 29 | func testNilOptionalSubscription() { 30 | let sub = SubscriptionTestsSubscriber() 31 | store.subscribe(sub, SimpleAuthViewModel.init) 32 | store.dispatch(SimpleAction.noop) 33 | XCTAssertNil(sub.lastModel) 34 | } 35 | 36 | func testNonNilOptionalSubscription() { 37 | let sub = SubscriptionTestsSubscriber() 38 | store.subscribe(sub, SimpleAuthViewModel.init) 39 | store.dispatch(SimpleAction.signIn(name: "bob")) 40 | XCTAssertNotNil(sub.lastModel) 41 | XCTAssertEqual("bob", sub.lastModel?.name) 42 | } 43 | 44 | func testUnsubscription() { 45 | let sub = SubscriptionTestsSubscriber() 46 | XCTAssert(store.isNewSubscriber(sub)) 47 | 48 | store.subscribe(sub, SimpleAuthViewModel.init) 49 | XCTAssert(!store.isNewSubscriber(sub)) 50 | 51 | store.unsubscribe(sub) 52 | XCTAssert(store.isNewSubscriber(sub)) 53 | } 54 | } 55 | -------------------------------------------------------------------------------- /Example/.ruby-version: -------------------------------------------------------------------------------- 1 | 2.3.1 2 | -------------------------------------------------------------------------------- /Example/CREDITS.md: -------------------------------------------------------------------------------- 1 | The store, reducer, and subscription code originated in [ReSwift](https://github.com/ReSwift/ReSwift). 2 | 3 | ``` 4 | The MIT License (MIT) Copyright (c) 2016 ReSwift Contributors 5 | 6 | Permission is hereby granted, free of charge, to any person obtaining a copy of 7 | this software and associated documentation files (the "Software"), to deal in the 8 | Software without restriction, including without limitation the rights to use, copy, 9 | modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, 10 | and to permit persons to whom the Software is furnished to do so, subject to the 11 | 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 IMPLIED, 17 | INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A 18 | PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT 19 | HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION 20 | OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE 21 | SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. 22 | ``` 23 | -------------------------------------------------------------------------------- /Example/CorduxPrototype.xcodeproj/project.pbxproj: -------------------------------------------------------------------------------- 1 | // !$*UTF8*$! 2 | { 3 | archiveVersion = 1; 4 | classes = { 5 | }; 6 | objectVersion = 46; 7 | objects = { 8 | 9 | /* Begin PBXBuildFile section */ 10 | 11F987EDEA335C26DE6118EC /* Pods_CorduxPrototype.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = D68095426899A4DDA2A6DE17 /* Pods_CorduxPrototype.framework */; }; 11 | D922A4F51D465A6B002FF3E7 /* MainViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = D922A4F41D465A6B002FF3E7 /* MainViewController.swift */; }; 12 | D922A4F91D46980F002FF3E7 /* Cordux.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = D922A4F81D46980F002FF3E7 /* Cordux.framework */; }; 13 | D946E9421D41027200DB4FCF /* AppDelegate.swift in Sources */ = {isa = PBXBuildFile; fileRef = D946E9411D41027200DB4FCF /* AppDelegate.swift */; }; 14 | D946E9441D41027200DB4FCF /* FirstViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = D946E9431D41027200DB4FCF /* FirstViewController.swift */; }; 15 | D946E9461D41027200DB4FCF /* SecondViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = D946E9451D41027200DB4FCF /* SecondViewController.swift */; }; 16 | D946E94B1D41027200DB4FCF /* Assets.xcassets in Resources */ = {isa = PBXBuildFile; fileRef = D946E94A1D41027200DB4FCF /* Assets.xcassets */; }; 17 | D946E94E1D41027200DB4FCF /* LaunchScreen.storyboard in Resources */ = {isa = PBXBuildFile; fileRef = D946E94C1D41027200DB4FCF /* LaunchScreen.storyboard */; }; 18 | D946E9591D41027200DB4FCF /* CorduxPrototypeTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = D946E9581D41027200DB4FCF /* CorduxPrototypeTests.swift */; }; 19 | D946E9641D41027200DB4FCF /* CorduxPrototypeUITests.swift in Sources */ = {isa = PBXBuildFile; fileRef = D946E9631D41027200DB4FCF /* CorduxPrototypeUITests.swift */; }; 20 | D946E9721D41039E00DB4FCF /* AppCoordinator.swift in Sources */ = {isa = PBXBuildFile; fileRef = D946E9711D41039E00DB4FCF /* AppCoordinator.swift */; }; 21 | D946E9741D4103CE00DB4FCF /* AppState.swift in Sources */ = {isa = PBXBuildFile; fileRef = D946E9731D4103CE00DB4FCF /* AppState.swift */; }; 22 | D946E9761D4105DF00DB4FCF /* Catalog.storyboard in Resources */ = {isa = PBXBuildFile; fileRef = D946E9751D4105DF00DB4FCF /* Catalog.storyboard */; }; 23 | D946E9781D41064D00DB4FCF /* Main.storyboard in Resources */ = {isa = PBXBuildFile; fileRef = D946E9771D41064D00DB4FCF /* Main.storyboard */; }; 24 | D946E97D1D41078A00DB4FCF /* CatalogCoordinator.swift in Sources */ = {isa = PBXBuildFile; fileRef = D946E97C1D41078A00DB4FCF /* CatalogCoordinator.swift */; }; 25 | D946E9801D410A2400DB4FCF /* Authentication.storyboard in Resources */ = {isa = PBXBuildFile; fileRef = D946E97F1D410A2400DB4FCF /* Authentication.storyboard */; }; 26 | D946E9821D410AE600DB4FCF /* AuthenticationCoordinator.swift in Sources */ = {isa = PBXBuildFile; fileRef = D946E9811D410AE600DB4FCF /* AuthenticationCoordinator.swift */; }; 27 | D946E9841D410B0500DB4FCF /* SignInViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = D946E9831D410B0500DB4FCF /* SignInViewController.swift */; }; 28 | D946E9861D410B1500DB4FCF /* ForgotPasswordViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = D946E9851D410B1500DB4FCF /* ForgotPasswordViewController.swift */; }; 29 | /* End PBXBuildFile section */ 30 | 31 | /* Begin PBXContainerItemProxy section */ 32 | D946E9551D41027200DB4FCF /* PBXContainerItemProxy */ = { 33 | isa = PBXContainerItemProxy; 34 | containerPortal = D946E9361D41027200DB4FCF /* Project object */; 35 | proxyType = 1; 36 | remoteGlobalIDString = D946E93D1D41027200DB4FCF; 37 | remoteInfo = CorduxPrototype; 38 | }; 39 | D946E9601D41027200DB4FCF /* PBXContainerItemProxy */ = { 40 | isa = PBXContainerItemProxy; 41 | containerPortal = D946E9361D41027200DB4FCF /* Project object */; 42 | proxyType = 1; 43 | remoteGlobalIDString = D946E93D1D41027200DB4FCF; 44 | remoteInfo = CorduxPrototype; 45 | }; 46 | /* End PBXContainerItemProxy section */ 47 | 48 | /* Begin PBXFileReference section */ 49 | 54AB738FE12A7D14DACD3E78 /* Pods-CorduxPrototype.debug.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-CorduxPrototype.debug.xcconfig"; path = "Pods/Target Support Files/Pods-CorduxPrototype/Pods-CorduxPrototype.debug.xcconfig"; sourceTree = ""; }; 50 | B2AF9DBFC9F31A01AD177704 /* Pods-CorduxPrototype.release.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-CorduxPrototype.release.xcconfig"; path = "Pods/Target Support Files/Pods-CorduxPrototype/Pods-CorduxPrototype.release.xcconfig"; sourceTree = ""; }; 51 | D68095426899A4DDA2A6DE17 /* Pods_CorduxPrototype.framework */ = {isa = PBXFileReference; explicitFileType = wrapper.framework; includeInIndex = 0; path = Pods_CorduxPrototype.framework; sourceTree = BUILT_PRODUCTS_DIR; }; 52 | D922A4F41D465A6B002FF3E7 /* MainViewController.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = MainViewController.swift; sourceTree = ""; }; 53 | D922A4F61D4697C1002FF3E7 /* Pods_CorduxPrototype.framework */ = {isa = PBXFileReference; lastKnownFileType = wrapper.framework; name = Pods_CorduxPrototype.framework; path = "../../../Library/Developer/Xcode/DerivedData/CorduxPrototype-fwijtkdjhhjcmubaqbjyuijepfer/Build/Products/Debug-iphonesimulator/Pods_CorduxPrototype.framework"; sourceTree = ""; }; 54 | D922A4F81D46980F002FF3E7 /* Cordux.framework */ = {isa = PBXFileReference; lastKnownFileType = wrapper.framework; name = Cordux.framework; path = "Pods/../build/Debug-iphoneos/Cordux/Cordux.framework"; sourceTree = ""; }; 55 | D946E93E1D41027200DB4FCF /* CorduxPrototype.app */ = {isa = PBXFileReference; explicitFileType = wrapper.application; includeInIndex = 0; path = CorduxPrototype.app; sourceTree = BUILT_PRODUCTS_DIR; }; 56 | D946E9411D41027200DB4FCF /* AppDelegate.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AppDelegate.swift; sourceTree = ""; }; 57 | D946E9431D41027200DB4FCF /* FirstViewController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = FirstViewController.swift; sourceTree = ""; }; 58 | D946E9451D41027200DB4FCF /* SecondViewController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SecondViewController.swift; sourceTree = ""; }; 59 | D946E94A1D41027200DB4FCF /* Assets.xcassets */ = {isa = PBXFileReference; lastKnownFileType = folder.assetcatalog; path = Assets.xcassets; sourceTree = ""; }; 60 | D946E94D1D41027200DB4FCF /* Base */ = {isa = PBXFileReference; lastKnownFileType = file.storyboard; name = Base; path = Base.lproj/LaunchScreen.storyboard; sourceTree = ""; }; 61 | D946E94F1D41027200DB4FCF /* Info.plist */ = {isa = PBXFileReference; lastKnownFileType = text.plist.xml; path = Info.plist; sourceTree = ""; }; 62 | D946E9541D41027200DB4FCF /* CorduxPrototypeTests.xctest */ = {isa = PBXFileReference; explicitFileType = wrapper.cfbundle; includeInIndex = 0; path = CorduxPrototypeTests.xctest; sourceTree = BUILT_PRODUCTS_DIR; }; 63 | D946E9581D41027200DB4FCF /* CorduxPrototypeTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CorduxPrototypeTests.swift; sourceTree = ""; }; 64 | D946E95A1D41027200DB4FCF /* Info.plist */ = {isa = PBXFileReference; lastKnownFileType = text.plist.xml; path = Info.plist; sourceTree = ""; }; 65 | D946E95F1D41027200DB4FCF /* CorduxPrototypeUITests.xctest */ = {isa = PBXFileReference; explicitFileType = wrapper.cfbundle; includeInIndex = 0; path = CorduxPrototypeUITests.xctest; sourceTree = BUILT_PRODUCTS_DIR; }; 66 | D946E9631D41027200DB4FCF /* CorduxPrototypeUITests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CorduxPrototypeUITests.swift; sourceTree = ""; }; 67 | D946E9651D41027200DB4FCF /* Info.plist */ = {isa = PBXFileReference; lastKnownFileType = text.plist.xml; path = Info.plist; sourceTree = ""; }; 68 | D946E9711D41039E00DB4FCF /* AppCoordinator.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = AppCoordinator.swift; sourceTree = ""; }; 69 | D946E9731D4103CE00DB4FCF /* AppState.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = AppState.swift; sourceTree = ""; }; 70 | D946E9751D4105DF00DB4FCF /* Catalog.storyboard */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = file.storyboard; path = Catalog.storyboard; sourceTree = ""; }; 71 | D946E9771D41064D00DB4FCF /* Main.storyboard */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = file.storyboard; path = Main.storyboard; sourceTree = ""; }; 72 | D946E97C1D41078A00DB4FCF /* CatalogCoordinator.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = CatalogCoordinator.swift; sourceTree = ""; }; 73 | D946E97F1D410A2400DB4FCF /* Authentication.storyboard */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = file.storyboard; path = Authentication.storyboard; sourceTree = ""; }; 74 | D946E9811D410AE600DB4FCF /* AuthenticationCoordinator.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = AuthenticationCoordinator.swift; sourceTree = ""; }; 75 | D946E9831D410B0500DB4FCF /* SignInViewController.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = SignInViewController.swift; sourceTree = ""; }; 76 | D946E9851D410B1500DB4FCF /* ForgotPasswordViewController.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = ForgotPasswordViewController.swift; sourceTree = ""; }; 77 | /* End PBXFileReference section */ 78 | 79 | /* Begin PBXFrameworksBuildPhase section */ 80 | D946E93B1D41027200DB4FCF /* Frameworks */ = { 81 | isa = PBXFrameworksBuildPhase; 82 | buildActionMask = 2147483647; 83 | files = ( 84 | D922A4F91D46980F002FF3E7 /* Cordux.framework in Frameworks */, 85 | 11F987EDEA335C26DE6118EC /* Pods_CorduxPrototype.framework in Frameworks */, 86 | ); 87 | runOnlyForDeploymentPostprocessing = 0; 88 | }; 89 | D946E9511D41027200DB4FCF /* Frameworks */ = { 90 | isa = PBXFrameworksBuildPhase; 91 | buildActionMask = 2147483647; 92 | files = ( 93 | ); 94 | runOnlyForDeploymentPostprocessing = 0; 95 | }; 96 | D946E95C1D41027200DB4FCF /* Frameworks */ = { 97 | isa = PBXFrameworksBuildPhase; 98 | buildActionMask = 2147483647; 99 | files = ( 100 | ); 101 | runOnlyForDeploymentPostprocessing = 0; 102 | }; 103 | /* End PBXFrameworksBuildPhase section */ 104 | 105 | /* Begin PBXGroup section */ 106 | 19007DAF54767E72B45238B6 /* Frameworks */ = { 107 | isa = PBXGroup; 108 | children = ( 109 | D922A4F81D46980F002FF3E7 /* Cordux.framework */, 110 | D922A4F61D4697C1002FF3E7 /* Pods_CorduxPrototype.framework */, 111 | D68095426899A4DDA2A6DE17 /* Pods_CorduxPrototype.framework */, 112 | ); 113 | name = Frameworks; 114 | sourceTree = ""; 115 | }; 116 | 4D2C81F7E511D38610FEE565 /* Pods */ = { 117 | isa = PBXGroup; 118 | children = ( 119 | 54AB738FE12A7D14DACD3E78 /* Pods-CorduxPrototype.debug.xcconfig */, 120 | B2AF9DBFC9F31A01AD177704 /* Pods-CorduxPrototype.release.xcconfig */, 121 | ); 122 | name = Pods; 123 | sourceTree = ""; 124 | }; 125 | D946E9351D41027200DB4FCF = { 126 | isa = PBXGroup; 127 | children = ( 128 | D946E9401D41027200DB4FCF /* CorduxPrototype */, 129 | D946E9571D41027200DB4FCF /* CorduxPrototypeTests */, 130 | D946E9621D41027200DB4FCF /* CorduxPrototypeUITests */, 131 | D946E93F1D41027200DB4FCF /* Products */, 132 | 19007DAF54767E72B45238B6 /* Frameworks */, 133 | 4D2C81F7E511D38610FEE565 /* Pods */, 134 | ); 135 | sourceTree = ""; 136 | }; 137 | D946E93F1D41027200DB4FCF /* Products */ = { 138 | isa = PBXGroup; 139 | children = ( 140 | D946E93E1D41027200DB4FCF /* CorduxPrototype.app */, 141 | D946E9541D41027200DB4FCF /* CorduxPrototypeTests.xctest */, 142 | D946E95F1D41027200DB4FCF /* CorduxPrototypeUITests.xctest */, 143 | ); 144 | name = Products; 145 | sourceTree = ""; 146 | }; 147 | D946E9401D41027200DB4FCF /* CorduxPrototype */ = { 148 | isa = PBXGroup; 149 | children = ( 150 | D946E9411D41027200DB4FCF /* AppDelegate.swift */, 151 | D946E9731D4103CE00DB4FCF /* AppState.swift */, 152 | D946E9791D41065C00DB4FCF /* Scenes */, 153 | D946E94A1D41027200DB4FCF /* Assets.xcassets */, 154 | D946E94C1D41027200DB4FCF /* LaunchScreen.storyboard */, 155 | D946E94F1D41027200DB4FCF /* Info.plist */, 156 | ); 157 | path = CorduxPrototype; 158 | sourceTree = ""; 159 | }; 160 | D946E9571D41027200DB4FCF /* CorduxPrototypeTests */ = { 161 | isa = PBXGroup; 162 | children = ( 163 | D946E9581D41027200DB4FCF /* CorduxPrototypeTests.swift */, 164 | D946E95A1D41027200DB4FCF /* Info.plist */, 165 | ); 166 | path = CorduxPrototypeTests; 167 | sourceTree = ""; 168 | }; 169 | D946E9621D41027200DB4FCF /* CorduxPrototypeUITests */ = { 170 | isa = PBXGroup; 171 | children = ( 172 | D946E9631D41027200DB4FCF /* CorduxPrototypeUITests.swift */, 173 | D946E9651D41027200DB4FCF /* Info.plist */, 174 | ); 175 | path = CorduxPrototypeUITests; 176 | sourceTree = ""; 177 | }; 178 | D946E9791D41065C00DB4FCF /* Scenes */ = { 179 | isa = PBXGroup; 180 | children = ( 181 | D946E97A1D41066900DB4FCF /* App */, 182 | D946E97E1D410A1700DB4FCF /* Authentication */, 183 | D946E97B1D41074A00DB4FCF /* Catalog */, 184 | ); 185 | name = Scenes; 186 | sourceTree = ""; 187 | }; 188 | D946E97A1D41066900DB4FCF /* App */ = { 189 | isa = PBXGroup; 190 | children = ( 191 | D946E9771D41064D00DB4FCF /* Main.storyboard */, 192 | D946E9711D41039E00DB4FCF /* AppCoordinator.swift */, 193 | D922A4F41D465A6B002FF3E7 /* MainViewController.swift */, 194 | ); 195 | name = App; 196 | sourceTree = ""; 197 | }; 198 | D946E97B1D41074A00DB4FCF /* Catalog */ = { 199 | isa = PBXGroup; 200 | children = ( 201 | D946E97C1D41078A00DB4FCF /* CatalogCoordinator.swift */, 202 | D946E9751D4105DF00DB4FCF /* Catalog.storyboard */, 203 | D946E9431D41027200DB4FCF /* FirstViewController.swift */, 204 | D946E9451D41027200DB4FCF /* SecondViewController.swift */, 205 | ); 206 | name = Catalog; 207 | sourceTree = ""; 208 | }; 209 | D946E97E1D410A1700DB4FCF /* Authentication */ = { 210 | isa = PBXGroup; 211 | children = ( 212 | D946E9811D410AE600DB4FCF /* AuthenticationCoordinator.swift */, 213 | D946E97F1D410A2400DB4FCF /* Authentication.storyboard */, 214 | D946E9831D410B0500DB4FCF /* SignInViewController.swift */, 215 | D946E9851D410B1500DB4FCF /* ForgotPasswordViewController.swift */, 216 | ); 217 | name = Authentication; 218 | sourceTree = ""; 219 | }; 220 | /* End PBXGroup section */ 221 | 222 | /* Begin PBXNativeTarget section */ 223 | D946E93D1D41027200DB4FCF /* CorduxPrototype */ = { 224 | isa = PBXNativeTarget; 225 | buildConfigurationList = D946E9681D41027200DB4FCF /* Build configuration list for PBXNativeTarget "CorduxPrototype" */; 226 | buildPhases = ( 227 | 895DFEE12740A16FE021B9F6 /* [CP] Check Pods Manifest.lock */, 228 | D946E93A1D41027200DB4FCF /* Sources */, 229 | D946E93B1D41027200DB4FCF /* Frameworks */, 230 | D946E93C1D41027200DB4FCF /* Resources */, 231 | B1CB19B0F0318728D98E0C4D /* [CP] Embed Pods Frameworks */, 232 | C6907585B2FAEA8147E749B2 /* [CP] Copy Pods Resources */, 233 | ); 234 | buildRules = ( 235 | ); 236 | dependencies = ( 237 | ); 238 | name = CorduxPrototype; 239 | productName = CorduxPrototype; 240 | productReference = D946E93E1D41027200DB4FCF /* CorduxPrototype.app */; 241 | productType = "com.apple.product-type.application"; 242 | }; 243 | D946E9531D41027200DB4FCF /* CorduxPrototypeTests */ = { 244 | isa = PBXNativeTarget; 245 | buildConfigurationList = D946E96B1D41027200DB4FCF /* Build configuration list for PBXNativeTarget "CorduxPrototypeTests" */; 246 | buildPhases = ( 247 | D946E9501D41027200DB4FCF /* Sources */, 248 | D946E9511D41027200DB4FCF /* Frameworks */, 249 | D946E9521D41027200DB4FCF /* Resources */, 250 | ); 251 | buildRules = ( 252 | ); 253 | dependencies = ( 254 | D946E9561D41027200DB4FCF /* PBXTargetDependency */, 255 | ); 256 | name = CorduxPrototypeTests; 257 | productName = CorduxPrototypeTests; 258 | productReference = D946E9541D41027200DB4FCF /* CorduxPrototypeTests.xctest */; 259 | productType = "com.apple.product-type.bundle.unit-test"; 260 | }; 261 | D946E95E1D41027200DB4FCF /* CorduxPrototypeUITests */ = { 262 | isa = PBXNativeTarget; 263 | buildConfigurationList = D946E96E1D41027200DB4FCF /* Build configuration list for PBXNativeTarget "CorduxPrototypeUITests" */; 264 | buildPhases = ( 265 | D946E95B1D41027200DB4FCF /* Sources */, 266 | D946E95C1D41027200DB4FCF /* Frameworks */, 267 | D946E95D1D41027200DB4FCF /* Resources */, 268 | ); 269 | buildRules = ( 270 | ); 271 | dependencies = ( 272 | D946E9611D41027200DB4FCF /* PBXTargetDependency */, 273 | ); 274 | name = CorduxPrototypeUITests; 275 | productName = CorduxPrototypeUITests; 276 | productReference = D946E95F1D41027200DB4FCF /* CorduxPrototypeUITests.xctest */; 277 | productType = "com.apple.product-type.bundle.ui-testing"; 278 | }; 279 | /* End PBXNativeTarget section */ 280 | 281 | /* Begin PBXProject section */ 282 | D946E9361D41027200DB4FCF /* Project object */ = { 283 | isa = PBXProject; 284 | attributes = { 285 | LastSwiftUpdateCheck = 0730; 286 | LastUpgradeCheck = 0810; 287 | ORGANIZATIONNAME = WillowTree; 288 | TargetAttributes = { 289 | D946E93D1D41027200DB4FCF = { 290 | CreatedOnToolsVersion = 7.3.1; 291 | LastSwiftMigration = 0800; 292 | }; 293 | D946E9531D41027200DB4FCF = { 294 | CreatedOnToolsVersion = 7.3.1; 295 | LastSwiftMigration = 0800; 296 | TestTargetID = D946E93D1D41027200DB4FCF; 297 | }; 298 | D946E95E1D41027200DB4FCF = { 299 | CreatedOnToolsVersion = 7.3.1; 300 | LastSwiftMigration = 0800; 301 | TestTargetID = D946E93D1D41027200DB4FCF; 302 | }; 303 | }; 304 | }; 305 | buildConfigurationList = D946E9391D41027200DB4FCF /* Build configuration list for PBXProject "CorduxPrototype" */; 306 | compatibilityVersion = "Xcode 3.2"; 307 | developmentRegion = English; 308 | hasScannedForEncodings = 0; 309 | knownRegions = ( 310 | en, 311 | Base, 312 | ); 313 | mainGroup = D946E9351D41027200DB4FCF; 314 | productRefGroup = D946E93F1D41027200DB4FCF /* Products */; 315 | projectDirPath = ""; 316 | projectRoot = ""; 317 | targets = ( 318 | D946E93D1D41027200DB4FCF /* CorduxPrototype */, 319 | D946E9531D41027200DB4FCF /* CorduxPrototypeTests */, 320 | D946E95E1D41027200DB4FCF /* CorduxPrototypeUITests */, 321 | ); 322 | }; 323 | /* End PBXProject section */ 324 | 325 | /* Begin PBXResourcesBuildPhase section */ 326 | D946E93C1D41027200DB4FCF /* Resources */ = { 327 | isa = PBXResourcesBuildPhase; 328 | buildActionMask = 2147483647; 329 | files = ( 330 | D946E94E1D41027200DB4FCF /* LaunchScreen.storyboard in Resources */, 331 | D946E94B1D41027200DB4FCF /* Assets.xcassets in Resources */, 332 | D946E9761D4105DF00DB4FCF /* Catalog.storyboard in Resources */, 333 | D946E9781D41064D00DB4FCF /* Main.storyboard in Resources */, 334 | D946E9801D410A2400DB4FCF /* Authentication.storyboard in Resources */, 335 | ); 336 | runOnlyForDeploymentPostprocessing = 0; 337 | }; 338 | D946E9521D41027200DB4FCF /* Resources */ = { 339 | isa = PBXResourcesBuildPhase; 340 | buildActionMask = 2147483647; 341 | files = ( 342 | ); 343 | runOnlyForDeploymentPostprocessing = 0; 344 | }; 345 | D946E95D1D41027200DB4FCF /* Resources */ = { 346 | isa = PBXResourcesBuildPhase; 347 | buildActionMask = 2147483647; 348 | files = ( 349 | ); 350 | runOnlyForDeploymentPostprocessing = 0; 351 | }; 352 | /* End PBXResourcesBuildPhase section */ 353 | 354 | /* Begin PBXShellScriptBuildPhase section */ 355 | 895DFEE12740A16FE021B9F6 /* [CP] Check Pods Manifest.lock */ = { 356 | isa = PBXShellScriptBuildPhase; 357 | buildActionMask = 2147483647; 358 | files = ( 359 | ); 360 | inputPaths = ( 361 | ); 362 | name = "[CP] Check Pods Manifest.lock"; 363 | outputPaths = ( 364 | ); 365 | runOnlyForDeploymentPostprocessing = 0; 366 | shellPath = /bin/sh; 367 | shellScript = "diff \"${PODS_ROOT}/../Podfile.lock\" \"${PODS_ROOT}/Manifest.lock\" > /dev/null\nif [[ $? != 0 ]] ; then\n cat << EOM\nerror: The sandbox is not in sync with the Podfile.lock. Run 'pod install' or update your CocoaPods installation.\nEOM\n exit 1\nfi\n"; 368 | showEnvVarsInLog = 0; 369 | }; 370 | B1CB19B0F0318728D98E0C4D /* [CP] Embed Pods Frameworks */ = { 371 | isa = PBXShellScriptBuildPhase; 372 | buildActionMask = 2147483647; 373 | files = ( 374 | ); 375 | inputPaths = ( 376 | ); 377 | name = "[CP] Embed Pods Frameworks"; 378 | outputPaths = ( 379 | ); 380 | runOnlyForDeploymentPostprocessing = 0; 381 | shellPath = /bin/sh; 382 | shellScript = "\"${SRCROOT}/Pods/Target Support Files/Pods-CorduxPrototype/Pods-CorduxPrototype-frameworks.sh\"\n"; 383 | showEnvVarsInLog = 0; 384 | }; 385 | C6907585B2FAEA8147E749B2 /* [CP] Copy Pods Resources */ = { 386 | isa = PBXShellScriptBuildPhase; 387 | buildActionMask = 2147483647; 388 | files = ( 389 | ); 390 | inputPaths = ( 391 | ); 392 | name = "[CP] Copy Pods Resources"; 393 | outputPaths = ( 394 | ); 395 | runOnlyForDeploymentPostprocessing = 0; 396 | shellPath = /bin/sh; 397 | shellScript = "\"${SRCROOT}/Pods/Target Support Files/Pods-CorduxPrototype/Pods-CorduxPrototype-resources.sh\"\n"; 398 | showEnvVarsInLog = 0; 399 | }; 400 | /* End PBXShellScriptBuildPhase section */ 401 | 402 | /* Begin PBXSourcesBuildPhase section */ 403 | D946E93A1D41027200DB4FCF /* Sources */ = { 404 | isa = PBXSourcesBuildPhase; 405 | buildActionMask = 2147483647; 406 | files = ( 407 | D946E9721D41039E00DB4FCF /* AppCoordinator.swift in Sources */, 408 | D946E9821D410AE600DB4FCF /* AuthenticationCoordinator.swift in Sources */, 409 | D946E9741D4103CE00DB4FCF /* AppState.swift in Sources */, 410 | D946E9461D41027200DB4FCF /* SecondViewController.swift in Sources */, 411 | D946E9841D410B0500DB4FCF /* SignInViewController.swift in Sources */, 412 | D922A4F51D465A6B002FF3E7 /* MainViewController.swift in Sources */, 413 | D946E9861D410B1500DB4FCF /* ForgotPasswordViewController.swift in Sources */, 414 | D946E9421D41027200DB4FCF /* AppDelegate.swift in Sources */, 415 | D946E97D1D41078A00DB4FCF /* CatalogCoordinator.swift in Sources */, 416 | D946E9441D41027200DB4FCF /* FirstViewController.swift in Sources */, 417 | ); 418 | runOnlyForDeploymentPostprocessing = 0; 419 | }; 420 | D946E9501D41027200DB4FCF /* Sources */ = { 421 | isa = PBXSourcesBuildPhase; 422 | buildActionMask = 2147483647; 423 | files = ( 424 | D946E9591D41027200DB4FCF /* CorduxPrototypeTests.swift in Sources */, 425 | ); 426 | runOnlyForDeploymentPostprocessing = 0; 427 | }; 428 | D946E95B1D41027200DB4FCF /* Sources */ = { 429 | isa = PBXSourcesBuildPhase; 430 | buildActionMask = 2147483647; 431 | files = ( 432 | D946E9641D41027200DB4FCF /* CorduxPrototypeUITests.swift in Sources */, 433 | ); 434 | runOnlyForDeploymentPostprocessing = 0; 435 | }; 436 | /* End PBXSourcesBuildPhase section */ 437 | 438 | /* Begin PBXTargetDependency section */ 439 | D946E9561D41027200DB4FCF /* PBXTargetDependency */ = { 440 | isa = PBXTargetDependency; 441 | target = D946E93D1D41027200DB4FCF /* CorduxPrototype */; 442 | targetProxy = D946E9551D41027200DB4FCF /* PBXContainerItemProxy */; 443 | }; 444 | D946E9611D41027200DB4FCF /* PBXTargetDependency */ = { 445 | isa = PBXTargetDependency; 446 | target = D946E93D1D41027200DB4FCF /* CorduxPrototype */; 447 | targetProxy = D946E9601D41027200DB4FCF /* PBXContainerItemProxy */; 448 | }; 449 | /* End PBXTargetDependency section */ 450 | 451 | /* Begin PBXVariantGroup section */ 452 | D946E94C1D41027200DB4FCF /* LaunchScreen.storyboard */ = { 453 | isa = PBXVariantGroup; 454 | children = ( 455 | D946E94D1D41027200DB4FCF /* Base */, 456 | ); 457 | name = LaunchScreen.storyboard; 458 | sourceTree = ""; 459 | }; 460 | /* End PBXVariantGroup section */ 461 | 462 | /* Begin XCBuildConfiguration section */ 463 | D946E9661D41027200DB4FCF /* Debug */ = { 464 | isa = XCBuildConfiguration; 465 | buildSettings = { 466 | ALWAYS_SEARCH_USER_PATHS = NO; 467 | CLANG_ANALYZER_NONNULL = YES; 468 | CLANG_CXX_LANGUAGE_STANDARD = "gnu++0x"; 469 | CLANG_CXX_LIBRARY = "libc++"; 470 | CLANG_ENABLE_MODULES = YES; 471 | CLANG_ENABLE_OBJC_ARC = YES; 472 | CLANG_WARN_BOOL_CONVERSION = YES; 473 | CLANG_WARN_CONSTANT_CONVERSION = YES; 474 | CLANG_WARN_DIRECT_OBJC_ISA_USAGE = YES_ERROR; 475 | CLANG_WARN_EMPTY_BODY = YES; 476 | CLANG_WARN_ENUM_CONVERSION = YES; 477 | CLANG_WARN_INFINITE_RECURSION = YES; 478 | CLANG_WARN_INT_CONVERSION = YES; 479 | CLANG_WARN_OBJC_ROOT_CLASS = YES_ERROR; 480 | CLANG_WARN_SUSPICIOUS_MOVE = YES; 481 | CLANG_WARN_UNREACHABLE_CODE = YES; 482 | CLANG_WARN__DUPLICATE_METHOD_MATCH = YES; 483 | "CODE_SIGN_IDENTITY[sdk=iphoneos*]" = "iPhone Developer"; 484 | COPY_PHASE_STRIP = NO; 485 | DEBUG_INFORMATION_FORMAT = dwarf; 486 | ENABLE_STRICT_OBJC_MSGSEND = YES; 487 | ENABLE_TESTABILITY = YES; 488 | GCC_C_LANGUAGE_STANDARD = gnu99; 489 | GCC_DYNAMIC_NO_PIC = NO; 490 | GCC_NO_COMMON_BLOCKS = YES; 491 | GCC_OPTIMIZATION_LEVEL = 0; 492 | GCC_PREPROCESSOR_DEFINITIONS = ( 493 | "DEBUG=1", 494 | "$(inherited)", 495 | ); 496 | GCC_WARN_64_TO_32_BIT_CONVERSION = YES; 497 | GCC_WARN_ABOUT_RETURN_TYPE = YES_ERROR; 498 | GCC_WARN_UNDECLARED_SELECTOR = YES; 499 | GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE; 500 | GCC_WARN_UNUSED_FUNCTION = YES; 501 | GCC_WARN_UNUSED_VARIABLE = YES; 502 | IPHONEOS_DEPLOYMENT_TARGET = 9.3; 503 | MTL_ENABLE_DEBUG_INFO = YES; 504 | ONLY_ACTIVE_ARCH = YES; 505 | SDKROOT = iphoneos; 506 | SWIFT_OPTIMIZATION_LEVEL = "-Onone"; 507 | TARGETED_DEVICE_FAMILY = "1,2"; 508 | }; 509 | name = Debug; 510 | }; 511 | D946E9671D41027200DB4FCF /* Release */ = { 512 | isa = XCBuildConfiguration; 513 | buildSettings = { 514 | ALWAYS_SEARCH_USER_PATHS = NO; 515 | CLANG_ANALYZER_NONNULL = YES; 516 | CLANG_CXX_LANGUAGE_STANDARD = "gnu++0x"; 517 | CLANG_CXX_LIBRARY = "libc++"; 518 | CLANG_ENABLE_MODULES = YES; 519 | CLANG_ENABLE_OBJC_ARC = YES; 520 | CLANG_WARN_BOOL_CONVERSION = YES; 521 | CLANG_WARN_CONSTANT_CONVERSION = YES; 522 | CLANG_WARN_DIRECT_OBJC_ISA_USAGE = YES_ERROR; 523 | CLANG_WARN_EMPTY_BODY = YES; 524 | CLANG_WARN_ENUM_CONVERSION = YES; 525 | CLANG_WARN_INFINITE_RECURSION = YES; 526 | CLANG_WARN_INT_CONVERSION = YES; 527 | CLANG_WARN_OBJC_ROOT_CLASS = YES_ERROR; 528 | CLANG_WARN_SUSPICIOUS_MOVE = YES; 529 | CLANG_WARN_UNREACHABLE_CODE = YES; 530 | CLANG_WARN__DUPLICATE_METHOD_MATCH = YES; 531 | "CODE_SIGN_IDENTITY[sdk=iphoneos*]" = "iPhone Developer"; 532 | COPY_PHASE_STRIP = NO; 533 | DEBUG_INFORMATION_FORMAT = "dwarf-with-dsym"; 534 | ENABLE_NS_ASSERTIONS = NO; 535 | ENABLE_STRICT_OBJC_MSGSEND = YES; 536 | GCC_C_LANGUAGE_STANDARD = gnu99; 537 | GCC_NO_COMMON_BLOCKS = YES; 538 | GCC_WARN_64_TO_32_BIT_CONVERSION = YES; 539 | GCC_WARN_ABOUT_RETURN_TYPE = YES_ERROR; 540 | GCC_WARN_UNDECLARED_SELECTOR = YES; 541 | GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE; 542 | GCC_WARN_UNUSED_FUNCTION = YES; 543 | GCC_WARN_UNUSED_VARIABLE = YES; 544 | IPHONEOS_DEPLOYMENT_TARGET = 9.3; 545 | MTL_ENABLE_DEBUG_INFO = NO; 546 | SDKROOT = iphoneos; 547 | TARGETED_DEVICE_FAMILY = "1,2"; 548 | VALIDATE_PRODUCT = YES; 549 | }; 550 | name = Release; 551 | }; 552 | D946E9691D41027200DB4FCF /* Debug */ = { 553 | isa = XCBuildConfiguration; 554 | baseConfigurationReference = 54AB738FE12A7D14DACD3E78 /* Pods-CorduxPrototype.debug.xcconfig */; 555 | buildSettings = { 556 | ALWAYS_EMBED_SWIFT_STANDARD_LIBRARIES = YES; 557 | ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon; 558 | INFOPLIST_FILE = CorduxPrototype/Info.plist; 559 | LD_RUNPATH_SEARCH_PATHS = "$(inherited) @executable_path/Frameworks"; 560 | PRODUCT_BUNDLE_IDENTIFIER = com.willowtreeapps.CorduxPrototype; 561 | PRODUCT_NAME = "$(TARGET_NAME)"; 562 | SWIFT_VERSION = 3.0; 563 | }; 564 | name = Debug; 565 | }; 566 | D946E96A1D41027200DB4FCF /* Release */ = { 567 | isa = XCBuildConfiguration; 568 | baseConfigurationReference = B2AF9DBFC9F31A01AD177704 /* Pods-CorduxPrototype.release.xcconfig */; 569 | buildSettings = { 570 | ALWAYS_EMBED_SWIFT_STANDARD_LIBRARIES = YES; 571 | ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon; 572 | INFOPLIST_FILE = CorduxPrototype/Info.plist; 573 | LD_RUNPATH_SEARCH_PATHS = "$(inherited) @executable_path/Frameworks"; 574 | PRODUCT_BUNDLE_IDENTIFIER = com.willowtreeapps.CorduxPrototype; 575 | PRODUCT_NAME = "$(TARGET_NAME)"; 576 | SWIFT_OPTIMIZATION_LEVEL = "-Owholemodule"; 577 | SWIFT_VERSION = 3.0; 578 | }; 579 | name = Release; 580 | }; 581 | D946E96C1D41027200DB4FCF /* Debug */ = { 582 | isa = XCBuildConfiguration; 583 | buildSettings = { 584 | BUNDLE_LOADER = "$(TEST_HOST)"; 585 | INFOPLIST_FILE = CorduxPrototypeTests/Info.plist; 586 | LD_RUNPATH_SEARCH_PATHS = "$(inherited) @executable_path/Frameworks @loader_path/Frameworks"; 587 | PRODUCT_BUNDLE_IDENTIFIER = com.willowtreeapps.CorduxPrototypeTests; 588 | PRODUCT_NAME = "$(TARGET_NAME)"; 589 | SWIFT_VERSION = 3.0; 590 | TEST_HOST = "$(BUILT_PRODUCTS_DIR)/CorduxPrototype.app/CorduxPrototype"; 591 | }; 592 | name = Debug; 593 | }; 594 | D946E96D1D41027200DB4FCF /* Release */ = { 595 | isa = XCBuildConfiguration; 596 | buildSettings = { 597 | BUNDLE_LOADER = "$(TEST_HOST)"; 598 | INFOPLIST_FILE = CorduxPrototypeTests/Info.plist; 599 | LD_RUNPATH_SEARCH_PATHS = "$(inherited) @executable_path/Frameworks @loader_path/Frameworks"; 600 | PRODUCT_BUNDLE_IDENTIFIER = com.willowtreeapps.CorduxPrototypeTests; 601 | PRODUCT_NAME = "$(TARGET_NAME)"; 602 | SWIFT_OPTIMIZATION_LEVEL = "-Owholemodule"; 603 | SWIFT_VERSION = 3.0; 604 | TEST_HOST = "$(BUILT_PRODUCTS_DIR)/CorduxPrototype.app/CorduxPrototype"; 605 | }; 606 | name = Release; 607 | }; 608 | D946E96F1D41027200DB4FCF /* Debug */ = { 609 | isa = XCBuildConfiguration; 610 | buildSettings = { 611 | INFOPLIST_FILE = CorduxPrototypeUITests/Info.plist; 612 | LD_RUNPATH_SEARCH_PATHS = "$(inherited) @executable_path/Frameworks @loader_path/Frameworks"; 613 | PRODUCT_BUNDLE_IDENTIFIER = com.willowtreeapps.CorduxPrototypeUITests; 614 | PRODUCT_NAME = "$(TARGET_NAME)"; 615 | SWIFT_VERSION = 3.0; 616 | TEST_TARGET_NAME = CorduxPrototype; 617 | }; 618 | name = Debug; 619 | }; 620 | D946E9701D41027200DB4FCF /* Release */ = { 621 | isa = XCBuildConfiguration; 622 | buildSettings = { 623 | INFOPLIST_FILE = CorduxPrototypeUITests/Info.plist; 624 | LD_RUNPATH_SEARCH_PATHS = "$(inherited) @executable_path/Frameworks @loader_path/Frameworks"; 625 | PRODUCT_BUNDLE_IDENTIFIER = com.willowtreeapps.CorduxPrototypeUITests; 626 | PRODUCT_NAME = "$(TARGET_NAME)"; 627 | SWIFT_OPTIMIZATION_LEVEL = "-Owholemodule"; 628 | SWIFT_VERSION = 3.0; 629 | TEST_TARGET_NAME = CorduxPrototype; 630 | }; 631 | name = Release; 632 | }; 633 | /* End XCBuildConfiguration section */ 634 | 635 | /* Begin XCConfigurationList section */ 636 | D946E9391D41027200DB4FCF /* Build configuration list for PBXProject "CorduxPrototype" */ = { 637 | isa = XCConfigurationList; 638 | buildConfigurations = ( 639 | D946E9661D41027200DB4FCF /* Debug */, 640 | D946E9671D41027200DB4FCF /* Release */, 641 | ); 642 | defaultConfigurationIsVisible = 0; 643 | defaultConfigurationName = Release; 644 | }; 645 | D946E9681D41027200DB4FCF /* Build configuration list for PBXNativeTarget "CorduxPrototype" */ = { 646 | isa = XCConfigurationList; 647 | buildConfigurations = ( 648 | D946E9691D41027200DB4FCF /* Debug */, 649 | D946E96A1D41027200DB4FCF /* Release */, 650 | ); 651 | defaultConfigurationIsVisible = 0; 652 | defaultConfigurationName = Release; 653 | }; 654 | D946E96B1D41027200DB4FCF /* Build configuration list for PBXNativeTarget "CorduxPrototypeTests" */ = { 655 | isa = XCConfigurationList; 656 | buildConfigurations = ( 657 | D946E96C1D41027200DB4FCF /* Debug */, 658 | D946E96D1D41027200DB4FCF /* Release */, 659 | ); 660 | defaultConfigurationIsVisible = 0; 661 | defaultConfigurationName = Release; 662 | }; 663 | D946E96E1D41027200DB4FCF /* Build configuration list for PBXNativeTarget "CorduxPrototypeUITests" */ = { 664 | isa = XCConfigurationList; 665 | buildConfigurations = ( 666 | D946E96F1D41027200DB4FCF /* Debug */, 667 | D946E9701D41027200DB4FCF /* Release */, 668 | ); 669 | defaultConfigurationIsVisible = 0; 670 | defaultConfigurationName = Release; 671 | }; 672 | /* End XCConfigurationList section */ 673 | }; 674 | rootObject = D946E9361D41027200DB4FCF /* Project object */; 675 | } 676 | -------------------------------------------------------------------------------- /Example/CorduxPrototype.xcodeproj/project.xcworkspace/contents.xcworkspacedata: -------------------------------------------------------------------------------- 1 | 2 | 4 | 6 | 7 | 8 | -------------------------------------------------------------------------------- /Example/CorduxPrototype.xcworkspace/contents.xcworkspacedata: -------------------------------------------------------------------------------- 1 | 2 | 4 | 6 | 7 | 9 | 10 | 11 | -------------------------------------------------------------------------------- /Example/CorduxPrototype/AppCoordinator.swift: -------------------------------------------------------------------------------- 1 | // 2 | // AppCoordinator.swift 3 | // CorduxPrototype 4 | // 5 | // Created by Ian Terrell on 7/21/16. 6 | // Copyright © 2016 WillowTree. All rights reserved. 7 | // 8 | 9 | import UIKit 10 | import Cordux 11 | 12 | final class AppCoordinator: SceneCoordinator { 13 | enum RouteSegment: String, RouteConvertible { 14 | case auth 15 | case catalog 16 | } 17 | 18 | let store: Store 19 | let container: UIViewController 20 | 21 | var currentScene: Scene? 22 | 23 | var rootViewController: UIViewController { 24 | return container 25 | } 26 | 27 | init(store: Store, container: UIViewController) { 28 | self.store = store 29 | self.container = container 30 | } 31 | 32 | func start(route: Route?) { 33 | store.rootCoordinator = self 34 | } 35 | 36 | public func coordinatorForTag(_ tag: String) -> AnyCoordinator? { 37 | guard let segment = RouteSegment(rawValue: tag) else { 38 | return nil 39 | } 40 | 41 | switch segment { 42 | case .auth: 43 | return AuthenticationCoordinator(store: store) 44 | case .catalog: 45 | return CatalogContainerCoordinator(store: store, rootCoordinator: CatalogCoordinator(store: store)) 46 | } 47 | } 48 | 49 | public func presentCoordinator(_ coordinator: AnyCoordinator?, completionHandler: @escaping () -> Void) { 50 | let old = container.childViewControllers.first 51 | guard let new = coordinator?.rootViewController else { 52 | completionHandler() 53 | return 54 | } 55 | 56 | old?.willMove(toParentViewController: nil) 57 | container.addChildViewController(new) 58 | container.view.addSubview(new.view) 59 | 60 | var constraints: [NSLayoutConstraint] = [] 61 | constraints.append(new.view.leftAnchor.constraint(equalTo: container.view.leftAnchor)) 62 | constraints.append(new.view.rightAnchor.constraint(equalTo: container.view.rightAnchor)) 63 | constraints.append(new.view.topAnchor.constraint(equalTo: container.view.topAnchor)) 64 | constraints.append(new.view.bottomAnchor.constraint(equalTo: container.view.bottomAnchor)) 65 | NSLayoutConstraint.activate(constraints) 66 | 67 | new.view.alpha = 0 68 | UIView.animate(withDuration: 0.3, animations: { 69 | old?.view.alpha = 0 70 | new.view.alpha = 1 71 | }, completion: { _ in 72 | old?.view.removeFromSuperview() 73 | old?.removeFromParentViewController() 74 | new.didMove(toParentViewController: self.container) 75 | completionHandler() 76 | }) 77 | } 78 | } 79 | -------------------------------------------------------------------------------- /Example/CorduxPrototype/AppDelegate.swift: -------------------------------------------------------------------------------- 1 | // 2 | // AppDelegate.swift 3 | // CorduxPrototype 4 | // 5 | // Created by Ian Terrell on 7/21/16. 6 | // Copyright © 2016 WillowTree. All rights reserved. 7 | // 8 | 9 | import UIKit 10 | import Cordux 11 | 12 | @UIApplicationMain 13 | class AppDelegate: UIResponder, UIApplicationDelegate { 14 | 15 | var window: UIWindow? 16 | var coordinator: AppCoordinator! 17 | 18 | func application(_ application: UIApplication, didFinishLaunchingWithOptions launchOptions: [UIApplicationLaunchOptionsKey : Any]? = nil) -> Bool { 19 | guard let mainController = window?.rootViewController as? MainViewController else { 20 | fatalError() 21 | } 22 | 23 | UIViewController.swizzleLifecycleDelegatingViewControllerMethods() 24 | 25 | var state = AppState() 26 | state.route = AppCoordinator.RouteSegment.auth.route() 27 | let store = Store(initialState: state, reducer: AppReducer(), middlewares: [ActionLogger()]) 28 | 29 | coordinator = AppCoordinator(store: store, container: mainController) 30 | coordinator.start(route: state.route) 31 | 32 | return true 33 | } 34 | 35 | func applicationWillResignActive(_ application: UIApplication) { 36 | // Sent when the application is about to move from active to inactive state. This can occur for certain types of temporary interruptions (such as an incoming phone call or SMS message) or when the user quits the application and it begins the transition to the background state. 37 | // Use this method to pause ongoing tasks, disable timers, and throttle down OpenGL ES frame rates. Games should use this method to pause the game. 38 | } 39 | 40 | func applicationDidEnterBackground(_ application: UIApplication) { 41 | // Use this method to release shared resources, save user data, invalidate timers, and store enough application state information to restore your application to its current state in case it is terminated later. 42 | // If your application supports background execution, this method is called instead of applicationWillTerminate: when the user quits. 43 | } 44 | 45 | func applicationWillEnterForeground(_ application: UIApplication) { 46 | // Called as part of the transition from the background to the inactive state; here you can undo many of the changes made on entering the background. 47 | } 48 | 49 | func applicationDidBecomeActive(_ application: UIApplication) { 50 | // Restart any tasks that were paused (or not yet started) while the application was inactive. If the application was previously in the background, optionally refresh the user interface. 51 | } 52 | 53 | func applicationWillTerminate(_ application: UIApplication) { 54 | // Called when the application is about to terminate. Save data if appropriate. See also applicationDidEnterBackground:. 55 | } 56 | 57 | 58 | } 59 | 60 | -------------------------------------------------------------------------------- /Example/CorduxPrototype/AppState.swift: -------------------------------------------------------------------------------- 1 | // 2 | // AppState.swift 3 | // CorduxPrototype 4 | // 5 | // Created by Ian Terrell on 7/21/16. 6 | // Copyright © 2016 WillowTree. All rights reserved. 7 | // 8 | 9 | import Foundation 10 | import Cordux 11 | 12 | typealias Store = Cordux.Store 13 | 14 | struct AppState: StateType { 15 | var route: Route = [] 16 | var name: String = "Hello" 17 | var authenticationState: AuthenticationState = .unauthenticated 18 | } 19 | 20 | enum AuthenticationState { 21 | case unauthenticated 22 | case authenticated 23 | } 24 | 25 | enum AuthenticationAction: Action { 26 | case signIn 27 | case signOut 28 | } 29 | 30 | enum ModalAction: Action { 31 | case present 32 | case dismiss 33 | } 34 | 35 | struct Noop: Action {} 36 | 37 | final class AppReducer: Reducer { 38 | func handleAction(_ action: Action, state: AppState) -> AppState { 39 | var state = state 40 | state = reduceAuthentication(action, state: state) 41 | state = reduceModal(action, state: state) 42 | return state 43 | } 44 | 45 | func reduceAuthentication(_ action: Action, state: AppState) -> AppState { 46 | guard let action = action as? AuthenticationAction else { 47 | return state 48 | } 49 | 50 | var state = state 51 | 52 | switch action { 53 | case .signIn: 54 | state.route = ["catalog"] 55 | state.authenticationState = .authenticated 56 | case .signOut: 57 | state.route = ["auth"] 58 | state.authenticationState = .unauthenticated 59 | } 60 | 61 | return state 62 | } 63 | 64 | func reduceModal(_ action: Action, state: AppState) -> AppState { 65 | guard let action = action as? ModalAction else { 66 | return state 67 | } 68 | 69 | var state = state 70 | 71 | switch action { 72 | case .present: 73 | state.route = state.route.reduce(.push("modal")) 74 | case .dismiss: 75 | state.route = state.route.reduce(.pop("modal")) 76 | } 77 | 78 | return state 79 | } 80 | } 81 | 82 | final class ActionLogger: Middleware { 83 | func before(action: Action, state: AppState) { 84 | print("Before: \(action): \(state)") 85 | } 86 | func after(action: Action, state: AppState) { 87 | print("After: \(action): \(state)") 88 | } 89 | } 90 | -------------------------------------------------------------------------------- /Example/CorduxPrototype/Assets.xcassets/AppIcon.appiconset/Contents.json: -------------------------------------------------------------------------------- 1 | { 2 | "images" : [ 3 | { 4 | "idiom" : "iphone", 5 | "size" : "29x29", 6 | "scale" : "2x" 7 | }, 8 | { 9 | "idiom" : "iphone", 10 | "size" : "29x29", 11 | "scale" : "3x" 12 | }, 13 | { 14 | "idiom" : "iphone", 15 | "size" : "40x40", 16 | "scale" : "2x" 17 | }, 18 | { 19 | "idiom" : "iphone", 20 | "size" : "40x40", 21 | "scale" : "3x" 22 | }, 23 | { 24 | "idiom" : "iphone", 25 | "size" : "60x60", 26 | "scale" : "2x" 27 | }, 28 | { 29 | "idiom" : "iphone", 30 | "size" : "60x60", 31 | "scale" : "3x" 32 | }, 33 | { 34 | "idiom" : "ipad", 35 | "size" : "29x29", 36 | "scale" : "1x" 37 | }, 38 | { 39 | "idiom" : "ipad", 40 | "size" : "29x29", 41 | "scale" : "2x" 42 | }, 43 | { 44 | "idiom" : "ipad", 45 | "size" : "40x40", 46 | "scale" : "1x" 47 | }, 48 | { 49 | "idiom" : "ipad", 50 | "size" : "40x40", 51 | "scale" : "2x" 52 | }, 53 | { 54 | "idiom" : "ipad", 55 | "size" : "76x76", 56 | "scale" : "1x" 57 | }, 58 | { 59 | "idiom" : "ipad", 60 | "size" : "76x76", 61 | "scale" : "2x" 62 | } 63 | ], 64 | "info" : { 65 | "version" : 1, 66 | "author" : "xcode" 67 | } 68 | } -------------------------------------------------------------------------------- /Example/CorduxPrototype/Assets.xcassets/first.imageset/Contents.json: -------------------------------------------------------------------------------- 1 | { 2 | "images" : [ 3 | { 4 | "idiom" : "universal", 5 | "filename" : "first.pdf" 6 | } 7 | ], 8 | "info" : { 9 | "version" : 1, 10 | "author" : "xcode" 11 | } 12 | } -------------------------------------------------------------------------------- /Example/CorduxPrototype/Assets.xcassets/first.imageset/first.pdf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/willowtreeapps/cordux/7b9ac675e008f0e4a6e7b404dba5aebe363e0c48/Example/CorduxPrototype/Assets.xcassets/first.imageset/first.pdf -------------------------------------------------------------------------------- /Example/CorduxPrototype/Assets.xcassets/second.imageset/Contents.json: -------------------------------------------------------------------------------- 1 | { 2 | "images" : [ 3 | { 4 | "idiom" : "universal", 5 | "filename" : "second.pdf" 6 | } 7 | ], 8 | "info" : { 9 | "version" : 1, 10 | "author" : "xcode" 11 | } 12 | } -------------------------------------------------------------------------------- /Example/CorduxPrototype/Assets.xcassets/second.imageset/second.pdf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/willowtreeapps/cordux/7b9ac675e008f0e4a6e7b404dba5aebe363e0c48/Example/CorduxPrototype/Assets.xcassets/second.imageset/second.pdf -------------------------------------------------------------------------------- /Example/CorduxPrototype/Authentication.storyboard: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 28 | 35 | 41 | 42 | 43 | 44 | 45 | 46 | 47 | 48 | 49 | 50 | 51 | 52 | 53 | 54 | 55 | 56 | 57 | 58 | 59 | 60 | 61 | 62 | 63 | 64 | 65 | 66 | 67 | 68 | 69 | 70 | 71 | 72 | 79 | 80 | 81 | 82 | 83 | 84 | 85 | 86 | 87 | 88 | 89 | 90 | 91 | 92 | 93 | 94 | -------------------------------------------------------------------------------- /Example/CorduxPrototype/AuthenticationCoordinator.swift: -------------------------------------------------------------------------------- 1 | // 2 | // AuthenticationCoordinator.swift 3 | // CorduxPrototype 4 | // 5 | // Created by Ian Terrell on 7/21/16. 6 | // Copyright © 2016 WillowTree. All rights reserved. 7 | // 8 | 9 | import UIKit 10 | import Cordux 11 | 12 | final class AuthenticationCoordinator: NavigationControllerCoordinator { 13 | enum RouteSegment: String, RouteConvertible { 14 | case signIn 15 | case fp 16 | } 17 | 18 | var store: Store 19 | 20 | let storyboard = UIStoryboard(name: "Authentication", bundle: nil) 21 | let navigationController: UINavigationController 22 | 23 | let signInViewController: SignInViewController 24 | 25 | init(store: Store) { 26 | self.store = store 27 | 28 | signInViewController = storyboard.instantiateInitialViewController() as! SignInViewController 29 | navigationController = UINavigationController(rootViewController: signInViewController) 30 | } 31 | 32 | func start(route: Route?) { 33 | signInViewController.inject(handler: self) 34 | signInViewController.corduxContext = Context(routeSegment: RouteSegment.signIn, lifecycleDelegate: self) 35 | 36 | let segments = parse(route: route) 37 | guard !segments.isEmpty else { 38 | store.setRoute(.push(RouteSegment.signIn)) 39 | return 40 | } 41 | 42 | if segments.last == .fp { 43 | navigationController.pushViewController(createForgotPasswordViewController(), animated: false) 44 | } 45 | } 46 | 47 | public func updateRoute(_ route: Route, completionHandler: @escaping () -> Void) { 48 | guard parse(route: route).last == .fp else { 49 | completionHandler() 50 | return 51 | } 52 | 53 | navigationController.pushViewController(createForgotPasswordViewController(), animated: true, completion: completionHandler) 54 | } 55 | func updateRoute(_ route: Route) { 56 | if parse(route: route).last == .fp { 57 | navigationController.pushViewController(createForgotPasswordViewController(), animated: true) 58 | } 59 | } 60 | 61 | func parse(route: Route?) -> [RouteSegment] { 62 | return route?.flatMap({ RouteSegment.init(rawValue: $0) }) ?? [] 63 | } 64 | 65 | func createForgotPasswordViewController() -> ForgotPasswordViewController { 66 | let forgotPasswordViewController = storyboard.instantiateViewController(withIdentifier: "ForgotPassword") as! ForgotPasswordViewController 67 | forgotPasswordViewController.inject(self) 68 | forgotPasswordViewController.corduxContext = Context(routeSegment: RouteSegment.fp, lifecycleDelegate: self) 69 | return forgotPasswordViewController 70 | } 71 | } 72 | 73 | extension AuthenticationCoordinator: ViewControllerLifecycleDelegate { 74 | @objc func viewDidLoad(viewController: UIViewController) { 75 | if viewController === signInViewController { 76 | store.subscribe(signInViewController, SignInViewModel.init) 77 | } 78 | } 79 | 80 | @objc func didMove(toParentViewController parentViewController: UIViewController?, viewController: UIViewController) { 81 | guard parentViewController == nil else { 82 | return 83 | } 84 | 85 | popRoute(viewController) 86 | } 87 | } 88 | 89 | extension AuthenticationCoordinator: SignInHandler { 90 | func signIn() { 91 | store.dispatch(AuthenticationAction.signIn) 92 | } 93 | 94 | func forgotPassword() { 95 | store.route(.push(RouteSegment.fp)) 96 | } 97 | } 98 | 99 | extension SignInViewController: Renderer {} 100 | 101 | extension SignInViewModel { 102 | init?(_ state: AppState) { 103 | return nil 104 | //name = state.name 105 | } 106 | } 107 | 108 | extension AuthenticationCoordinator: ForgotPasswordHandler { 109 | func emailPassword() { 110 | 111 | } 112 | } 113 | 114 | -------------------------------------------------------------------------------- /Example/CorduxPrototype/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 | -------------------------------------------------------------------------------- /Example/CorduxPrototype/Catalog.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 | 45 | 46 | 47 | 48 | 55 | 56 | 57 | 58 | 59 | 60 | 61 | 62 | 63 | 64 | 65 | 66 | 67 | 68 | 69 | 70 | 71 | 72 | 73 | 74 | 75 | 76 | 77 | 78 | 79 | 80 | 81 | 82 | 83 | 84 | 85 | 91 | 92 | 93 | 94 | 101 | 108 | 109 | 110 | 111 | 112 | 113 | 114 | 115 | 116 | 117 | 118 | 119 | 120 | 121 | 122 | 123 | 124 | 125 | 126 | 127 | 128 | -------------------------------------------------------------------------------- /Example/CorduxPrototype/CatalogCoordinator.swift: -------------------------------------------------------------------------------- 1 | // 2 | // CatalogCoordinator.swift 3 | // CorduxPrototype 4 | // 5 | // Created by Ian Terrell on 7/21/16. 6 | // Copyright © 2016 WillowTree. All rights reserved. 7 | // 8 | 9 | import UIKit 10 | import Cordux 11 | 12 | final class CatalogContainerCoordinator: PresentingCoordinator { 13 | var presented: Scene? 14 | 15 | lazy var presentables: [GeneratingScene] = { 16 | return [GeneratingScene(tag: "modal") { return ModalCoordinator(store: self.store) }] 17 | }() 18 | 19 | var store: Store 20 | var rootCoordinator: AnyCoordinator 21 | 22 | init(store: Store, rootCoordinator: AnyCoordinator) { 23 | self.store = store 24 | self.rootCoordinator = rootCoordinator 25 | } 26 | } 27 | 28 | final class CatalogCoordinator: NSObject, TabBarControllerCoordinator { 29 | var store: Store 30 | 31 | let scenes: [Scene] 32 | let tabBarController: UITabBarController 33 | 34 | init(store: Store) { 35 | self.store = store 36 | scenes = [ 37 | Scene(tag: "first", coordinator: FirstCoordinator(store: store)), 38 | Scene(tag: "second", coordinator: SecondCoordinator(store: store)), 39 | ] 40 | 41 | tabBarController = UIStoryboard(name: "Catalog", bundle: nil) 42 | .instantiateInitialViewController() as! UITabBarController 43 | 44 | tabBarController.viewControllers = scenes.map { $0.coordinator.rootViewController } 45 | } 46 | 47 | func start(route: Route?) { 48 | tabBarController.delegate = self 49 | scenes.forEach { $0.coordinator.start(route: []) } 50 | store.setRoute(.push(scenes[tabBarController.selectedIndex])) 51 | } 52 | } 53 | 54 | extension CatalogCoordinator: UITabBarControllerDelegate { 55 | func tabBarController(_ tabBarController: UITabBarController, shouldSelect viewController: UIViewController) -> Bool { 56 | return setRouteForViewController(viewController) 57 | } 58 | } 59 | 60 | final class FirstCoordinator: NavigationControllerCoordinator { 61 | var store: Store 62 | 63 | let navigationController: UINavigationController 64 | var rootViewController: UIViewController { return navigationController } 65 | let first: FirstViewController 66 | 67 | init(store: Store) { 68 | self.store = store 69 | 70 | first = FirstViewController.make() 71 | navigationController = UINavigationController(rootViewController: first) 72 | } 73 | 74 | func start(route: Route?) { 75 | first.inject(handler: self) 76 | } 77 | 78 | func updateRoute(_ route: Route, completionHandler: @escaping () -> Void) { 79 | completionHandler() 80 | } 81 | } 82 | 83 | extension FirstCoordinator: FirstHandler { 84 | func performAction() { 85 | store.dispatch(ModalAction.present) 86 | } 87 | } 88 | 89 | final class SecondCoordinator: NavigationControllerCoordinator { 90 | var store: Store 91 | 92 | let navigationController: UINavigationController 93 | var rootViewController: UIViewController { return navigationController } 94 | let second: SecondViewController 95 | 96 | init(store: Store) { 97 | self.store = store 98 | 99 | second = SecondViewController.make() 100 | navigationController = UINavigationController(rootViewController: second) 101 | } 102 | 103 | func start(route: Route?) { 104 | second.inject(handler: self) 105 | } 106 | 107 | func updateRoute(_ route: Route, completionHandler: @escaping () -> Void) { 108 | completionHandler() 109 | } 110 | } 111 | 112 | extension SecondCoordinator: SecondHandler { 113 | func performAction() { 114 | store.dispatch(Noop()) 115 | } 116 | 117 | func signOut() { 118 | store.dispatch(AuthenticationAction.signOut) 119 | } 120 | } 121 | 122 | final class ModalCoordinator: Coordinator { 123 | var store: Store 124 | 125 | var route: Route = [] 126 | 127 | var first: FirstViewController 128 | var rootViewController: UIViewController { return first } 129 | 130 | init(store: Store) { 131 | self.store = store 132 | 133 | first = FirstViewController.make() 134 | } 135 | 136 | func start(route: Route?) { 137 | first.inject(handler: self) 138 | } 139 | 140 | func prepareForRoute(_: Route?, completionHandler: @escaping () -> Void) { 141 | completionHandler() 142 | } 143 | 144 | func setRoute(_: Route?, completionHandler: @escaping () -> Void) { 145 | completionHandler() 146 | } 147 | } 148 | 149 | extension ModalCoordinator: FirstHandler { 150 | func performAction() { 151 | store.dispatch(ModalAction.dismiss) 152 | } 153 | } 154 | -------------------------------------------------------------------------------- /Example/CorduxPrototype/FirstViewController.swift: -------------------------------------------------------------------------------- 1 | // 2 | // FirstViewController.swift 3 | // CorduxPrototype 4 | // 5 | // Created by Ian Terrell on 7/21/16. 6 | // Copyright © 2016 WillowTree. All rights reserved. 7 | // 8 | 9 | import UIKit 10 | 11 | protocol FirstHandler: class { 12 | func performAction() 13 | } 14 | 15 | class FirstViewController: UIViewController { 16 | weak var handler: FirstHandler? 17 | 18 | static func make() -> FirstViewController { 19 | let storyboard = UIStoryboard(name: "Catalog", bundle: nil) 20 | return storyboard.instantiateViewController(withIdentifier: "First") as! FirstViewController 21 | } 22 | 23 | func inject(handler: FirstHandler) { 24 | self.handler = handler 25 | } 26 | 27 | @IBAction func performAction(_ sender: AnyObject) { 28 | handler?.performAction() 29 | } 30 | } 31 | 32 | -------------------------------------------------------------------------------- /Example/CorduxPrototype/ForgotPasswordViewController.swift: -------------------------------------------------------------------------------- 1 | // 2 | // ForgotPasswordViewController.swift 3 | // CorduxPrototype 4 | // 5 | // Created by Ian Terrell on 7/21/16. 6 | // Copyright © 2016 WillowTree. All rights reserved. 7 | // 8 | 9 | import UIKit 10 | 11 | protocol ForgotPasswordHandler { 12 | func emailPassword() 13 | } 14 | 15 | class ForgotPasswordViewController: UIViewController { 16 | 17 | var handler: ForgotPasswordHandler! 18 | 19 | func inject(_ handler: ForgotPasswordHandler) { 20 | self.handler = handler 21 | } 22 | 23 | @IBAction func emailPassword(_ sender: AnyObject) { 24 | handler.emailPassword() 25 | } 26 | } 27 | -------------------------------------------------------------------------------- /Example/CorduxPrototype/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 | UILaunchStoryboardName 26 | LaunchScreen 27 | UIMainStoryboardFile 28 | Main 29 | UIRequiredDeviceCapabilities 30 | 31 | armv7 32 | 33 | UIStatusBarTintParameters 34 | 35 | UINavigationBar 36 | 37 | Style 38 | UIBarStyleDefault 39 | Translucent 40 | 41 | 42 | 43 | UISupportedInterfaceOrientations 44 | 45 | UIInterfaceOrientationPortrait 46 | UIInterfaceOrientationLandscapeLeft 47 | UIInterfaceOrientationLandscapeRight 48 | 49 | UISupportedInterfaceOrientations~ipad 50 | 51 | UIInterfaceOrientationPortrait 52 | UIInterfaceOrientationPortraitUpsideDown 53 | UIInterfaceOrientationLandscapeLeft 54 | UIInterfaceOrientationLandscapeRight 55 | 56 | 57 | 58 | -------------------------------------------------------------------------------- /Example/CorduxPrototype/Main.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/CorduxPrototype/MainViewController.swift: -------------------------------------------------------------------------------- 1 | // 2 | // MainViewController.swift 3 | // CorduxPrototype 4 | // 5 | // Created by Ian Terrell on 7/25/16. 6 | // Copyright © 2016 WillowTree. All rights reserved. 7 | // 8 | 9 | import UIKit 10 | 11 | class MainViewController: UIViewController { 12 | 13 | override func viewDidLoad() { 14 | super.viewDidLoad() 15 | 16 | // Do any additional setup after loading the view. 17 | } 18 | 19 | override func didReceiveMemoryWarning() { 20 | super.didReceiveMemoryWarning() 21 | // Dispose of any resources that can be recreated. 22 | } 23 | 24 | 25 | /* 26 | // MARK: - Navigation 27 | 28 | // In a storyboard-based application, you will often want to do a little preparation before navigation 29 | override func prepareForSegue(segue: UIStoryboardSegue, sender: AnyObject?) { 30 | // Get the new view controller using segue.destinationViewController. 31 | // Pass the selected object to the new view controller. 32 | } 33 | */ 34 | 35 | } 36 | -------------------------------------------------------------------------------- /Example/CorduxPrototype/SecondViewController.swift: -------------------------------------------------------------------------------- 1 | // 2 | // SecondViewController.swift 3 | // CorduxPrototype 4 | // 5 | // Created by Ian Terrell on 7/21/16. 6 | // Copyright © 2016 WillowTree. All rights reserved. 7 | // 8 | 9 | import UIKit 10 | 11 | protocol SecondHandler: class { 12 | func performAction() 13 | func signOut() 14 | } 15 | 16 | class SecondViewController: UIViewController { 17 | weak var handler: SecondHandler? 18 | 19 | static func make() -> SecondViewController { 20 | let storyboard = UIStoryboard(name: "Catalog", bundle: nil) 21 | return storyboard.instantiateViewController(withIdentifier: "Second") as! SecondViewController 22 | } 23 | 24 | 25 | func inject(handler: SecondHandler) { 26 | self.handler = handler 27 | } 28 | 29 | @IBAction func performAction(_ sender: AnyObject) { 30 | handler?.performAction() 31 | } 32 | 33 | @IBAction func signOut(_ sender: AnyObject) { 34 | handler?.signOut() 35 | } 36 | } 37 | 38 | -------------------------------------------------------------------------------- /Example/CorduxPrototype/SignInViewController.swift: -------------------------------------------------------------------------------- 1 | // 2 | // SignInViewController.swift 3 | // CorduxPrototype 4 | // 5 | // Created by Ian Terrell on 7/21/16. 6 | // Copyright © 2016 WillowTree. All rights reserved. 7 | // 8 | 9 | import UIKit 10 | 11 | struct SignInViewModel { 12 | let name: String 13 | } 14 | 15 | protocol SignInHandler { 16 | func signIn() 17 | func forgotPassword() 18 | } 19 | 20 | final class SignInViewController: UIViewController { 21 | 22 | @IBOutlet var nameLabel: UILabel! 23 | 24 | var handler: SignInHandler! 25 | 26 | func inject(handler: SignInHandler) { 27 | self.handler = handler 28 | } 29 | 30 | func render(_ viewModel: SignInViewModel?) { 31 | nameLabel?.text = viewModel?.name 32 | } 33 | 34 | @IBAction func signIn(_ sender: AnyObject) { 35 | handler.signIn() 36 | } 37 | 38 | @IBAction func forgotPassword(_ sender: AnyObject) { 39 | handler.forgotPassword() 40 | } 41 | } 42 | -------------------------------------------------------------------------------- /Example/CorduxPrototypeTests/CorduxPrototypeTests.swift: -------------------------------------------------------------------------------- 1 | // 2 | // CorduxPrototypeTests.swift 3 | // CorduxPrototypeTests 4 | // 5 | // Created by Ian Terrell on 7/21/16. 6 | // Copyright © 2016 WillowTree. All rights reserved. 7 | // 8 | 9 | import XCTest 10 | @testable import CorduxPrototype 11 | 12 | class CorduxPrototypeTests: XCTestCase { 13 | 14 | override func setUp() { 15 | super.setUp() 16 | // Put setup code here. This method is called before the invocation of each test method in the class. 17 | } 18 | 19 | override func tearDown() { 20 | // Put teardown code here. This method is called after the invocation of each test method in the class. 21 | super.tearDown() 22 | } 23 | 24 | func testExample() { 25 | // This is an example of a functional test case. 26 | // Use XCTAssert and related functions to verify your tests produce the correct results. 27 | } 28 | 29 | func testPerformanceExample() { 30 | // This is an example of a performance test case. 31 | self.measure { 32 | // Put the code you want to measure the time of here. 33 | } 34 | } 35 | 36 | } 37 | -------------------------------------------------------------------------------- /Example/CorduxPrototypeTests/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 | -------------------------------------------------------------------------------- /Example/CorduxPrototypeUITests/CorduxPrototypeUITests.swift: -------------------------------------------------------------------------------- 1 | // 2 | // CorduxPrototypeUITests.swift 3 | // CorduxPrototypeUITests 4 | // 5 | // Created by Ian Terrell on 7/21/16. 6 | // Copyright © 2016 WillowTree. All rights reserved. 7 | // 8 | 9 | import XCTest 10 | 11 | class CorduxPrototypeUITests: XCTestCase { 12 | 13 | override func setUp() { 14 | super.setUp() 15 | 16 | // Put setup code here. This method is called before the invocation of each test method in the class. 17 | 18 | // In UI tests it is usually best to stop immediately when a failure occurs. 19 | continueAfterFailure = false 20 | // UI tests must launch the application that they test. Doing this in setup will make sure it happens for each test method. 21 | XCUIApplication().launch() 22 | 23 | // In UI tests it’s important to set the initial state - such as interface orientation - required for your tests before they run. The setUp method is a good place to do this. 24 | } 25 | 26 | override func tearDown() { 27 | // Put teardown code here. This method is called after the invocation of each test method in the class. 28 | super.tearDown() 29 | } 30 | 31 | func testExample() { 32 | // Use recording to get started writing UI tests. 33 | // Use XCTAssert and related functions to verify your tests produce the correct results. 34 | } 35 | 36 | } 37 | -------------------------------------------------------------------------------- /Example/CorduxPrototypeUITests/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 | -------------------------------------------------------------------------------- /Example/Podfile: -------------------------------------------------------------------------------- 1 | use_frameworks! 2 | platform :ios, '9.0' 3 | inhibit_all_warnings! 4 | 5 | target 'CorduxPrototype' do 6 | pod 'Cordux', path: '..' 7 | end 8 | 9 | post_install do |installer| 10 | installer.pods_project.targets.each do |target| 11 | target.build_configurations.each do |config| 12 | config.build_settings['SWIFT_VERSION'] = '3.0' 13 | end 14 | end 15 | end 16 | -------------------------------------------------------------------------------- /Example/Podfile.lock: -------------------------------------------------------------------------------- 1 | PODS: 2 | - Cordux (0.1.7) 3 | 4 | DEPENDENCIES: 5 | - Cordux (from `..`) 6 | 7 | EXTERNAL SOURCES: 8 | Cordux: 9 | :path: ".." 10 | 11 | SPEC CHECKSUMS: 12 | Cordux: 9c9c485f0aa845c8c54679f31ccaa3c58a9fb56e 13 | 14 | PODFILE CHECKSUM: 284a10d3aecab71ef0d599dc2c45c053852d04a7 15 | 16 | COCOAPODS: 1.0.1 17 | -------------------------------------------------------------------------------- /Example/Pods/Local Podspecs/Cordux.podspec.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "Cordux", 3 | "version": "0.1.7", 4 | "summary": "App Coordinators & Redux. A framework for UIKit development.", 5 | "description": "Cordux combines app coordiantors with a redux-like architecture.\n\nThis allows rapid and powerful UI development with full support\nfor routing and deep linking.", 6 | "homepage": "http://github.com/willowtreeapps/cordux", 7 | "license": "MIT", 8 | "authors": { 9 | "Ian Terrell": "ian.terrell@gmail.com" 10 | }, 11 | "source": { 12 | "git": "https://github.com/willowtreeapps/cordux.git", 13 | "tag": "v0.1.7" 14 | }, 15 | "platforms": { 16 | "ios": "9.0", 17 | "tvos": "9.1" 18 | }, 19 | "source_files": "Sources/*.swift" 20 | } 21 | -------------------------------------------------------------------------------- /Example/Pods/Manifest.lock: -------------------------------------------------------------------------------- 1 | PODS: 2 | - Cordux (0.1.7) 3 | 4 | DEPENDENCIES: 5 | - Cordux (from `..`) 6 | 7 | EXTERNAL SOURCES: 8 | Cordux: 9 | :path: ".." 10 | 11 | SPEC CHECKSUMS: 12 | Cordux: 9c9c485f0aa845c8c54679f31ccaa3c58a9fb56e 13 | 14 | PODFILE CHECKSUM: 284a10d3aecab71ef0d599dc2c45c053852d04a7 15 | 16 | COCOAPODS: 1.0.1 17 | -------------------------------------------------------------------------------- /Example/Pods/Target Support Files/Cordux/Cordux-dummy.m: -------------------------------------------------------------------------------- 1 | #import 2 | @interface PodsDummy_Cordux : NSObject 3 | @end 4 | @implementation PodsDummy_Cordux 5 | @end 6 | -------------------------------------------------------------------------------- /Example/Pods/Target Support Files/Cordux/Cordux-prefix.pch: -------------------------------------------------------------------------------- 1 | #ifdef __OBJC__ 2 | #import 3 | #endif 4 | 5 | -------------------------------------------------------------------------------- /Example/Pods/Target Support Files/Cordux/Cordux-umbrella.h: -------------------------------------------------------------------------------- 1 | #import 2 | 3 | 4 | FOUNDATION_EXPORT double CorduxVersionNumber; 5 | FOUNDATION_EXPORT const unsigned char CorduxVersionString[]; 6 | 7 | -------------------------------------------------------------------------------- /Example/Pods/Target Support Files/Cordux/Cordux.modulemap: -------------------------------------------------------------------------------- 1 | framework module Cordux { 2 | umbrella header "Cordux-umbrella.h" 3 | 4 | export * 5 | module * { export * } 6 | } 7 | -------------------------------------------------------------------------------- /Example/Pods/Target Support Files/Cordux/Cordux.xcconfig: -------------------------------------------------------------------------------- 1 | CONFIGURATION_BUILD_DIR = $PODS_CONFIGURATION_BUILD_DIR/Cordux 2 | GCC_PREPROCESSOR_DEFINITIONS = $(inherited) COCOAPODS=1 3 | HEADER_SEARCH_PATHS = "${PODS_ROOT}/Headers/Private" "${PODS_ROOT}/Headers/Public" 4 | OTHER_SWIFT_FLAGS = $(inherited) "-D" "COCOAPODS" 5 | PODS_BUILD_DIR = $BUILD_DIR 6 | PODS_CONFIGURATION_BUILD_DIR = $PODS_BUILD_DIR/$(CONFIGURATION)$(EFFECTIVE_PLATFORM_NAME) 7 | PODS_ROOT = ${SRCROOT} 8 | PRODUCT_BUNDLE_IDENTIFIER = org.cocoapods.${PRODUCT_NAME:rfc1034identifier} 9 | SKIP_INSTALL = YES 10 | -------------------------------------------------------------------------------- /Example/Pods/Target Support Files/Cordux/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 | 0.1.7 19 | CFBundleSignature 20 | ???? 21 | CFBundleVersion 22 | ${CURRENT_PROJECT_VERSION} 23 | NSPrincipalClass 24 | 25 | 26 | 27 | -------------------------------------------------------------------------------- /Example/Pods/Target Support Files/Pods-CorduxPrototype/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 | -------------------------------------------------------------------------------- /Example/Pods/Target Support Files/Pods-CorduxPrototype/Pods-CorduxPrototype-acknowledgements.markdown: -------------------------------------------------------------------------------- 1 | # Acknowledgements 2 | This application makes use of the following third party libraries: 3 | 4 | ## Cordux 5 | 6 | MIT License 7 | 8 | Copyright (c) 2016 WillowTree, Inc. 9 | 10 | Permission is hereby granted, free of charge, to any person obtaining a copy 11 | of this software and associated documentation files (the "Software"), to deal 12 | in the Software without restriction, including without limitation the rights 13 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 14 | copies of the Software, and to permit persons to whom the Software is 15 | furnished to do so, subject to the following conditions: 16 | 17 | The above copyright notice and this permission notice shall be included in all 18 | copies or substantial portions of the Software. 19 | 20 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 21 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 22 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 23 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 24 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 25 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 26 | SOFTWARE. 27 | 28 | Generated by CocoaPods - https://cocoapods.org 29 | -------------------------------------------------------------------------------- /Example/Pods/Target Support Files/Pods-CorduxPrototype/Pods-CorduxPrototype-acknowledgements.plist: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | PreferenceSpecifiers 6 | 7 | 8 | FooterText 9 | This application makes use of the following third party libraries: 10 | Title 11 | Acknowledgements 12 | Type 13 | PSGroupSpecifier 14 | 15 | 16 | FooterText 17 | MIT License 18 | 19 | Copyright (c) 2016 WillowTree, Inc. 20 | 21 | Permission is hereby granted, free of charge, to any person obtaining a copy 22 | of this software and associated documentation files (the "Software"), to deal 23 | in the Software without restriction, including without limitation the rights 24 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 25 | copies of the Software, and to permit persons to whom the Software is 26 | furnished to do so, subject to the following conditions: 27 | 28 | The above copyright notice and this permission notice shall be included in all 29 | copies or substantial portions of the Software. 30 | 31 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 32 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 33 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 34 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 35 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 36 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 37 | SOFTWARE. 38 | 39 | Title 40 | Cordux 41 | Type 42 | PSGroupSpecifier 43 | 44 | 45 | FooterText 46 | Generated by CocoaPods - https://cocoapods.org 47 | Title 48 | 49 | Type 50 | PSGroupSpecifier 51 | 52 | 53 | StringsTable 54 | Acknowledgements 55 | Title 56 | Acknowledgements 57 | 58 | 59 | -------------------------------------------------------------------------------- /Example/Pods/Target Support Files/Pods-CorduxPrototype/Pods-CorduxPrototype-dummy.m: -------------------------------------------------------------------------------- 1 | #import 2 | @interface PodsDummy_Pods_CorduxPrototype : NSObject 3 | @end 4 | @implementation PodsDummy_Pods_CorduxPrototype 5 | @end 6 | -------------------------------------------------------------------------------- /Example/Pods/Target Support Files/Pods-CorduxPrototype/Pods-CorduxPrototype-frameworks.sh: -------------------------------------------------------------------------------- 1 | #!/bin/sh 2 | set -e 3 | 4 | echo "mkdir -p ${CONFIGURATION_BUILD_DIR}/${FRAMEWORKS_FOLDER_PATH}" 5 | mkdir -p "${CONFIGURATION_BUILD_DIR}/${FRAMEWORKS_FOLDER_PATH}" 6 | 7 | SWIFT_STDLIB_PATH="${DT_TOOLCHAIN_DIR}/usr/lib/swift/${PLATFORM_NAME}" 8 | 9 | install_framework() 10 | { 11 | if [ -r "${BUILT_PRODUCTS_DIR}/$1" ]; then 12 | local source="${BUILT_PRODUCTS_DIR}/$1" 13 | elif [ -r "${BUILT_PRODUCTS_DIR}/$(basename "$1")" ]; then 14 | local source="${BUILT_PRODUCTS_DIR}/$(basename "$1")" 15 | elif [ -r "$1" ]; then 16 | local source="$1" 17 | fi 18 | 19 | local destination="${TARGET_BUILD_DIR}/${FRAMEWORKS_FOLDER_PATH}" 20 | 21 | if [ -L "${source}" ]; then 22 | echo "Symlinked..." 23 | source="$(readlink "${source}")" 24 | fi 25 | 26 | # use filter instead of exclude so missing patterns dont' throw errors 27 | echo "rsync -av --filter \"- CVS/\" --filter \"- .svn/\" --filter \"- .git/\" --filter \"- .hg/\" --filter \"- Headers\" --filter \"- PrivateHeaders\" --filter \"- Modules\" \"${source}\" \"${destination}\"" 28 | rsync -av --filter "- CVS/" --filter "- .svn/" --filter "- .git/" --filter "- .hg/" --filter "- Headers" --filter "- PrivateHeaders" --filter "- Modules" "${source}" "${destination}" 29 | 30 | local basename 31 | basename="$(basename -s .framework "$1")" 32 | binary="${destination}/${basename}.framework/${basename}" 33 | if ! [ -r "$binary" ]; then 34 | binary="${destination}/${basename}" 35 | fi 36 | 37 | # Strip invalid architectures so "fat" simulator / device frameworks work on device 38 | if [[ "$(file "$binary")" == *"dynamically linked shared library"* ]]; then 39 | strip_invalid_archs "$binary" 40 | fi 41 | 42 | # Resign the code if required by the build settings to avoid unstable apps 43 | code_sign_if_enabled "${destination}/$(basename "$1")" 44 | 45 | # Embed linked Swift runtime libraries. No longer necessary as of Xcode 7. 46 | if [ "${XCODE_VERSION_MAJOR}" -lt 7 ]; then 47 | local swift_runtime_libs 48 | swift_runtime_libs=$(xcrun otool -LX "$binary" | grep --color=never @rpath/libswift | sed -E s/@rpath\\/\(.+dylib\).*/\\1/g | uniq -u && exit ${PIPESTATUS[0]}) 49 | for lib in $swift_runtime_libs; do 50 | echo "rsync -auv \"${SWIFT_STDLIB_PATH}/${lib}\" \"${destination}\"" 51 | rsync -auv "${SWIFT_STDLIB_PATH}/${lib}" "${destination}" 52 | code_sign_if_enabled "${destination}/${lib}" 53 | done 54 | fi 55 | } 56 | 57 | # Signs a framework with the provided identity 58 | code_sign_if_enabled() { 59 | if [ -n "${EXPANDED_CODE_SIGN_IDENTITY}" -a "${CODE_SIGNING_REQUIRED}" != "NO" -a "${CODE_SIGNING_ALLOWED}" != "NO" ]; then 60 | # Use the current code_sign_identitiy 61 | echo "Code Signing $1 with Identity ${EXPANDED_CODE_SIGN_IDENTITY_NAME}" 62 | echo "/usr/bin/codesign --force --sign ${EXPANDED_CODE_SIGN_IDENTITY} ${OTHER_CODE_SIGN_FLAGS} --preserve-metadata=identifier,entitlements \"$1\"" 63 | /usr/bin/codesign --force --sign ${EXPANDED_CODE_SIGN_IDENTITY} ${OTHER_CODE_SIGN_FLAGS} --preserve-metadata=identifier,entitlements "$1" 64 | fi 65 | } 66 | 67 | # Strip invalid architectures 68 | strip_invalid_archs() { 69 | binary="$1" 70 | # Get architectures for current file 71 | archs="$(lipo -info "$binary" | rev | cut -d ':' -f1 | rev)" 72 | stripped="" 73 | for arch in $archs; do 74 | if ! [[ "${VALID_ARCHS}" == *"$arch"* ]]; then 75 | # Strip non-valid architectures in-place 76 | lipo -remove "$arch" -output "$binary" "$binary" || exit 1 77 | stripped="$stripped $arch" 78 | fi 79 | done 80 | if [[ "$stripped" ]]; then 81 | echo "Stripped $binary of architectures:$stripped" 82 | fi 83 | } 84 | 85 | 86 | if [[ "$CONFIGURATION" == "Debug" ]]; then 87 | install_framework "$BUILT_PRODUCTS_DIR/Cordux/Cordux.framework" 88 | fi 89 | if [[ "$CONFIGURATION" == "Release" ]]; then 90 | install_framework "$BUILT_PRODUCTS_DIR/Cordux/Cordux.framework" 91 | fi 92 | -------------------------------------------------------------------------------- /Example/Pods/Target Support Files/Pods-CorduxPrototype/Pods-CorduxPrototype-resources.sh: -------------------------------------------------------------------------------- 1 | #!/bin/sh 2 | set -e 3 | 4 | mkdir -p "${TARGET_BUILD_DIR}/${UNLOCALIZED_RESOURCES_FOLDER_PATH}" 5 | 6 | RESOURCES_TO_COPY=${PODS_ROOT}/resources-to-copy-${TARGETNAME}.txt 7 | > "$RESOURCES_TO_COPY" 8 | 9 | XCASSET_FILES=() 10 | 11 | case "${TARGETED_DEVICE_FAMILY}" in 12 | 1,2) 13 | TARGET_DEVICE_ARGS="--target-device ipad --target-device iphone" 14 | ;; 15 | 1) 16 | TARGET_DEVICE_ARGS="--target-device iphone" 17 | ;; 18 | 2) 19 | TARGET_DEVICE_ARGS="--target-device ipad" 20 | ;; 21 | *) 22 | TARGET_DEVICE_ARGS="--target-device mac" 23 | ;; 24 | esac 25 | 26 | realpath() { 27 | DIRECTORY="$(cd "${1%/*}" && pwd)" 28 | FILENAME="${1##*/}" 29 | echo "$DIRECTORY/$FILENAME" 30 | } 31 | 32 | install_resource() 33 | { 34 | if [[ "$1" = /* ]] ; then 35 | RESOURCE_PATH="$1" 36 | else 37 | RESOURCE_PATH="${PODS_ROOT}/$1" 38 | fi 39 | if [[ ! -e "$RESOURCE_PATH" ]] ; then 40 | cat << EOM 41 | error: Resource "$RESOURCE_PATH" not found. Run 'pod install' to update the copy resources script. 42 | EOM 43 | exit 1 44 | fi 45 | case $RESOURCE_PATH in 46 | *.storyboard) 47 | echo "ibtool --reference-external-strings-file --errors --warnings --notices --minimum-deployment-target ${!DEPLOYMENT_TARGET_SETTING_NAME} --output-format human-readable-text --compile ${TARGET_BUILD_DIR}/${UNLOCALIZED_RESOURCES_FOLDER_PATH}/`basename \"$RESOURCE_PATH\" .storyboard`.storyboardc $RESOURCE_PATH --sdk ${SDKROOT} ${TARGET_DEVICE_ARGS}" 48 | ibtool --reference-external-strings-file --errors --warnings --notices --minimum-deployment-target ${!DEPLOYMENT_TARGET_SETTING_NAME} --output-format human-readable-text --compile "${TARGET_BUILD_DIR}/${UNLOCALIZED_RESOURCES_FOLDER_PATH}/`basename \"$RESOURCE_PATH\" .storyboard`.storyboardc" "$RESOURCE_PATH" --sdk "${SDKROOT}" ${TARGET_DEVICE_ARGS} 49 | ;; 50 | *.xib) 51 | echo "ibtool --reference-external-strings-file --errors --warnings --notices --minimum-deployment-target ${!DEPLOYMENT_TARGET_SETTING_NAME} --output-format human-readable-text --compile ${TARGET_BUILD_DIR}/${UNLOCALIZED_RESOURCES_FOLDER_PATH}/`basename \"$RESOURCE_PATH\" .xib`.nib $RESOURCE_PATH --sdk ${SDKROOT} ${TARGET_DEVICE_ARGS}" 52 | ibtool --reference-external-strings-file --errors --warnings --notices --minimum-deployment-target ${!DEPLOYMENT_TARGET_SETTING_NAME} --output-format human-readable-text --compile "${TARGET_BUILD_DIR}/${UNLOCALIZED_RESOURCES_FOLDER_PATH}/`basename \"$RESOURCE_PATH\" .xib`.nib" "$RESOURCE_PATH" --sdk "${SDKROOT}" ${TARGET_DEVICE_ARGS} 53 | ;; 54 | *.framework) 55 | echo "mkdir -p ${TARGET_BUILD_DIR}/${FRAMEWORKS_FOLDER_PATH}" 56 | mkdir -p "${TARGET_BUILD_DIR}/${FRAMEWORKS_FOLDER_PATH}" 57 | echo "rsync -av $RESOURCE_PATH ${TARGET_BUILD_DIR}/${FRAMEWORKS_FOLDER_PATH}" 58 | rsync -av "$RESOURCE_PATH" "${TARGET_BUILD_DIR}/${FRAMEWORKS_FOLDER_PATH}" 59 | ;; 60 | *.xcdatamodel) 61 | echo "xcrun momc \"$RESOURCE_PATH\" \"${TARGET_BUILD_DIR}/${UNLOCALIZED_RESOURCES_FOLDER_PATH}/`basename "$RESOURCE_PATH"`.mom\"" 62 | xcrun momc "$RESOURCE_PATH" "${TARGET_BUILD_DIR}/${UNLOCALIZED_RESOURCES_FOLDER_PATH}/`basename "$RESOURCE_PATH" .xcdatamodel`.mom" 63 | ;; 64 | *.xcdatamodeld) 65 | echo "xcrun momc \"$RESOURCE_PATH\" \"${TARGET_BUILD_DIR}/${UNLOCALIZED_RESOURCES_FOLDER_PATH}/`basename "$RESOURCE_PATH" .xcdatamodeld`.momd\"" 66 | xcrun momc "$RESOURCE_PATH" "${TARGET_BUILD_DIR}/${UNLOCALIZED_RESOURCES_FOLDER_PATH}/`basename "$RESOURCE_PATH" .xcdatamodeld`.momd" 67 | ;; 68 | *.xcmappingmodel) 69 | echo "xcrun mapc \"$RESOURCE_PATH\" \"${TARGET_BUILD_DIR}/${UNLOCALIZED_RESOURCES_FOLDER_PATH}/`basename "$RESOURCE_PATH" .xcmappingmodel`.cdm\"" 70 | xcrun mapc "$RESOURCE_PATH" "${TARGET_BUILD_DIR}/${UNLOCALIZED_RESOURCES_FOLDER_PATH}/`basename "$RESOURCE_PATH" .xcmappingmodel`.cdm" 71 | ;; 72 | *.xcassets) 73 | ABSOLUTE_XCASSET_FILE=$(realpath "$RESOURCE_PATH") 74 | XCASSET_FILES+=("$ABSOLUTE_XCASSET_FILE") 75 | ;; 76 | *) 77 | echo "$RESOURCE_PATH" 78 | echo "$RESOURCE_PATH" >> "$RESOURCES_TO_COPY" 79 | ;; 80 | esac 81 | } 82 | 83 | mkdir -p "${TARGET_BUILD_DIR}/${UNLOCALIZED_RESOURCES_FOLDER_PATH}" 84 | rsync -avr --copy-links --no-relative --exclude '*/.svn/*' --files-from="$RESOURCES_TO_COPY" / "${TARGET_BUILD_DIR}/${UNLOCALIZED_RESOURCES_FOLDER_PATH}" 85 | if [[ "${ACTION}" == "install" ]] && [[ "${SKIP_INSTALL}" == "NO" ]]; then 86 | mkdir -p "${INSTALL_DIR}/${UNLOCALIZED_RESOURCES_FOLDER_PATH}" 87 | rsync -avr --copy-links --no-relative --exclude '*/.svn/*' --files-from="$RESOURCES_TO_COPY" / "${INSTALL_DIR}/${UNLOCALIZED_RESOURCES_FOLDER_PATH}" 88 | fi 89 | rm -f "$RESOURCES_TO_COPY" 90 | 91 | if [[ -n "${WRAPPER_EXTENSION}" ]] && [ "`xcrun --find actool`" ] && [ -n "$XCASSET_FILES" ] 92 | then 93 | # Find all other xcassets (this unfortunately includes those of path pods and other targets). 94 | OTHER_XCASSETS=$(find "$PWD" -iname "*.xcassets" -type d) 95 | while read line; do 96 | if [[ $line != "`realpath $PODS_ROOT`*" ]]; then 97 | XCASSET_FILES+=("$line") 98 | fi 99 | done <<<"$OTHER_XCASSETS" 100 | 101 | printf "%s\0" "${XCASSET_FILES[@]}" | xargs -0 xcrun actool --output-format human-readable-text --notices --warnings --platform "${PLATFORM_NAME}" --minimum-deployment-target "${!DEPLOYMENT_TARGET_SETTING_NAME}" ${TARGET_DEVICE_ARGS} --compress-pngs --compile "${BUILT_PRODUCTS_DIR}/${UNLOCALIZED_RESOURCES_FOLDER_PATH}" 102 | fi 103 | -------------------------------------------------------------------------------- /Example/Pods/Target Support Files/Pods-CorduxPrototype/Pods-CorduxPrototype-umbrella.h: -------------------------------------------------------------------------------- 1 | #import 2 | 3 | 4 | FOUNDATION_EXPORT double Pods_CorduxPrototypeVersionNumber; 5 | FOUNDATION_EXPORT const unsigned char Pods_CorduxPrototypeVersionString[]; 6 | 7 | -------------------------------------------------------------------------------- /Example/Pods/Target Support Files/Pods-CorduxPrototype/Pods-CorduxPrototype.debug.xcconfig: -------------------------------------------------------------------------------- 1 | EMBEDDED_CONTENT_CONTAINS_SWIFT = YES 2 | FRAMEWORK_SEARCH_PATHS = $(inherited) "$PODS_CONFIGURATION_BUILD_DIR/Cordux" 3 | GCC_PREPROCESSOR_DEFINITIONS = $(inherited) COCOAPODS=1 4 | LD_RUNPATH_SEARCH_PATHS = $(inherited) '@executable_path/Frameworks' '@loader_path/Frameworks' 5 | OTHER_CFLAGS = $(inherited) -iquote "$PODS_CONFIGURATION_BUILD_DIR/Cordux/Cordux.framework/Headers" 6 | OTHER_LDFLAGS = $(inherited) -framework "Cordux" 7 | OTHER_SWIFT_FLAGS = $(inherited) "-D" "COCOAPODS" 8 | PODS_BUILD_DIR = $BUILD_DIR 9 | PODS_CONFIGURATION_BUILD_DIR = $PODS_BUILD_DIR/$(CONFIGURATION)$(EFFECTIVE_PLATFORM_NAME) 10 | PODS_ROOT = ${SRCROOT}/Pods 11 | -------------------------------------------------------------------------------- /Example/Pods/Target Support Files/Pods-CorduxPrototype/Pods-CorduxPrototype.modulemap: -------------------------------------------------------------------------------- 1 | framework module Pods_CorduxPrototype { 2 | umbrella header "Pods-CorduxPrototype-umbrella.h" 3 | 4 | export * 5 | module * { export * } 6 | } 7 | -------------------------------------------------------------------------------- /Example/Pods/Target Support Files/Pods-CorduxPrototype/Pods-CorduxPrototype.release.xcconfig: -------------------------------------------------------------------------------- 1 | EMBEDDED_CONTENT_CONTAINS_SWIFT = YES 2 | FRAMEWORK_SEARCH_PATHS = $(inherited) "$PODS_CONFIGURATION_BUILD_DIR/Cordux" 3 | GCC_PREPROCESSOR_DEFINITIONS = $(inherited) COCOAPODS=1 4 | LD_RUNPATH_SEARCH_PATHS = $(inherited) '@executable_path/Frameworks' '@loader_path/Frameworks' 5 | OTHER_CFLAGS = $(inherited) -iquote "$PODS_CONFIGURATION_BUILD_DIR/Cordux/Cordux.framework/Headers" 6 | OTHER_LDFLAGS = $(inherited) -framework "Cordux" 7 | OTHER_SWIFT_FLAGS = $(inherited) "-D" "COCOAPODS" 8 | PODS_BUILD_DIR = $BUILD_DIR 9 | PODS_CONFIGURATION_BUILD_DIR = $PODS_BUILD_DIR/$(CONFIGURATION)$(EFFECTIVE_PLATFORM_NAME) 10 | PODS_ROOT = ${SRCROOT}/Pods 11 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2016 WillowTree, Inc. 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Cordux 2 | 3 | [![Carthage compatible](https://img.shields.io/badge/Carthage-compatible-4BC51D.svg?style=flat)](https://github.com/Carthage/Carthage) 4 | 5 | You got your Redux in my app coordinator 6 | 7 | ## Installation 8 | 9 | CocoaPods: 10 | 11 | ```ruby 12 | pod 'Cordux' 13 | ``` 14 | 15 | Carthage: 16 | 17 | ```ruby 18 | github "willowtreeapps/cordux" >= 0.1 19 | ``` 20 | 21 | ## Cordux combines app coordinators with Redux 22 | 23 | Soroush Khanlou's [blog post](http://khanlou.com/2015/10/coordinators-redux/) 24 | gives a great rationale for and explanation of app coordinators. This project 25 | combines those ideas with a Redux-like architecture, which you can read about 26 | on the [ReSwift](https://github.com/ReSwift/ReSwift) repository. 27 | 28 | Combining the two natively leads to a lot of advantages: 29 | 30 | * View controllers become even simpler 31 | * Action creator methods clearly live in app coordinators 32 | * Navigating via route becomes simpler 33 | * Normal navigation and routing share code paths 34 | * Deep linking becomes almost trivial 35 | 36 | In this model, view controllers have exactly two responsibilities: 37 | 38 | 1. Render latest state 39 | 2. Convert UI interaction into user intents 40 | 41 | For example... 42 | 43 | ```swift 44 | struct ProductViewModel { 45 | let name: String 46 | let sku: String 47 | let price: Double 48 | } 49 | 50 | protocol ProductViewControllerHandler: class { 51 | func purchase(sku: String) 52 | } 53 | 54 | final class ProductViewController: UIViewController { 55 | @IBOutlet var nameLabel: UILabel! 56 | 57 | weak var handler: ProductViewControllerHandler! 58 | var viewModel: ProductViewModel! 59 | 60 | func inject(handler handler: ProductViewControllerHandler) { 61 | self.handler = handler 62 | } 63 | 64 | func render(viewModel: ProductViewModel) { 65 | self.viewModel = viewModel 66 | nameLabel.text = viewModel.name 67 | } 68 | 69 | @IBAction func purchaseButtonTapped(sender: AnyObject) { 70 | handler.purchase(viewModel.sku) 71 | } 72 | } 73 | ``` 74 | 75 | ## Usage 76 | 77 | To use Cordux, you will need to create app coordinators for each main "scene" in 78 | your app. For example coordinators, please see the included 79 | [sample app](https://github.com/willowtreeapps/cordux/tree/develop/Example). 80 | 81 | ## Alternatives 82 | 83 | The biggest win that Cordux provides is in routing. If your app does not need 84 | to manage route via state, or navigate via route, it may make more sense to 85 | implement your own app coordinators and use ReSwift for your state management. 86 | 87 | ## Goals & Non-Goals 88 | 89 | *Goals* 90 | 91 | The primary goal is to simplify app level code with robust framework code. The 92 | following are the main areas of interest: 93 | 94 | * Call point API 95 | * Navigation code 96 | * Routing code 97 | * Subscriptions to the store 98 | * View controller lifecycle needs 99 | 100 | *Non-Goals* 101 | 102 | * Time travel 103 | * State restoration 104 | * State and action serialization 105 | * Supporting additional strategies for asynchronous work or side effects 106 | 107 | ## Roadmap 108 | 109 | Production level apps at WillowTree are currently being developed with Cordux. 110 | By the end of 2016 we hope to have the needs of this framework figured out. 111 | 112 | If ReSwift adopts our routing needs, Cordux may become a ReSwift extension. 113 | 114 | ## Code of Conduct 115 | 116 | Please read our [code of conduct](https://github.com/willowtreeapps/cordux/blob/develop/CODE_OF_CONDUCT.md) 117 | before participating. Please report any violations to 118 | [open.source.conduct@willowtreeapps.com](mailto:open.source.conduct@willowtreeapps.com.). 119 | 120 | ## Contributing 121 | 122 | Please read our 123 | [contributing guidelines](https://github.com/willowtreeapps/cordux/blob/develop/CONTRIBUTING.md) 124 | before contributing. Included are directions for opening issues, coding 125 | standards, and notes on development. 126 | 127 | Beyond contributing to the main code base, documentation and unit tests are 128 | always welcome contributions. 129 | -------------------------------------------------------------------------------- /Sources/Context.swift: -------------------------------------------------------------------------------- 1 | // 2 | // ViewControllerContext.swift 3 | // CorduxPrototype 4 | // 5 | // Created by Ian Terrell on 7/22/16. 6 | // Copyright © 2016 WillowTree. All rights reserved. 7 | // 8 | 9 | import UIKit 10 | 11 | public protocol Contextual: class { 12 | var corduxContext: Context? { get } 13 | } 14 | 15 | public final class Context: NSObject { 16 | public let routeSegment: RouteConvertible? 17 | var lifecycleDelegate: LifecycleDelegateCollection 18 | 19 | public init(routeSegment: RouteConvertible? = nil, lifecycleDelegate: ViewControllerLifecycleDelegate? = nil) { 20 | self.routeSegment = routeSegment 21 | self.lifecycleDelegate = LifecycleDelegateCollection() 22 | 23 | if let delegate = lifecycleDelegate { 24 | self.lifecycleDelegate.append(delegate) 25 | } 26 | } 27 | } 28 | 29 | extension UIViewController: Contextual { 30 | private struct ViewControllerKeys { 31 | static var Context = "cordux_context" 32 | } 33 | 34 | public var corduxContext: Context? { 35 | get { 36 | return objc_getAssociatedObject(self, &ViewControllerKeys.Context) as? Context 37 | } 38 | 39 | set { 40 | if let newValue = newValue { 41 | objc_setAssociatedObject( 42 | self, 43 | &ViewControllerKeys.Context, 44 | newValue as Context?, 45 | .OBJC_ASSOCIATION_RETAIN_NONATOMIC 46 | ) 47 | } 48 | } 49 | } 50 | 51 | public func addLifecycleDelegate(_ delegate: ViewControllerLifecycleDelegate) { 52 | guard let context = corduxContext else { 53 | corduxContext = Context(lifecycleDelegate: delegate) 54 | return 55 | } 56 | 57 | context.lifecycleDelegate.append(delegate) 58 | } 59 | } 60 | -------------------------------------------------------------------------------- /Sources/Coordinator.swift: -------------------------------------------------------------------------------- 1 | // 2 | // Coordinators.swift 3 | // CorduxPrototype 4 | // 5 | // Created by Ian Terrell on 7/22/16. 6 | // Copyright © 2016 WillowTree. All rights reserved. 7 | // 8 | 9 | import UIKit 10 | 11 | /// Type erased protocol for coordinators. 12 | public protocol AnyCoordinator: class { 13 | 14 | /// The current route that the coordinator is managing. 15 | /// 16 | /// This property should return the current route for the current view controller hierarchy 17 | /// from the point of view of this coordinator. 18 | var route: Route { get } 19 | 20 | /// This method is called before `prepareForRoute(_:completionHandler:)` with the same route to determine if that 21 | /// method needs called. 22 | /// 23 | /// A `route` of `nil` is used to indicate that the coordinator's root view controller will be removed from the 24 | /// hierarchy. If the coordinator is managing a view controller that could interfere with routing, such as a popover 25 | /// or a modal, then it should return true to tell the system that it needs to do so. 26 | /// 27 | /// - Parameters: 28 | /// - _: the `Route` that will be prepared for in `prepareForRoute(_:completionHandler:)` following this call 29 | func needsToPrepareForRoute(_: Route?) -> Bool 30 | 31 | /// This method is called before `setRoute(_:completionHandler:)` and with the same values, but only when 32 | /// `needsToPrepareForRoute(_:)` returns true. 33 | /// 34 | /// A `route` of `nil` is used to indicate that the coordinator's root view controller will be removed from the 35 | /// hierarchy. If the coordinator is managing a view controller that could interfere with routing, such as a popover 36 | /// or a modal, then it should reset the view controller state to one that can be properly routed. 37 | /// 38 | /// When the view controller state has been made ready for routing, `completionHandler` must be called. 39 | /// 40 | /// - Parameters: 41 | /// - _: the `Route` that will be routed to in `setRoute(_:completionHandler:)` following this call 42 | /// - completionHandler: this closure must be called when the view controller hierarchy has been prepared for 43 | /// routing 44 | func prepareForRoute(_: Route?, completionHandler: @escaping () -> Void) 45 | 46 | /// When called, the coordinator should adjust the view controller hierarchy to what is 47 | /// desired by the `Route`. When the navigation has been completed, `completionHandler` must be called. 48 | /// 49 | /// - Parameters: 50 | /// - _: the `Route` that should be navigated to 51 | /// - completionHandler: this closure must be called when the routing is completed 52 | func setRoute(_: Route?, completionHandler: @escaping () -> Void) 53 | 54 | /// Start the coordinator at the current route. A route of nil is used to indicate a coordinator for which 55 | /// its view controller is not intended to be part of the current view hierarchy. 56 | /// 57 | /// If the coordinator needs to adjust the route from what's given, it should call store.setRoute(_:) 58 | /// during this method. This may occur when the route is empty, and the coordinator wishes 59 | /// to set up initial state according to its own internal logic. 60 | /// 61 | /// Otherwise, if the route is complete, it should set up the view controller 62 | /// hierarchy accordingly. 63 | /// 64 | /// In all cases, the rootViewController should be ready to be displayed after this method 65 | /// is called. 66 | func start(route: Route?) 67 | 68 | /// The root view controller of the hierarchy managed by this coordinator. 69 | var rootViewController: UIViewController { get } 70 | } 71 | 72 | public protocol Coordinator: AnyCoordinator { 73 | associatedtype State: StateType 74 | var store: Store { get } 75 | } 76 | 77 | public func wrapPresentingCompletionHandler(coordinator: AnyCoordinator, completionHandler: @escaping () -> Void) -> () -> Void { 78 | return { 79 | guard let coordinator = coordinator as? AnyPresentingCoordinator, coordinator.hasStoredPresentable else { 80 | completionHandler() 81 | return 82 | } 83 | 84 | coordinator.presentStoredPresentable(completionHandler: completionHandler) 85 | } 86 | } 87 | -------------------------------------------------------------------------------- /Sources/Logging.swift: -------------------------------------------------------------------------------- 1 | // 2 | // Logging.swift 3 | // Cordux 4 | // 5 | // Created by Ian Terrell on 8/22/16. 6 | // Copyright © 2016 WillowTree. All rights reserved. 7 | // 8 | 9 | import Foundation 10 | 11 | public typealias RouteLogger = (RouteEvent)->() 12 | 13 | /// A routing event that can be logged 14 | public enum RouteEvent { 15 | /// Route was updated directly by app code 16 | case set(Route) 17 | 18 | /// Route was updated by a routing action handled by the store 19 | case store(Route) 20 | 21 | /// Route was updated by the app's reducer 22 | case reducer(Route) 23 | } 24 | 25 | public func ConsoleRouteLogger(event: RouteEvent) { 26 | switch event { 27 | case .set(let route): 28 | log(route: route, annotation: "(set) ") 29 | case .store(let route): 30 | log(route: route, annotation: "(store) ") 31 | case .reducer(let route): 32 | log(route: route, annotation: "(reducer)") 33 | } 34 | } 35 | 36 | #if swift(>=3) 37 | private func log(route: Route, annotation: String) { 38 | print("ROUTE \(annotation): \(route.components.joined(separator: "/"))") 39 | } 40 | #else 41 | private func log(route route: Route, annotation: String) { 42 | print("ROUTE \(annotation): \(route.components.joinWithSeparator("/"))") 43 | } 44 | #endif 45 | -------------------------------------------------------------------------------- /Sources/Middleware.swift: -------------------------------------------------------------------------------- 1 | // 2 | // Middleware.swift 3 | // Cordux 4 | // 5 | // Created by Ian Terrell on 8/22/16. 6 | // Copyright © 2016 WillowTree. All rights reserved. 7 | // 8 | 9 | import Foundation 10 | 11 | #if swift(>=3) 12 | public protocol AnyMiddleware { 13 | func _before(action: Action, state: StateType) 14 | func _after(action: Action, state: StateType) 15 | } 16 | 17 | public protocol Middleware: AnyMiddleware { 18 | associatedtype State 19 | func before(action: Action, state: State) 20 | func after(action: Action, state: State) 21 | } 22 | 23 | public extension Middleware { 24 | func _before(action: Action, state: StateType) { 25 | withSpecificTypes(action, state: state, function: before) 26 | } 27 | func _after(action: Action, state: StateType) { 28 | withSpecificTypes(action, state: state, function: after) 29 | } 30 | } 31 | #else 32 | public protocol AnyMiddleware { 33 | func _before(action action: Action, state: StateType) 34 | func _after(action action: Action, state: StateType) 35 | } 36 | 37 | public protocol Middleware: AnyMiddleware { 38 | associatedtype State 39 | func before(action action: Action, state: State) 40 | func after(action action: Action, state: State) 41 | } 42 | 43 | public extension Middleware { 44 | func _before(action action: Action, state: StateType) { 45 | withSpecificTypes(action, state: state, function: before) 46 | } 47 | func _after(action: Action, state: StateType) { 48 | withSpecificTypes(action, state: state, function: after) 49 | } 50 | } 51 | #endif 52 | -------------------------------------------------------------------------------- /Sources/NavigationControllerCoordinator.swift: -------------------------------------------------------------------------------- 1 | // 2 | // NavigationControllerCoordinator.swift 3 | // Cordux 4 | // 5 | // Created by Ian Terrell on 7/28/16. 6 | // Copyright © 2016 WillowTree. All rights reserved. 7 | // 8 | 9 | import UIKit 10 | 11 | public protocol NavigationControllerCoordinator: Coordinator { 12 | var navigationController: UINavigationController { get } 13 | func updateRoute(_ route: Route, completionHandler: @escaping () -> Void) 14 | } 15 | 16 | public extension NavigationControllerCoordinator { 17 | public var rootViewController: UIViewController { return navigationController } 18 | 19 | public var route: Route { 20 | var route: Route = [] 21 | navigationController.viewControllers.forEach { vc in 22 | #if swift(>=3) 23 | route.append(contentsOf: vc.corduxContext?.routeSegment?.route() ?? []) 24 | #else 25 | route.appendContentsOf(vc.corduxContext?.routeSegment?.route() ?? []) 26 | #endif 27 | } 28 | return route 29 | } 30 | 31 | public func needsToPrepareForRoute(_ route: Route?) -> Bool { 32 | return false 33 | } 34 | 35 | public func prepareForRoute(_ route: Route?, completionHandler: @escaping () -> Void) { 36 | completionHandler() 37 | } 38 | 39 | public func setRoute(_ newRoute: Route?, completionHandler: @escaping () -> Void) { 40 | guard let newRoute = newRoute, newRoute != route else { 41 | completionHandler() 42 | return 43 | } 44 | 45 | updateRoute(newRoute, completionHandler: completionHandler) 46 | } 47 | 48 | public func popRoute(_ viewController: UIViewController) { 49 | guard let routeSegment = viewController.corduxContext?.routeSegment else { 50 | return 51 | } 52 | 53 | store.setRoute(.pop(routeSegment.route())) 54 | } 55 | } 56 | 57 | extension UINavigationController { 58 | public func pushViewController(_ viewController: UIViewController, animated: Bool, completion: @escaping () -> Void) { 59 | pushViewController(viewController, animated: animated) 60 | animateWithCompletion(animated: animated, completion: completion) 61 | } 62 | 63 | public func popViewController(animated: Bool, completion: @escaping () -> Void) { 64 | popViewController(animated: animated) 65 | animateWithCompletion(animated: animated, completion: completion) 66 | } 67 | 68 | public func popToViewController(_ viewController: UIViewController, animated: Bool, completion: @escaping () -> Void) { 69 | popToViewController(viewController, animated: animated) 70 | animateWithCompletion(animated: animated, completion: completion) 71 | } 72 | 73 | @discardableResult 74 | public func popToRootViewController(animated: Bool, completion: @escaping () -> Void) -> [UIViewController]? { 75 | let controllers = popToRootViewController(animated: animated) 76 | animateWithCompletion(animated: animated, completion: completion) 77 | return controllers 78 | } 79 | 80 | public func setViewControllers(_ viewControllers: [UIViewController], animated: Bool, completion: @escaping () -> Void) { 81 | setViewControllers(viewControllers, animated: animated) 82 | animateWithCompletion(animated: animated, completion: completion) 83 | } 84 | 85 | func animateWithCompletion(animated: Bool, completion: @escaping () -> Void) { 86 | guard animated, let coordinator = transitionCoordinator else { 87 | completion() 88 | return 89 | } 90 | 91 | coordinator.animate(alongsideTransition: nil) { _ in completion() } 92 | } 93 | } 94 | -------------------------------------------------------------------------------- /Sources/NavigationControllerMetaCoordinator.swift: -------------------------------------------------------------------------------- 1 | // 2 | // NavigationControllerMetaCoordinator.swift 3 | // Cordux 4 | // 5 | // Created by Ian Terrell on 11/18/16. 6 | // Copyright © 2016 WillowTree. All rights reserved. 7 | // 8 | 9 | import UIKit 10 | 11 | /// A coordinator to manage a navigation controller where each child view controller is managed by a single coordinator. 12 | /// 13 | /// This has some limitations: 14 | /// - One controller per coordinator 15 | /// - Routes for each child coordinator are fixed 16 | /// - This coordinator does not rearrange children; only pushes and pops 17 | /// - All child coordinators that are disappearing have `prepareForRoute(nil)` called simultaneously. 18 | /// 19 | /// Implementing classes should define the `ViewControllerLifecycleDelegate` method 20 | /// `didMove(toParentViewController:viewController:` to pop off coordinators when necessary. This logic should suffice: 21 | /** 22 | ``` 23 | func didMove(toParentViewController parent: UIViewController?, viewController: UIViewController) { 24 | if parent == nil { 25 | coordinators.removeLast() 26 | } 27 | } 28 | ``` 29 | */ 30 | /// 31 | public protocol NavigationControllerMetaCoordinator: Coordinator, ViewControllerLifecycleDelegate { 32 | var navigationController: UINavigationController { get } 33 | 34 | var coordinators: [AnyCoordinator] { get set } 35 | 36 | func coordinators(for route: Route) -> [AnyCoordinator] 37 | } 38 | 39 | public extension NavigationControllerMetaCoordinator { 40 | public var rootViewController: UIViewController { return navigationController } 41 | 42 | public var route: Route { 43 | var route: Route = [] 44 | coordinators.forEach { coordinator in 45 | #if swift(>=3) 46 | route.append(contentsOf: coordinator.route) 47 | #else 48 | route.appendContentsOf(coordinator.route) 49 | #endif 50 | } 51 | return route 52 | } 53 | 54 | /// - note: This method always returns false and child coordinators are not queried. 55 | public func needsToPrepareForRoute(_ route: Route?) -> Bool { 56 | return false 57 | } 58 | 59 | /// - note: The completion handler is called immediately, so all leaving coordinators can only reasonably 60 | /// prepare for nil routes synchronously. 61 | public func prepareForRoute(_ route: Route?, completionHandler: @escaping () -> Void) { 62 | let number = numberOfLastExistingCoordinator(for: route) 63 | for i in number.. Void) { 70 | guard let route = route, route != self.route else { 71 | completionHandler() 72 | return 73 | } 74 | 75 | let number = numberOfLastExistingCoordinator(for: route) 76 | let numberToRemove = coordinators.count - number 77 | coordinators.removeLast(numberToRemove) 78 | var viewControllers = navigationController.viewControllers 79 | viewControllers.removeLast(numberToRemove) 80 | 81 | var newRouteTail = route 82 | for i in 0.. Int { 98 | guard var route = route else { 99 | return 0 100 | } 101 | 102 | var index = -1 103 | for (i, coordinator) in coordinators.enumerated() { 104 | if route.isPrefixed(with: coordinator.route) { 105 | index = i 106 | route = Route(route.suffix(from: coordinator.route.components.count)) 107 | } else { 108 | return index + 1 109 | } 110 | } 111 | return index + 1 112 | } 113 | 114 | public func popRoute(_ viewController: UIViewController) { 115 | guard let routeSegment = viewController.corduxContext?.routeSegment else { 116 | return 117 | } 118 | 119 | store.setRoute(.pop(routeSegment.route())) 120 | } 121 | } 122 | 123 | -------------------------------------------------------------------------------- /Sources/PresentingCoordinator.swift: -------------------------------------------------------------------------------- 1 | // 2 | // PresentingCoordinator.swift 3 | // Cordux 4 | // 5 | // Created by Ian Terrell on 11/2/16. 6 | // Copyright © 2016 WillowTree. All rights reserved. 7 | // 8 | 9 | import UIKit 10 | 11 | public protocol AnyPresentingCoordinator { 12 | var hasStoredPresentable: Bool { get } 13 | func presentStoredPresentable(completionHandler: @escaping () -> Void) 14 | } 15 | 16 | public protocol PresentingCoordinator: Coordinator, AnyPresentingCoordinator, ViewControllerLifecycleDelegate { 17 | var rootCoordinator: AnyCoordinator { get } 18 | var presented: Scene? { get set } 19 | 20 | var presentables: [GeneratingScene] { get } 21 | func parsePresentableRoute(_ route: Route) -> (rootRoute: Route, presentable: (route: Route, scene: GeneratingScene)?) 22 | 23 | func present(scene: GeneratingScene, route: Route, completionHandler: @escaping () -> Void) 24 | func dismiss(completionHandler: @escaping () -> Void) 25 | } 26 | 27 | public extension PresentingCoordinator { 28 | public var rootViewController: UIViewController { 29 | return rootCoordinator.rootViewController 30 | } 31 | 32 | public var route: Route { 33 | var route: Route = [] 34 | #if swift(>=3) 35 | route.append(contentsOf: rootCoordinator.route) 36 | #else 37 | route.appendContentsOf(rootCoordinator.route) 38 | #endif 39 | if let presented = presented, isPresentedViewControllerUpToDate { 40 | #if swift(>=3) 41 | route.append(contentsOf: presented.route()) 42 | #else 43 | route.appendContentsOf(presented.route()) 44 | #endif 45 | } 46 | return route 47 | } 48 | 49 | public func needsToPrepareForRoute(_ newValue: Route?) -> Bool { 50 | pruneOutOfDatePresented() 51 | 52 | guard let route = newValue else { 53 | if presented != nil { 54 | return true 55 | } 56 | return rootCoordinator.needsToPrepareForRoute(nil) 57 | } 58 | 59 | let (rootRoute, presentable) = parsePresentableRoute(route) 60 | let rootNeedsToPrepare = rootCoordinator.needsToPrepareForRoute(rootRoute) 61 | 62 | guard let presented = presented else { 63 | return rootNeedsToPrepare 64 | } 65 | 66 | guard 67 | let newPresentable = presentable, 68 | presented.tag == newPresentable.scene.tag, 69 | !newPresentable.route.components.isEmpty 70 | else { 71 | return true 72 | } 73 | 74 | let presentedNeedsToPrepare = presented.coordinator.needsToPrepareForRoute(newPresentable.route) 75 | return rootNeedsToPrepare || presentedNeedsToPrepare 76 | } 77 | 78 | public func prepareForRoute(_ newValue: Route?, completionHandler: @escaping () -> Void) { 79 | guard let route = newValue else { 80 | withGroup(completionHandler) { group in 81 | group.enter() 82 | rootCoordinator.prepareForRoute(nil) { 83 | group.leave() 84 | } 85 | group.enter() 86 | dismiss() { 87 | group.leave() 88 | } 89 | } 90 | return 91 | } 92 | 93 | withGroup(completionHandler) { group in 94 | let (rootRoute, presentable) = parsePresentableRoute(route) 95 | 96 | group.enter() 97 | rootCoordinator.prepareForRoute(rootRoute) { 98 | group.leave() 99 | } 100 | 101 | if let presented = presented { 102 | group.enter() 103 | if let presentedRoute = presentable?.route { 104 | presented.coordinator.prepareForRoute(presentedRoute) { 105 | group.leave() 106 | } 107 | } else { 108 | dismiss() { 109 | group.leave() 110 | } 111 | } 112 | } 113 | } 114 | } 115 | 116 | public func setRoute(_ newValue: Route?, completionHandler: @escaping () -> Void) { 117 | guard let newValue = newValue else { 118 | dismiss(completionHandler: completionHandler) 119 | return 120 | } 121 | 122 | guard newValue != route else { 123 | completionHandler() 124 | return 125 | } 126 | 127 | withGroup(completionHandler) { group in 128 | let (rootRoute, presentable) = parsePresentableRoute(newValue) 129 | 130 | group.enter() 131 | rootCoordinator.setRoute(rootRoute) { 132 | group.leave() 133 | } 134 | 135 | group.enter() 136 | if let presentable = presentable { 137 | present(scene: presentable.scene, route: presentable.route) { 138 | group.leave() 139 | } 140 | } else { 141 | dismiss() { 142 | group.leave() 143 | } 144 | } 145 | } 146 | } 147 | 148 | func start(route: Route?) { 149 | guard let route = route else { 150 | rootCoordinator.start(route: nil) 151 | return 152 | } 153 | 154 | let (rootRoute, presentable) = parsePresentableRoute(route) 155 | rootCoordinator.start(route: rootRoute) 156 | 157 | if let presentable = presentable { 158 | rootCoordinator.rootViewController.corduxToPresent = ToPresentBox(route: presentable.route, scene: presentable.scene) 159 | } 160 | } 161 | 162 | /// Checks whether the presented coordinator's view controller is still presented (or nil if none). 163 | /// 164 | /// A dead presented coordinator can occur if the presented view controller has already been dismissed outside of 165 | /// normal routing, e.g. from a button press on a UIAlertController. 166 | var isPresentedViewControllerUpToDate: Bool { 167 | return rootViewController.presentedViewController == presented?.coordinator.rootViewController 168 | } 169 | 170 | /// Prunes the current presented coordinator if not up to date 171 | func pruneOutOfDatePresented() { 172 | if !isPresentedViewControllerUpToDate { 173 | self.presented = nil 174 | } 175 | } 176 | 177 | public var hasStoredPresentable: Bool { 178 | return rootCoordinator.rootViewController.corduxToPresent != nil 179 | } 180 | 181 | public func presentStoredPresentable(completionHandler: @escaping () -> Void) { 182 | guard let toPresent = rootCoordinator.rootViewController.corduxToPresent else { 183 | completionHandler() 184 | return 185 | } 186 | 187 | rootCoordinator.rootViewController.corduxToPresent = nil 188 | present(scene: toPresent.scene, route: toPresent.route, completionHandler: completionHandler) 189 | } 190 | 191 | func parsePresentableRoute(_ route: Route) -> (rootRoute: Route, presentable: (route: Route, scene: GeneratingScene)?) { 192 | for presentable in presentables { 193 | let parts = route.components.split(separator: presentable.tag, 194 | maxSplits: 2, 195 | omittingEmptySubsequences: false) 196 | if parts.count == 2 { 197 | return (Route(parts[0]), (Route(parts[1]), presentable)) 198 | } 199 | } 200 | return (route, nil) 201 | } 202 | 203 | /// Presents the presentable coordinator. 204 | /// If it is already presented, this method merely adjusts the route. 205 | /// If a different presentable is currently presented, this method dismisses it first. 206 | func present(scene: GeneratingScene, route: Route, completionHandler: @escaping () -> Void) { 207 | if let presented = presented { 208 | if scene.tag != presented.tag { 209 | dismiss() { 210 | DispatchQueue.main.async { 211 | self.present(scene: scene, route: route, completionHandler: completionHandler) 212 | } 213 | } 214 | } else { 215 | presented.coordinator.setRoute(route, completionHandler: completionHandler) 216 | } 217 | return 218 | } 219 | 220 | let coordinator = scene.buildCoordinator() 221 | coordinator.start(route: route) 222 | let presentedController = coordinator.rootViewController 223 | presentedController.addLifecycleDelegate(self) 224 | rootViewController.present(presentedController, animated: true) { 225 | self.presented = Scene(tag: scene.tag, coordinator: coordinator) 226 | completionHandler() 227 | } 228 | } 229 | 230 | /// Dismisses the currently presented coordinator if present. Noop if there isn't one. 231 | func dismiss(completionHandler: @escaping () -> Void) { 232 | guard let presented = presented else { 233 | completionHandler() 234 | return 235 | } 236 | 237 | presented.coordinator.prepareForRoute(nil) { 238 | presented.coordinator.rootViewController.dismiss(animated: true) { 239 | self.presented = nil 240 | completionHandler() 241 | } 242 | } 243 | } 244 | 245 | /// Helper method for synchronizing activities with a DispatchGroup 246 | /// 247 | /// - Parameter perform: Callback to do work with the group, executed on the main queue 248 | func withGroup(_ completionHandler: @escaping () -> Void, perform: (DispatchGroup) -> Void) { 249 | let group = DispatchGroup() 250 | perform(group) 251 | let queue = DispatchQueue(label: "PresentingCoordinatorSync") 252 | queue.async { 253 | group.wait() 254 | DispatchQueue.main.async(execute: completionHandler) 255 | } 256 | } 257 | } 258 | 259 | // MARK: - Storage Helpers 260 | 261 | fileprivate final class ToPresentBox: NSObject { 262 | let route: Route 263 | let scene: GeneratingScene 264 | 265 | init(route: Route, scene: GeneratingScene) { 266 | self.route = route 267 | self.scene = scene 268 | } 269 | } 270 | 271 | extension UIViewController { 272 | private struct CorduxPresentingCoordinatorKeys { 273 | static var ToPresent = "cordux_to_present" 274 | } 275 | 276 | fileprivate var corduxToPresent: ToPresentBox? { 277 | get { 278 | return objc_getAssociatedObject(self, &CorduxPresentingCoordinatorKeys.ToPresent) as? ToPresentBox 279 | } 280 | 281 | set { 282 | if let newValue = newValue { 283 | objc_setAssociatedObject( 284 | self, 285 | &CorduxPresentingCoordinatorKeys.ToPresent, 286 | newValue as ToPresentBox?, 287 | .OBJC_ASSOCIATION_RETAIN_NONATOMIC 288 | ) 289 | } 290 | } 291 | } 292 | } 293 | -------------------------------------------------------------------------------- /Sources/Reducer.swift: -------------------------------------------------------------------------------- 1 | // 2 | // Reducer.swift 3 | // Cordux 4 | // 5 | // Created by Ian Terrell on 7/28/16. 6 | // Copyright © 2016 WillowTree. All rights reserved. 7 | // 8 | 9 | import Foundation 10 | 11 | public protocol AnyReducer { 12 | func _handleAction(_ action: Action, state: StateType) -> StateType 13 | } 14 | 15 | public protocol Reducer: AnyReducer { 16 | associatedtype State 17 | func handleAction(_ action: Action, state: State) -> State 18 | } 19 | 20 | public extension Reducer { 21 | func _handleAction(_ action: Action, state: StateType) -> StateType { 22 | return withSpecificTypes(action, state: state, function: handleAction) 23 | } 24 | } 25 | -------------------------------------------------------------------------------- /Sources/Routing.swift: -------------------------------------------------------------------------------- 1 | // 2 | // Routing.swift 3 | // CorduxPrototype 4 | // 5 | // Created by Ian Terrell on 7/22/16. 6 | // Copyright © 2016 WillowTree. All rights reserved. 7 | // 8 | 9 | import Foundation 10 | 11 | public struct Route { 12 | public var components: [String] 13 | 14 | public func reduce(_ action: RouteAction) -> Route { 15 | switch action { 16 | case .goto(let route): 17 | return route.route() 18 | case .push(let segment): 19 | return route() + segment.route() 20 | case .pop(let segment): 21 | let segmentRoute = segment.route() 22 | let n = route().count 23 | let m = segmentRoute.count 24 | guard n >= m else { 25 | return self 26 | } 27 | let tail = Route(Array(route()[n-m.. Route 41 | } 42 | 43 | public enum RouteAction: Action { 44 | case goto(T) 45 | case push(T) 46 | case pop(T) 47 | case replace(T, T) 48 | } 49 | 50 | #if swift(>=3) 51 | extension Route: 52 | RandomAccessCollection, 53 | Sequence, 54 | RangeReplaceableCollection, 55 | ExpressibleByArrayLiteral 56 | {} 57 | #else 58 | extension Route: 59 | CollectionType, 60 | SequenceType, 61 | RangeReplaceableCollectionType, 62 | ArrayLiteralConvertible 63 | {} 64 | #endif 65 | 66 | extension Route { 67 | public init() { 68 | self.components = [] 69 | } 70 | 71 | public init(_ components: [String]) { 72 | self.components = components 73 | } 74 | 75 | public init(_ component: String) { 76 | self.init([component]) 77 | } 78 | 79 | public init(_ slice: Slice) { 80 | self.init(Array(slice)) 81 | } 82 | } 83 | 84 | extension Route: Equatable {} 85 | 86 | public func ==(lhs: Route, rhs: Route) -> Bool { 87 | return lhs.components == rhs.components 88 | } 89 | 90 | extension Route: RouteConvertible { 91 | public func route() -> Route { 92 | return self 93 | } 94 | } 95 | 96 | extension Route { 97 | public init(arrayLiteral elements: String...) { 98 | components = elements 99 | } 100 | } 101 | 102 | extension Route { 103 | func isPrefixed(with route: Route) -> Bool { 104 | return Array(components.prefix(route.components.count)) == route.components 105 | } 106 | } 107 | 108 | 109 | extension Route { 110 | #if swift(>=3) 111 | public typealias Iterator = AnyIterator 112 | public func makeIterator() -> Iterator { 113 | return AnyIterator(makeGenerator()) 114 | } 115 | #else 116 | public typealias Generator = AnyGenerator 117 | public func generate() -> Generator { 118 | return AnyGenerator(body: makeGenerator()) 119 | } 120 | #endif 121 | 122 | func makeGenerator() -> () -> (String?) { 123 | var index = 0 124 | return { 125 | if index < self.components.count { 126 | let c = self.components[index] 127 | index += 1 128 | return c 129 | 130 | } 131 | return nil 132 | } 133 | } 134 | } 135 | 136 | extension Route { 137 | public typealias Index = Int 138 | 139 | #if swift(>=3) 140 | public typealias Indices = CountableRange 141 | #endif 142 | 143 | public var startIndex: Int { 144 | return 0 145 | } 146 | 147 | public var endIndex: Int { 148 | return components.count 149 | } 150 | 151 | public func index(after i: Int) -> Int { 152 | return i + 1 153 | } 154 | 155 | public func index(before i: Int) -> Int { 156 | return i - 1 157 | } 158 | 159 | public subscript(i: Int) -> String { 160 | return components[i] 161 | } 162 | } 163 | 164 | extension Route { 165 | public mutating func reserveCapacity(_ minimumCapacity: Int) { 166 | components.reserveCapacity(minimumCapacity) 167 | } 168 | 169 | #if swift(>=3) 170 | public mutating func replaceSubrange(_ subRange: Range, with newElements: C) where C.Iterator.Element == Iterator.Element { 171 | components.replaceSubrange(subRange, with: newElements) 172 | } 173 | #else 174 | public mutating func replaceRange(_ subRange: Range, with newElements: C) where C.Generator.Element == Generator.Element { 175 | components.replaceRange(subRange, with: newElements) 176 | } 177 | #endif 178 | } 179 | 180 | public func +(lhs: Route, rhs: Route) -> Route { 181 | return Route(lhs.components + rhs.components) 182 | } 183 | 184 | public func +(lhs: RouteConvertible, rhs: Route) -> Route { 185 | return Route(lhs.route().components + rhs.components) 186 | } 187 | 188 | public func +(lhs: Route, rhs: RouteConvertible) -> Route { 189 | return Route(lhs.components + rhs.route().components) 190 | } 191 | 192 | extension String: RouteConvertible { 193 | public func route() -> Route { 194 | return Route(self) 195 | } 196 | } 197 | 198 | public extension RawRepresentable where RawValue == String { 199 | public func route() -> Route { 200 | return self.rawValue.route() 201 | } 202 | } 203 | -------------------------------------------------------------------------------- /Sources/SceneCoordinator.swift: -------------------------------------------------------------------------------- 1 | // 2 | // SceneCoordinator.swift 3 | // Cordux 4 | // 5 | // Created by Ian Terrell on 7/28/16. 6 | // Copyright © 2016 WillowTree. All rights reserved. 7 | // 8 | 9 | import UIKit 10 | 11 | /// A SceneCoordinator can manage the transition between disparate coordinators. 12 | /// For instance, if the coordinator has three child scenes, A, B, and C, then 13 | /// the scene coordinator can manage switching between them, showing only one at a time, 14 | /// as well as routing the subroute to the current scene (either A, B, or C). 15 | /// 16 | /// To do this, it wants a route prefix to indicate which scene should be shown, and 17 | /// a currentScene property to know which scene to route to. 18 | public protocol SceneCoordinator: Coordinator { 19 | /// The current scene being shown by the coordinator. 20 | var currentScene: Scene? { get set } 21 | 22 | func coordinatorForTag(_ tag: String) -> (coordinator: AnyCoordinator, started: Bool)? 23 | func presentCoordinator(_ coordinator: AnyCoordinator?, completionHandler: @escaping () -> Void) 24 | } 25 | 26 | public extension SceneCoordinator { 27 | public var route: Route { 28 | return currentScene?.route() ?? [] 29 | } 30 | 31 | public func needsToPrepareForRoute(_ route: Route?) -> Bool { 32 | guard let currentScene = currentScene else { 33 | return false 34 | } 35 | 36 | guard let route = route else { 37 | return currentScene.coordinator.needsToPrepareForRoute(nil) 38 | } 39 | 40 | return currentScene.coordinator.needsToPrepareForRoute(route) 41 | } 42 | 43 | public func prepareForRoute(_ route: Route?, completionHandler: @escaping () -> Void) { 44 | guard let currentScene = currentScene else { 45 | completionHandler() 46 | return 47 | } 48 | 49 | guard let route = route else { 50 | currentScene.coordinator.prepareForRoute(nil, completionHandler: completionHandler) 51 | return 52 | } 53 | 54 | currentScene.coordinator.prepareForRoute(sceneRoute(route), completionHandler: completionHandler) 55 | } 56 | 57 | public func setRoute(_ route: Route?, completionHandler: @escaping () -> Void) { 58 | guard let route = route, let tag = route.first else { 59 | completionHandler() 60 | return 61 | } 62 | 63 | if tag != currentScene?.tag { 64 | if let (coordinator, started) = coordinatorForTag(tag) { 65 | let wrappedCompletionHandler = wrapPresentingCompletionHandler(coordinator: coordinator, 66 | completionHandler: completionHandler) 67 | currentScene = Scene(tag: tag, coordinator: coordinator) 68 | if !started { 69 | coordinator.start(route: sceneRoute(route)) 70 | } 71 | presentCoordinator(coordinator, completionHandler: { 72 | if started { 73 | coordinator.setRoute(self.sceneRoute(route), completionHandler: wrappedCompletionHandler) 74 | } else { 75 | wrappedCompletionHandler() 76 | } 77 | }) 78 | } else { 79 | presentCoordinator(nil, completionHandler: completionHandler) 80 | } 81 | } else { 82 | currentScene?.coordinator.setRoute(sceneRoute(route), completionHandler: completionHandler) 83 | } 84 | } 85 | 86 | func sceneRoute(_ route: Route) -> Route { 87 | return Route(route.dropFirst()) 88 | } 89 | } 90 | -------------------------------------------------------------------------------- /Sources/Scenes.swift: -------------------------------------------------------------------------------- 1 | // 2 | // TaggedCoordinator.swift 3 | // Cordux 4 | // 5 | // Created by Ian Terrell on 11/2/16. 6 | // Copyright © 2016 WillowTree. All rights reserved. 7 | // 8 | 9 | import Foundation 10 | 11 | public class Scene: RouteConvertible { 12 | /// The label used to identify the coordinator and to be used as a route segment. 13 | public let tag: String 14 | 15 | /// The coordinator to attach with the label. 16 | public let coordinator: AnyCoordinator 17 | 18 | /// Initializes a Scene. 19 | /// 20 | /// - Parameters: 21 | /// - tag: The label used to identify the coordinator and to be used as a route segment. 22 | /// - coordinator: The coordinator to attach with the label. 23 | public init(tag: String, coordinator: AnyCoordinator) { 24 | self.tag = tag 25 | self.coordinator = coordinator 26 | } 27 | 28 | public func route() -> Route { 29 | return Route(tag) + coordinator.route 30 | } 31 | } 32 | 33 | public class GeneratingScene { 34 | /// The label used to identify the coordinator and to be used as a route segment. 35 | public let tag: String 36 | 37 | let factory: ()->(AnyCoordinator) 38 | 39 | /// Initializes a GeneratingScene. 40 | /// 41 | /// - Parameters: 42 | /// - tag: The label used to identify the coordinator and to be used as a route segment. 43 | /// - factory: A closure to create the coordinator for the scene. 44 | /// 45 | /// - Important: The factory closure is stored indefinitely. If you are storing this type for later use, be sure 46 | /// to mind what you capture over. Most often this will mean you should be using [unowned self]. 47 | public init(tag: String, factory: @escaping ()->(AnyCoordinator)) { 48 | self.tag = tag 49 | self.factory = factory 50 | } 51 | 52 | /// Builds a new coordinator from the factory provided at initialization. 53 | public func buildCoordinator() -> AnyCoordinator { 54 | return self.factory() 55 | } 56 | } 57 | -------------------------------------------------------------------------------- /Sources/SimpleCoordinator.swift: -------------------------------------------------------------------------------- 1 | // 2 | // SimpleCoordinator.swift 3 | // Cordux 4 | // 5 | // Created by Ian Terrell on 11/7/16. 6 | // Copyright © 2016 WillowTree. All rights reserved. 7 | // 8 | 9 | import UIKit 10 | 11 | public protocol SimpleCoordinator: Coordinator { 12 | 13 | } 14 | 15 | public extension SimpleCoordinator { 16 | public func needsToPrepareForRoute(_ route: Route?) -> Bool { 17 | return false 18 | } 19 | 20 | public func prepareForRoute(_ route: Route?, completionHandler: @escaping () -> Void) { 21 | completionHandler() 22 | } 23 | 24 | public func setRoute(_ route: Route?, completionHandler: @escaping () -> Void) { 25 | completionHandler() 26 | } 27 | } 28 | -------------------------------------------------------------------------------- /Sources/Store.swift: -------------------------------------------------------------------------------- 1 | // 2 | // Store.swift 3 | // Cordux 4 | // 5 | // Created by Ian Terrell on 7/28/16. 6 | // Copyright © 2016 WillowTree. All rights reserved. 7 | // 8 | 9 | import Foundation 10 | 11 | private let kRouteTimeoutDuration: TimeInterval = 3 12 | 13 | /// Action is a marker type that describes types that can modify state. 14 | public protocol Action {} 15 | 16 | /// StateType describes the minimum requirements for state. 17 | public protocol StateType { 18 | 19 | /// The current representation of the route for the app. 20 | /// 21 | /// This describes what the user is currently seeing and how they navigated there. 22 | var route: Route { get set } 23 | } 24 | 25 | public final class Store { 26 | public private(set) var state: State 27 | public let reducer: AnyReducer 28 | public let middlewares: [AnyMiddleware] 29 | 30 | /// Isolation queue to protect routingCount 31 | private let routeStatusQueue = DispatchQueue(label: "CorduxRoutingStatus") 32 | /// The number of routing actions currently in progress 33 | private var routingCount = 0 34 | /// Serial queue to order routing actions 35 | private let routeQueue = DispatchQueue(label: "CorduxRouting", qos: .userInitiated) 36 | /// Logger to log routing events 37 | private let routeLogger: RouteLogger? 38 | /// The last route that has been propagated 39 | private var lastRoute: Route? 40 | 41 | /// The root coordinator for the app; used for routing. 42 | public weak var rootCoordinator: AnyCoordinator? { 43 | didSet { 44 | propagateRoute(state.route) 45 | } 46 | } 47 | 48 | typealias SubscriptionType = Subscription 49 | var subscriptions: [SubscriptionType] = [] 50 | 51 | public init(initialState: State, reducer: AnyReducer, middlewares: [AnyMiddleware] = [], 52 | routeLogger: RouteLogger? = ConsoleRouteLogger) { 53 | self.state = initialState 54 | self.reducer = reducer 55 | self.middlewares = middlewares 56 | self.routeLogger = routeLogger 57 | } 58 | 59 | #if swift(>=3) 60 | public func subscribe(_ subscriber: Subscriber, _ transform: ((State) -> SelectedState)? = nil) where Subscriber.StoreSubscriberStateType == SelectedState { 61 | guard isNewSubscriber(subscriber) else { 62 | return 63 | } 64 | 65 | let sub = Subscription(subscriber: subscriber, transform: transform) 66 | subscriptions.append(sub) 67 | sub.subscriber?._newState(sub.transform?(state) ?? state) 68 | } 69 | #else 70 | public func subscribe(_ subscriber: Subscriber, _ transform: ((State) -> SelectedState)? = nil) { 71 | guard isNewSubscriber(subscriber) else { 72 | return 73 | } 74 | 75 | let sub = Subscription(subscriber: subscriber, transform: transform) 76 | subscriptions.append(sub) 77 | sub.subscriber?._newState(sub.transform?(state) ?? state) 78 | } 79 | #endif 80 | 81 | public func unsubscribe(_ subscriber: AnyStoreSubscriber) { 82 | #if swift(>=3) 83 | if let index = subscriptions.index(where: { return $0.subscriber === subscriber }) { 84 | subscriptions.remove(at: index) 85 | } 86 | #else 87 | if let index = subscriptions.index(where: { return $0.subscriber === subscriber }) { 88 | subscriptions.remove(at: index) 89 | } 90 | #endif 91 | } 92 | 93 | public func route(_ action: RouteAction) { 94 | state.route = state.route.reduce(action) 95 | routeLogger?(.store(state.route)) 96 | dispatch(action) 97 | } 98 | 99 | public func setRoute(_ action: RouteAction) { 100 | state.route = state.route.reduce(action) 101 | lastRoute = state.route 102 | routeLogger?(.set(state.route)) 103 | } 104 | 105 | public func dispatch(_ action: Action) { 106 | let state = self.state 107 | middlewares.forEach { $0._before(action: action, state: state) } 108 | let newState = reducer._handleAction(action, state: state) as! State 109 | self.state = newState 110 | 111 | #if swift(>=3) 112 | middlewares.reversed().forEach { $0._after(action: action, state: newState) } 113 | #else 114 | middlewares.reverse().forEach { $0._after(action: action, state: newState) } 115 | #endif 116 | 117 | if state.route != newState.route { 118 | routeLogger?(.reducer(newState.route)) 119 | } 120 | 121 | propagateRoute(newState.route) 122 | 123 | subscriptions = subscriptions.filter { $0.subscriber != nil } 124 | subscriptions.forEach { $0.subscriber?._newState($0.transform?(newState) ?? newState) } 125 | } 126 | 127 | /// Propagates the route through the app via the `rootCoordinator`. 128 | /// 129 | /// This method will execute the routing action synchronously if no other routing actions are already in progress. 130 | /// If a routing action is already occurring, the route will be performed asynchronously. All actions occur on 131 | /// the main thread. 132 | /// 133 | /// - Note: Does not re-propagate identical routes 134 | /// - Note: If a route is attempted to be propagated while another route is working, it is executed after the 135 | /// original propagation completes. Multiple routes are propagated asynchronously in order. 136 | /// - Note: All routing methods must call their completion handlers on the main thread. 137 | /// This method times out after 3 seconds. 138 | /// 139 | /// - Parameter route: The route to propagate 140 | private func propagateRoute(_ route: Route) { 141 | guard route != lastRoute, let rootCoordinator = rootCoordinator else { 142 | return 143 | } 144 | lastRoute = route 145 | 146 | func waitForRoutingCompletion(_ group: DispatchGroup) { 147 | let result = group.wait(timeout: .now() + kRouteTimeoutDuration) 148 | if case .timedOut = result { 149 | alertStuckRouter() 150 | } 151 | self.decrementRoutingCount() 152 | } 153 | 154 | func _setRoute(_ group: DispatchGroup) { 155 | rootCoordinator.setRoute(route) { 156 | group.leave() 157 | } 158 | } 159 | 160 | func setRoute(_ group: DispatchGroup) { 161 | guard rootCoordinator.needsToPrepareForRoute(route) else { 162 | _setRoute(group) 163 | return 164 | } 165 | rootCoordinator.prepareForRoute(route) { 166 | _setRoute(group) 167 | } 168 | } 169 | 170 | let shouldRouteAsynchronously = isRouting 171 | self.incrementRoutingCount() 172 | 173 | if shouldRouteAsynchronously { 174 | routeQueue.async { 175 | let group = DispatchGroup() 176 | group.enter() 177 | DispatchQueue.main.async { 178 | setRoute(group) 179 | } 180 | waitForRoutingCompletion(group) 181 | } 182 | } else { 183 | let group = DispatchGroup() 184 | group.enter() 185 | routeQueue.async { 186 | waitForRoutingCompletion(group) 187 | } 188 | setRoute(group) 189 | } 190 | } 191 | 192 | private var isRouting: Bool { return routeStatusQueue.sync { self.routingCount > 0 } } 193 | private func incrementRoutingCount() { routeStatusQueue.async { self.routingCount += 1 } } 194 | private func decrementRoutingCount() { routeStatusQueue.async { self.routingCount -= 1 } } 195 | 196 | func isNewSubscriber(_ subscriber: AnyStoreSubscriber) -> Bool { 197 | #if swift(>=3) 198 | guard !subscriptions.contains(where: { $0.subscriber === subscriber }) else { 199 | return false 200 | } 201 | #else 202 | guard !subscriptions.contains({ $0.subscriber === subscriber }) else { 203 | return false 204 | } 205 | #endif 206 | return true 207 | } 208 | } 209 | 210 | func alertStuckRouter() { 211 | print("[Cordux]: Router is stuck waiting for a completion handler to be called.") 212 | print("[Cordux]: Please make sure that you have called the completion handler in all routing methods (prepareForRoute, setRoute, updateRoute).") 213 | print("[Cordux]: Set a symbolic breakpoint for the `CorduxRouterStuck` symbol in order to halt the program when this happens.") 214 | CorduxRouterStuck() 215 | } 216 | 217 | func CorduxRouterStuck() {} 218 | -------------------------------------------------------------------------------- /Sources/Subscriptions.swift: -------------------------------------------------------------------------------- 1 | // 2 | // Cordux.swift 3 | // CorduxPrototype 4 | // 5 | // Created by Ian Terrell on 7/21/16. 6 | // Copyright © 2016 WillowTree. All rights reserved. 7 | // 8 | 9 | import Foundation 10 | 11 | struct Subscription { 12 | weak var subscriber: AnyStoreSubscriber? 13 | let transform: ((State) -> Any)? 14 | } 15 | 16 | public protocol AnyStoreSubscriber: class { 17 | func _newState(_ state: Any) 18 | } 19 | 20 | public protocol SubscriberType: AnyStoreSubscriber { 21 | associatedtype StoreSubscriberStateType 22 | func newState(_ subscription: StoreSubscriberStateType) 23 | } 24 | 25 | extension SubscriberType { 26 | public func _newState(_ state: Any) { 27 | if let typedState = state as? StoreSubscriberStateType { 28 | newState(typedState) 29 | } else { 30 | #if swift(>=3) 31 | preconditionFailure("Expected \(StoreSubscriberStateType.self) but received \(type(of:state))") 32 | #else 33 | preconditionFailure("Expected \(StoreSubscriberStateType.self) but received \(state.dynamicType))") 34 | #endif 35 | } 36 | } 37 | } 38 | 39 | /// Renderer is a special subscriber that has semantics that match what we expect 40 | /// in a view controller. 41 | public protocol Renderer: AnyStoreSubscriber { 42 | associatedtype ViewModel 43 | func render(_ viewModel: ViewModel) 44 | } 45 | 46 | extension Renderer { 47 | public func _newState(_ state: Any) { 48 | if let viewModel = state as? ViewModel { 49 | render(viewModel) 50 | } else { 51 | #if swift(>=3) 52 | preconditionFailure("Expected \(ViewModel.self) but received \(type(of:state))") 53 | #else 54 | preconditionFailure("Expected \(ViewModel.self) but received \(state.dynamicType))") 55 | #endif 56 | } 57 | } 58 | } 59 | -------------------------------------------------------------------------------- /Sources/TabBarControllerCoordinator.swift: -------------------------------------------------------------------------------- 1 | // 2 | // TabBarControllerCoordinator.swift 3 | // Cordux 4 | // 5 | // Created by Ian Terrell on 7/28/16. 6 | // Copyright © 2016 WillowTree. All rights reserved. 7 | // 8 | 9 | import UIKit 10 | 11 | /// A scene coordinator that keeps its scenes in an array (always in memory), and is rendered by a UITabBarController. 12 | /// Conforming types' start method should invoke the start method for each scene. 13 | public protocol TabBarControllerCoordinator: SceneCoordinator { 14 | var tabBarController: UITabBarController { get } 15 | var scenes: [Scene] { get } 16 | } 17 | 18 | public extension TabBarControllerCoordinator { 19 | public var rootViewController: UIViewController { return tabBarController } 20 | public var currentScene: Scene? { 21 | get { 22 | return scenes[tabBarController.selectedIndex] 23 | } 24 | set { 25 | // noop 26 | // The coordinator is retained by scenes array and identified by tabBarController.selectedIndex. 27 | } 28 | } 29 | 30 | func coordinatorForTag(_ tag: String) -> (coordinator: AnyCoordinator, started: Bool)? { 31 | return scenes.first(where: { $0.tag == tag }).map { ($0.coordinator, true) } 32 | } 33 | 34 | func presentCoordinator(_ coordinator: AnyCoordinator?, completionHandler: @escaping () -> Void) { 35 | guard let coordinator = coordinator else { 36 | completionHandler() 37 | return 38 | } 39 | 40 | tabBarController.selectedIndex = scenes.index(where: { $0.coordinator === coordinator }) ?? 0 41 | completionHandler() 42 | } 43 | 44 | public func setRouteForViewController(_ viewController: UIViewController) -> Bool { 45 | for scene in scenes { 46 | if scene.coordinator.rootViewController === viewController { 47 | store.setRoute(.replace(route, scene.route())) 48 | return true 49 | } 50 | } 51 | return false 52 | } 53 | } 54 | -------------------------------------------------------------------------------- /Sources/TypeHelpers.swift: -------------------------------------------------------------------------------- 1 | // 2 | // TypeHelpers.swift 3 | // Cordux 4 | // 5 | // Created by Ian Terrell on 8/22/16. 6 | // Copyright © 2016 WillowTree. All rights reserved. 7 | // 8 | 9 | import Foundation 10 | 11 | #if swift(>=3) 12 | func withSpecificTypes(_ action: Action, state genericStateType: StateType, 13 | function: (_ action: Action, _ state: SpecificStateType) -> SpecificStateType) -> StateType { 14 | guard let specificStateType = genericStateType as? SpecificStateType else { 15 | return genericStateType 16 | } 17 | 18 | return function(action, specificStateType) as! StateType 19 | } 20 | 21 | func withSpecificTypes(_ action: Action, state genericStateType: StateType, 22 | function: (_ action: Action, _ state: SpecificStateType) -> ()) { 23 | guard let specificStateType = genericStateType as? SpecificStateType else { 24 | return 25 | } 26 | 27 | function(action, specificStateType) 28 | } 29 | 30 | #else 31 | 32 | func withSpecificTypes(_ action: Action, state genericStateType: StateType, @noescape function: (action: Action, state: SpecificStateType) -> SpecificStateType) -> StateType { 33 | guard let specificStateType = genericStateType as? SpecificStateType else { 34 | return genericStateType 35 | } 36 | 37 | return function(action: action, state: specificStateType) as! StateType 38 | } 39 | 40 | func withSpecificTypes(_ action: Action, state genericStateType: StateType, @noescape function: (action: Action, state: SpecificStateType) -> ()) { 41 | guard let specificStateType = genericStateType as? SpecificStateType else { 42 | return 43 | } 44 | 45 | function(action: action, state: specificStateType) 46 | } 47 | #endif 48 | -------------------------------------------------------------------------------- /Sources/ViewControllerLifecycle.swift: -------------------------------------------------------------------------------- 1 | // 2 | // ViewControllerLifecycle.swift 3 | // CorduxPrototype 4 | // 5 | // Created by Ian Terrell on 7/22/16. 6 | // Copyright © 2016 WillowTree. All rights reserved. 7 | // 8 | 9 | import UIKit 10 | 11 | @objc public protocol ViewControllerLifecycleDelegate { 12 | @objc optional func viewDidLoad(viewController: UIViewController) 13 | @objc optional func viewWillAppear(_ animated: Bool, viewController: UIViewController) 14 | @objc optional func viewDidAppear(_ animated: Bool, viewController: UIViewController) 15 | @objc optional func viewWillDisappear(_ animated: Bool, viewController: UIViewController) 16 | @objc optional func viewDidDisappear(_ animated: Bool, viewController: UIViewController) 17 | @objc optional func didMove(toParentViewController: UIViewController?, viewController: UIViewController) 18 | } 19 | 20 | final class LifecycleDelegateCollection { 21 | var delegates: [_wrapper] = [] 22 | 23 | func append(_ delegate: ViewControllerLifecycleDelegate?) { 24 | guard let delegate = delegate else { 25 | return 26 | } 27 | delegates.append(_wrapper(delegate)) 28 | } 29 | 30 | struct _wrapper { 31 | weak var delegate: ViewControllerLifecycleDelegate? 32 | init(_ delegate: ViewControllerLifecycleDelegate) { 33 | self.delegate = delegate 34 | } 35 | } 36 | } 37 | 38 | extension LifecycleDelegateCollection { 39 | func forEach(_ body: (ViewControllerLifecycleDelegate?) -> Void) { 40 | delegates.forEach { body($0.delegate) } 41 | } 42 | 43 | func viewDidLoad(viewController: UIViewController) { 44 | forEach { $0?.viewDidLoad?(viewController: viewController) } 45 | } 46 | 47 | func viewWillAppear(_ animated: Bool, viewController: UIViewController) { 48 | forEach { $0?.viewWillAppear?(animated, viewController: viewController) } 49 | } 50 | 51 | func viewDidAppear(_ animated: Bool, viewController: UIViewController) { 52 | forEach { $0?.viewDidAppear?(animated, viewController: viewController) } 53 | } 54 | 55 | func viewWillDisappear(_ animated: Bool, viewController: UIViewController) { 56 | forEach { $0?.viewWillDisappear?(animated, viewController: viewController) } 57 | } 58 | 59 | func viewDidDisappear(_ animated: Bool, viewController: UIViewController) { 60 | forEach { $0?.viewDidDisappear?(animated, viewController: viewController) } 61 | } 62 | 63 | func didMove(toParentViewController parent: UIViewController?, viewController: UIViewController) { 64 | forEach { $0?.didMove?(toParentViewController: parent, viewController: viewController) } 65 | } 66 | } 67 | 68 | extension UIViewController { 69 | static let swizzle: Void = { 70 | UIViewController.cordux_swizzleMethod(#selector(UIViewController.viewDidLoad), 71 | swizzled: #selector(UIViewController.cordux_viewDidLoad)) 72 | 73 | UIViewController.cordux_swizzleMethod(#selector(UIViewController.viewWillAppear), 74 | swizzled: #selector(UIViewController.cordux_viewWillAppear)) 75 | UIViewController.cordux_swizzleMethod(#selector(UIViewController.viewDidAppear), 76 | swizzled: #selector(UIViewController.cordux_viewDidAppear)) 77 | UIViewController.cordux_swizzleMethod(#selector(UIViewController.viewWillDisappear), 78 | swizzled: #selector(UIViewController.cordux_viewWillDisappear)) 79 | UIViewController.cordux_swizzleMethod(#selector(UIViewController.viewDidDisappear), 80 | swizzled: #selector(UIViewController.cordux_viewDidDisappear)) 81 | 82 | #if swift(>=3) 83 | UIViewController.cordux_swizzleMethod(#selector(UIViewController.didMove), 84 | swizzled: #selector(UIViewController.cordux_didMoveToParentViewController(_:))) 85 | #else 86 | UIViewController.cordux_swizzleMethod(#selector(UIViewController.didMoveToParentViewController(_:)), 87 | swizzled: #selector(UIViewController.cordux_didMoveToParentViewController(_:))) 88 | #endif 89 | }() 90 | 91 | public class func swizzleLifecycleDelegatingViewControllerMethods() { 92 | _ = swizzle 93 | } 94 | 95 | @objc func cordux_viewDidLoad() { 96 | self.cordux_viewDidLoad() 97 | #if swift(>=3) 98 | self.corduxContext?.lifecycleDelegate.viewDidLoad(viewController: self) 99 | #else 100 | self.corduxContext?.lifecycleDelegate.viewDidLoad(self) 101 | #endif 102 | } 103 | 104 | @objc func cordux_viewWillAppear(_ animated: Bool) { 105 | self.cordux_viewWillAppear(animated) 106 | self.corduxContext?.lifecycleDelegate.viewWillAppear(animated, viewController: self) 107 | } 108 | 109 | @objc func cordux_viewDidAppear(_ animated: Bool) { 110 | self.cordux_viewDidAppear(animated) 111 | self.corduxContext?.lifecycleDelegate.viewDidAppear(animated, viewController: self) 112 | } 113 | 114 | @objc func cordux_viewWillDisappear(_ animated: Bool) { 115 | self.cordux_viewWillDisappear(animated) 116 | self.corduxContext?.lifecycleDelegate.viewWillDisappear(animated, viewController: self) 117 | } 118 | 119 | @objc func cordux_viewDidDisappear(_ animated: Bool) { 120 | self.cordux_viewDidDisappear(animated) 121 | self.corduxContext?.lifecycleDelegate.viewDidDisappear(animated, viewController: self) 122 | } 123 | 124 | @objc func cordux_didMoveToParentViewController(_ parentViewController: UIViewController?) { 125 | self.cordux_didMoveToParentViewController(parentViewController) 126 | 127 | #if swift(>=3) 128 | self.corduxContext?.lifecycleDelegate.didMove(toParentViewController: parentViewController, viewController: self) 129 | #else 130 | self.corduxContext?.lifecycleDelegate.didMove(parentViewController, viewController: self) 131 | #endif 132 | } 133 | 134 | static func cordux_swizzleMethod(_ original: Selector, swizzled: Selector) { 135 | guard let originalMethod = class_getInstanceMethod(self, original), 136 | let swizzledMethod = class_getInstanceMethod(self, swizzled) else { 137 | return 138 | } 139 | 140 | let didAddMethod = class_addMethod(self, original, method_getImplementation(swizzledMethod), method_getTypeEncoding(swizzledMethod)) 141 | 142 | if didAddMethod { 143 | class_replaceMethod(self, swizzled, method_getImplementation(originalMethod), method_getTypeEncoding(originalMethod)) 144 | } else { 145 | method_exchangeImplementations(originalMethod, swizzledMethod) 146 | } 147 | } 148 | } 149 | -------------------------------------------------------------------------------- /diagrams/Makefile: -------------------------------------------------------------------------------- 1 | DOTS=$(wildcard *.dot) 2 | 3 | .SUFFIXES: .dot .png 4 | 5 | .PHONY: all 6 | all: $(patsubst %.dot, %.png, $(DOTS)) 7 | 8 | .dot.png: $(DOTS) 9 | dot -Tpng $< > $@ 10 | 11 | .PHONY: clean 12 | clean: 13 | rm -f *.png 14 | -------------------------------------------------------------------------------- /diagrams/architecture.dot: -------------------------------------------------------------------------------- 1 | digraph { 2 | label="Cordux Flow" 3 | compound=true 4 | outputorder=edgesfirst 5 | 6 | graph [fontsize=10 fontname="Helvetica-Bold"] 7 | node [shape=rect style="rounded" fontsize=12 fontname="Helvetica-Light"] 8 | edge [fontsize=10 fontname="Helvetica"] 9 | 10 | 11 | subgraph cluster_app { 12 | label="App" 13 | 14 | store 15 | } 16 | 17 | subgraph cluster_scene { 18 | label="Scene" 19 | 20 | subgraph cluster_coordinator { 21 | label="Coordinator" 22 | coordinator [label="Coordinator"] 23 | view_handler [label="View Handler"] 24 | } 25 | 26 | subgraph cluster_view { 27 | label="View" 28 | 29 | view_store_query [label="Query"] 30 | view_controller [label="View Controller"] 31 | view [label="View"] 32 | } 33 | } 34 | 35 | 36 | subgraph cluster_services { 37 | label="Services" 38 | 39 | api 40 | db 41 | } 42 | 43 | subgraph data_flow { 44 | edge [color="blue"] 45 | store -> view_store_query 46 | view_store_query -> view_controller 47 | view_controller -> view_handler 48 | coordinator -> store [ltail=cluster_coordinator] 49 | } 50 | 51 | subgraph relationships { 52 | edge [style="dotted"] 53 | 54 | coordinator -> view_handler [label="conforms to"] 55 | coordinator -> view_store_query [label="instantiates"] 56 | coordinator -> view_controller [label="instantiates"] 57 | 58 | 59 | view_controller -> view [label="controls"] 60 | view -> view_controller 61 | } 62 | 63 | coordinator -> db [weight=10;lhead=cluster_services] 64 | db -> coordinator [ltail=cluster_services] 65 | 66 | subgraph layout { 67 | edge [style="invis"] 68 | 69 | store -> view_store_query 70 | coordinator -> view_controller [weight=10] 71 | coordinator -> view_handler [weight=100] 72 | } 73 | } 74 | -------------------------------------------------------------------------------- /yougot.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/willowtreeapps/cordux/7b9ac675e008f0e4a6e7b404dba5aebe363e0c48/yougot.jpg --------------------------------------------------------------------------------