From 3805c1c5db83bee77e0de961297603c80a006497 Mon Sep 17 00:00:00 2001 From: Jay Clark Date: Wed, 21 Mar 2018 14:30:46 -0400 Subject: [PATCH 1/5] Coordinator Cleanup --- .../app/PRODUCTNAME.xcodeproj/project.pbxproj | 20 ++-- .../PRODUCTNAME/Application/AppDelegate.swift | 7 +- .../Coordinators/AppCoordinator.swift | 87 ++++++++------ .../Coordinators/AuthCoordinator.swift | 110 ++++-------------- .../Coordinators/ContentCoordinator.swift | 31 ----- .../Coordinators/Coordinator.swift | 52 +++++---- .../Coordinators/HomeCoordinator.swift | 21 ++++ .../Coordinators/OnboardingCoordinator.swift | 51 +++----- .../Coordinators/SignInCoordinator.swift | 41 ------- .../Actionable+AutoConformance.swift | 28 +---- .../Resources/Generated/Localized.swift | 16 ++- .../PRODUCTNAME/Resources/Localizable.strings | 8 +- .../OnboardingSamplePageViewController.swift | 11 +- ...r.swift => OnboardingViewController.swift} | 45 ++++--- 14 files changed, 203 insertions(+), 325 deletions(-) delete mode 100644 PRODUCTNAME/app/PRODUCTNAME/Coordinators/ContentCoordinator.swift create mode 100644 PRODUCTNAME/app/PRODUCTNAME/Coordinators/HomeCoordinator.swift delete mode 100644 PRODUCTNAME/app/PRODUCTNAME/Coordinators/SignInCoordinator.swift rename PRODUCTNAME/app/PRODUCTNAME/Screens/Onboarding/{OnboardingPageViewController.swift => OnboardingViewController.swift} (84%) diff --git a/PRODUCTNAME/app/PRODUCTNAME.xcodeproj/project.pbxproj b/PRODUCTNAME/app/PRODUCTNAME.xcodeproj/project.pbxproj index e4d6119..61c9731 100644 --- a/PRODUCTNAME/app/PRODUCTNAME.xcodeproj/project.pbxproj +++ b/PRODUCTNAME/app/PRODUCTNAME.xcodeproj/project.pbxproj @@ -17,9 +17,8 @@ 2D75C6C71E8EEAE900F2EB1D /* OnboardingCoordinator.swift in Sources */ = {isa = PBXBuildFile; fileRef = 2DFF1CEC1E8944B900B1AD70 /* OnboardingCoordinator.swift */; }; 2D75C6C81E8EEB1900F2EB1D /* AuthCoordinator.swift in Sources */ = {isa = PBXBuildFile; fileRef = 2D4FA8F81E8577B5006C38ED /* AuthCoordinator.swift */; }; 2DAB59F81E95336100310ABF /* OnboardingSamplePageViewModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = 2DAB59F71E95336100310ABF /* OnboardingSamplePageViewModel.swift */; }; - 2DFF1CEF1E89493300B1AD70 /* SignInCoordinator.swift in Sources */ = {isa = PBXBuildFile; fileRef = 2DFF1CEE1E89493300B1AD70 /* SignInCoordinator.swift */; }; - 2DFF1CF11E8950F000B1AD70 /* ContentCoordinator.swift in Sources */ = {isa = PBXBuildFile; fileRef = 2DFF1CF01E8950F000B1AD70 /* ContentCoordinator.swift */; }; - 2DFF1D121E8BE27B00B1AD70 /* OnboardingPageViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 2DFF1D111E8BE27B00B1AD70 /* OnboardingPageViewController.swift */; }; + 2DFF1CF11E8950F000B1AD70 /* HomeCoordinator.swift in Sources */ = {isa = PBXBuildFile; fileRef = 2DFF1CF01E8950F000B1AD70 /* HomeCoordinator.swift */; }; + 2DFF1D121E8BE27B00B1AD70 /* OnboardingViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 2DFF1D111E8BE27B00B1AD70 /* OnboardingViewController.swift */; }; 2DFF1D161E8BF38F00B1AD70 /* UIColor+Extensions.swift in Sources */ = {isa = PBXBuildFile; fileRef = 2DFF1D151E8BF38F00B1AD70 /* UIColor+Extensions.swift */; }; 2DFF1D181E8BF82A00B1AD70 /* Colors.swift in Sources */ = {isa = PBXBuildFile; fileRef = 2DFF1D171E8BF82A00B1AD70 /* Colors.swift */; }; 2DFF1D1C1E8C424800B1AD70 /* OnboardingSamplePageViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 2DFF1D1B1E8C424800B1AD70 /* OnboardingSamplePageViewController.swift */; }; @@ -133,9 +132,8 @@ 2D4FA8F81E8577B5006C38ED /* AuthCoordinator.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = AuthCoordinator.swift; sourceTree = ""; }; 2DAB59F71E95336100310ABF /* OnboardingSamplePageViewModel.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = OnboardingSamplePageViewModel.swift; sourceTree = ""; }; 2DFF1CEC1E8944B900B1AD70 /* OnboardingCoordinator.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = OnboardingCoordinator.swift; sourceTree = ""; }; - 2DFF1CEE1E89493300B1AD70 /* SignInCoordinator.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = SignInCoordinator.swift; sourceTree = ""; }; - 2DFF1CF01E8950F000B1AD70 /* ContentCoordinator.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = ContentCoordinator.swift; sourceTree = ""; }; - 2DFF1D111E8BE27B00B1AD70 /* OnboardingPageViewController.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = OnboardingPageViewController.swift; sourceTree = ""; }; + 2DFF1CF01E8950F000B1AD70 /* HomeCoordinator.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = HomeCoordinator.swift; sourceTree = ""; }; + 2DFF1D111E8BE27B00B1AD70 /* OnboardingViewController.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = OnboardingViewController.swift; sourceTree = ""; }; 2DFF1D151E8BF38F00B1AD70 /* UIColor+Extensions.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = "UIColor+Extensions.swift"; sourceTree = ""; }; 2DFF1D171E8BF82A00B1AD70 /* Colors.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = Colors.swift; sourceTree = ""; }; 2DFF1D1B1E8C424800B1AD70 /* OnboardingSamplePageViewController.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = OnboardingSamplePageViewController.swift; sourceTree = ""; }; @@ -270,10 +268,9 @@ children = ( 2D4FA8F41E8574F9006C38ED /* AppCoordinator.swift */, 2D4FA8F81E8577B5006C38ED /* AuthCoordinator.swift */, - 2DFF1CF01E8950F000B1AD70 /* ContentCoordinator.swift */, + 2DFF1CF01E8950F000B1AD70 /* HomeCoordinator.swift */, 2D4FA8F61E85752B006C38ED /* Coordinator.swift */, 2DFF1CEC1E8944B900B1AD70 /* OnboardingCoordinator.swift */, - 2DFF1CEE1E89493300B1AD70 /* SignInCoordinator.swift */, ); path = Coordinators; sourceTree = ""; @@ -281,7 +278,7 @@ 2DFF1D131E8BE3D400B1AD70 /* Onboarding */ = { isa = PBXGroup; children = ( - 2DFF1D111E8BE27B00B1AD70 /* OnboardingPageViewController.swift */, + 2DFF1D111E8BE27B00B1AD70 /* OnboardingViewController.swift */, 2DFF1D1B1E8C424800B1AD70 /* OnboardingSamplePageViewController.swift */, 2DAB59F71E95336100310ABF /* OnboardingSamplePageViewModel.swift */, ); @@ -978,7 +975,7 @@ 2D75C6C81E8EEB1900F2EB1D /* AuthCoordinator.swift in Sources */, 2D75C6C71E8EEAE900F2EB1D /* OnboardingCoordinator.swift in Sources */, 2DFF1D1C1E8C424800B1AD70 /* OnboardingSamplePageViewController.swift in Sources */, - 2DFF1CF11E8950F000B1AD70 /* ContentCoordinator.swift in Sources */, + 2DFF1CF11E8950F000B1AD70 /* HomeCoordinator.swift in Sources */, ABF84F8D1DC99CFD002DB24D /* TextStyle.swift in Sources */, 8662C00A1FA2AA9900ADCBA9 /* SimpleTableCellItem.swift in Sources */, 861732F61FA2984600C14354 /* ViewRepresentable.swift in Sources */, @@ -986,7 +983,6 @@ BE4C21EB1E81970A00645143 /* DebugMenuConfiguration.swift in Sources */, 2D4FA8F71E85752B006C38ED /* Coordinator.swift in Sources */, 2D4FA8F51E8574F9006C38ED /* AppCoordinator.swift in Sources */, - 2DFF1CEF1E89493300B1AD70 /* SignInCoordinator.swift in Sources */, 2DAB59F81E95336100310ABF /* OnboardingSamplePageViewModel.swift in Sources */, 861732F41FA2984600C14354 /* TableCellItem.swift in Sources */, 861732F81FA2989100C14354 /* TableViewContainerCell.swift in Sources */, @@ -1017,7 +1013,7 @@ 2DFF1D181E8BF82A00B1AD70 /* Colors.swift in Sources */, 861732D51FA28DB600C14354 /* ModalDismissBehavior.swift in Sources */, BE28092A1E802893006E50D5 /* Analytics.swift in Sources */, - 2DFF1D121E8BE27B00B1AD70 /* OnboardingPageViewController.swift in Sources */, + 2DFF1D121E8BE27B00B1AD70 /* OnboardingViewController.swift in Sources */, ); runOnlyForDeploymentPostprocessing = 0; }; diff --git a/PRODUCTNAME/app/PRODUCTNAME/Application/AppDelegate.swift b/PRODUCTNAME/app/PRODUCTNAME/Application/AppDelegate.swift index 4b1e3d5..614e103 100644 --- a/PRODUCTNAME/app/PRODUCTNAME/Application/AppDelegate.swift +++ b/PRODUCTNAME/app/PRODUCTNAME/Application/AppDelegate.swift @@ -41,13 +41,14 @@ class AppDelegate: UIResponder, UIApplicationDelegate { let window = UIWindow(frame: UIScreen.main.bounds) self.window = window - self.coordinator = AppCoordinator(window: window) - coordinator.start(animated: true, completion: { + coordinator.start { rootViewController in + window.rootViewController = rootViewController + window.makeKeyAndVisible() for config in self.rootViewControllerDependentConfigurations where config.isEnabled { config.onDidLaunch(application: application, launchOptions: launchOptions) } - }) + } return true } diff --git a/PRODUCTNAME/app/PRODUCTNAME/Coordinators/AppCoordinator.swift b/PRODUCTNAME/app/PRODUCTNAME/Coordinators/AppCoordinator.swift index d69dbe5..8c13db4 100644 --- a/PRODUCTNAME/app/PRODUCTNAME/Coordinators/AppCoordinator.swift +++ b/PRODUCTNAME/app/PRODUCTNAME/Coordinators/AppCoordinator.swift @@ -7,63 +7,78 @@ // import UIKit -import Services class AppCoordinator: Coordinator { private let window: UIWindow - fileprivate let rootController: UIViewController - var childCoordinator: Coordinator? init(window: UIWindow) { self.window = window - let rootController = UIViewController() - rootController.view.backgroundColor = .white - self.rootController = rootController } - func start(animated: Bool, completion: VoidClosure?) { - // Configure window/root view controller - window.setRootViewController(rootController, animated: false, completion: { - self.window.makeKeyAndVisible() + func start(with presentation: (UIViewController) -> Void) { + guard OnboardingCoordinator.hasOnboarded else { + let onboardingCoordinator = OnboardingCoordinator() + onboardingCoordinator.delegate = self + attach(to: onboardingCoordinator) + onboardingCoordinator.start(with: presentation) + return + } - // Spin off auth coordinator - let authCoordinator = AuthCoordinator(self.rootController) + let authCoordinator = AuthCoordinator() + guard authCoordinator.isAuthenticated else { authCoordinator.delegate = self - self.childCoordinator = authCoordinator - authCoordinator.start(animated: animated, completion: completion) - }) - } + attach(to: authCoordinator) + authCoordinator.startSignIn(with: presentation) + return + } - func cleanup(animated: Bool, completion: VoidClosure?) { - completion?() + let homeCoordinator = HomeCoordinator() + attach(to: homeCoordinator) + homeCoordinator.start(with: presentation) } } -extension AppCoordinator: AuthCoordinatorDelegate { +extension AppCoordinator: OnboardingCoordinator.Delegate { - func authCoordinator(_ coordinator: AuthCoordinator, didNotify action: AuthCoordinator.Action) { + func onboardingCoordinator(_ coordinator: OnboardingCoordinator, didNotify action: OnboardingCoordinator.Action) { switch action { - case .didSignIn: - guard let authCoordinator = childCoordinator as? AuthCoordinator else { - preconditionFailure("Upon signing in, AppCoordinator should have an AuthCoordinator as a child.") + case .skip: + let homeCoordinator = HomeCoordinator() + attach(to: homeCoordinator) + homeCoordinator.start { + window.setRootViewController($0, animated: true) } - childCoordinator = nil - authCoordinator.cleanup(animated: true) { - let contentCoordinator = ContentCoordinator(self.rootController) - self.childCoordinator = contentCoordinator - contentCoordinator.start(animated: true, completion: nil) + + case .join: + let authCoordinator = AuthCoordinator() + attach(to: authCoordinator) + authCoordinator.startSignUp { + window.rootViewController?.present($0, animated: true, completion: nil) } - case .didSkipAuth: - guard let authCoordinator = childCoordinator as? AuthCoordinator else { - preconditionFailure("Upon signing in, AppCoordinator should have an AuthCoordinator as a child.") + + case .signIn: + let authCoordinator = AuthCoordinator() + attach(to: authCoordinator) + authCoordinator.startSignIn { + window.rootViewController?.present($0, animated: true, completion: nil) } - childCoordinator = nil - authCoordinator.cleanup(animated: false) { - let contentCoordinator = ContentCoordinator(self.rootController) - self.childCoordinator = contentCoordinator - contentCoordinator.start(animated: true, completion: nil) + + } + } + +} + +extension AppCoordinator: AuthCoordinator.Delegate { + + func authCoordinator(_ coordinator: AuthCoordinator, didNotify action: AuthCoordinator.Action) { + switch action { + case .signedIn: + let homeCoordinator = HomeCoordinator() + attach(to: homeCoordinator) + homeCoordinator.start { + window.setRootViewController($0, animated: true) } } } diff --git a/PRODUCTNAME/app/PRODUCTNAME/Coordinators/AuthCoordinator.swift b/PRODUCTNAME/app/PRODUCTNAME/Coordinators/AuthCoordinator.swift index 8136cad..1c39ea1 100644 --- a/PRODUCTNAME/app/PRODUCTNAME/Coordinators/AuthCoordinator.swift +++ b/PRODUCTNAME/app/PRODUCTNAME/Coordinators/AuthCoordinator.swift @@ -7,58 +7,36 @@ // import UIKit -import Services - -private enum State { - - case authenticated - case onboarded - case needsOnboarding - -} class AuthCoordinator: Coordinator { - var childCoordinator: Coordinator? - let baseController: UIViewController weak var delegate: Delegate? - private let client = APIClient.shared - private var state: State { - if client.oauthClient.isAuthenticated { - return .authenticated - } - else if UserDefaults.hasOnboarded { - return .onboarded - } - else { - return .needsOnboarding - } + var isAuthenticated: Bool { + // TODO: inject and check from services + return false } - init(_ baseController: UIViewController) { - self.baseController = baseController - } + func startSignIn(with presentation: (UIViewController) -> Void) { + let nav = UINavigationController() + attach(to: nav) - func start(animated: Bool, completion: VoidClosure?) { - switch state { - case .authenticated: - notify(.didSignIn) - case .onboarded: - let signInCoordinator = SignInCoordinator(baseController) - signInCoordinator.delegate = self - signInCoordinator.start(animated: animated, completion: completion) - childCoordinator = signInCoordinator - case .needsOnboarding: - let onboardCoordinator = OnboardingCoordinator(baseController) - onboardCoordinator.delegate = self - onboardCoordinator.start(animated: animated, completion: completion) - childCoordinator = onboardCoordinator - } + let signInViewController = UIViewController() + signInViewController.view.backgroundColor = .yellow + signInViewController.title = L10n.Signin.title + nav.viewControllers = [signInViewController] + presentation(nav) } - func cleanup(animated: Bool, completion: VoidClosure?) { - childCoordinator?.cleanup(animated: animated, completion: completion) + func startSignUp(with presentation: (UIViewController) -> Void) { + let nav = UINavigationController() + attach(to: nav) + + let signUpViewController = UIViewController() + signUpViewController.view.backgroundColor = .orange + signUpViewController.title = L10n.Signup.title + nav.viewControllers = [signUpViewController] + presentation(nav) } } @@ -66,53 +44,7 @@ class AuthCoordinator: Coordinator { extension AuthCoordinator: Actionable { enum Action { - case didSignIn - case didSkipAuth - } - -} - -extension AuthCoordinator: SignInCoordinatorDelegate { - - func signInCoordinator(_ coordinator: SignInCoordinator, didNotify action: SignInCoordinator.Action) { - switch action { - case .didSignIn: - notify(.didSignIn) - } - } - -} - -extension AuthCoordinator: OnboardingCoordinatorDelegate { - - func onboardingCoordinator(_ coordinator: OnboardingCoordinator, didNotify action: OnboardingCoordinator.Action) { - switch action { - case .didSkipAuth: - notify(.didSkipAuth) - case .didRequestJoin: - guard let onboardCoordinator = childCoordinator as? OnboardingCoordinator else { - preconditionFailure("Upon signing in, AppCoordinator should have an AuthCoordinator as a child.") - } - childCoordinator = nil - onboardCoordinator.cleanup(animated: true) { - let signInCoordinator = SignInCoordinator(self.baseController) - signInCoordinator.delegate = self - self.childCoordinator = signInCoordinator - // TODO - signInCoordinator move from signIn to register here - signInCoordinator.start(animated: true, completion: nil) - } - case .didRequestSignIn: - guard let onboardCoordinator = childCoordinator as? OnboardingCoordinator else { - preconditionFailure("Upon signing in, AppCoordinator should have an AuthCoordinator as a child.") - } - childCoordinator = nil - onboardCoordinator.cleanup(animated: true) { - let signInCoordinator = SignInCoordinator(self.baseController) - signInCoordinator.delegate = self - self.childCoordinator = signInCoordinator - signInCoordinator.start(animated: true, completion: nil) - } - } + case signedIn } } diff --git a/PRODUCTNAME/app/PRODUCTNAME/Coordinators/ContentCoordinator.swift b/PRODUCTNAME/app/PRODUCTNAME/Coordinators/ContentCoordinator.swift deleted file mode 100644 index 214e57c..0000000 --- a/PRODUCTNAME/app/PRODUCTNAME/Coordinators/ContentCoordinator.swift +++ /dev/null @@ -1,31 +0,0 @@ -// -// ContentCoordinator.swift -// PRODUCTNAME -// -// Created by LEADDEVELOPER on 3/27/17. -// Copyright © 2017 ORGANIZATION. All rights reserved. -// - -import UIKit -import Services - -class ContentCoordinator: Coordinator { - - let baseController: UIViewController - var childCoordinator: Coordinator? - - init(_ baseController: UIViewController) { - self.baseController = baseController - } - - func start(animated: Bool, completion: VoidClosure?) { - let vc = ContentTabBarViewController() - vc.modalTransitionStyle = .crossDissolve - baseController.present(vc, animated: animated, completion: completion) - } - - func cleanup(animated: Bool, completion: VoidClosure?) { - baseController.dismiss(animated: animated, completion: completion) - } - -} diff --git a/PRODUCTNAME/app/PRODUCTNAME/Coordinators/Coordinator.swift b/PRODUCTNAME/app/PRODUCTNAME/Coordinators/Coordinator.swift index 63346dc..5fcfd0a 100644 --- a/PRODUCTNAME/app/PRODUCTNAME/Coordinators/Coordinator.swift +++ b/PRODUCTNAME/app/PRODUCTNAME/Coordinators/Coordinator.swift @@ -6,30 +6,32 @@ // Copyright © 2017 ORGANIZATION. All rights reserved. // -import UIKit -import Services - -protocol Coordinator { - - /// A child coordinator spun off by this coordinator. - /// Important to keep a reference to prevent deallocation. - var childCoordinator: Coordinator? { get set } - - /// Start any action this coordinator should take. Often, this is - /// presenting/pushing a new controller, or starting up a - /// child coordinator. - /// - /// - Parameters: - /// - animated: whether to animate any transitions. - /// - completion: a completion block. - func start(animated: Bool, completion: VoidClosure?) - - /// Clean up after this coordinator. Should get the app back to the - /// state it was in when this coordinator started. - /// - /// - Parameters: - /// - animated: whether to animate any transitions. - /// - completion: a completion block. - func cleanup(animated: Bool, completion: VoidClosure?) +import ObjectiveC.runtime + +protocol CoordinatorProtocol: class, NSObjectProtocol { + func attach(to child: AnyObject) + func detatch(from child: AnyObject) +} + +class Coordinator: NSObject, CoordinatorProtocol {} + +extension Coordinator { + + private static var key = 0 + + func attach(to child: AnyObject) { + var attached = objc_getAssociatedObject(child, &Coordinator.key) as? NSMutableArray + + if attached == nil { + attached = NSMutableArray() + objc_setAssociatedObject(child, &Coordinator.key, attached, .OBJC_ASSOCIATION_RETAIN) + } + + attached?.add(self) + } + + func detatch(from child: AnyObject) { + (objc_getAssociatedObject(child, &Coordinator.key) as? NSMutableArray)?.remove(self) + } } diff --git a/PRODUCTNAME/app/PRODUCTNAME/Coordinators/HomeCoordinator.swift b/PRODUCTNAME/app/PRODUCTNAME/Coordinators/HomeCoordinator.swift new file mode 100644 index 0000000..c6cba86 --- /dev/null +++ b/PRODUCTNAME/app/PRODUCTNAME/Coordinators/HomeCoordinator.swift @@ -0,0 +1,21 @@ +// +// HomeCoordinator.swift +// PRODUCTNAME +// +// Created by LEADDEVELOPER on 3/27/17. +// Copyright © 2017 ORGANIZATION. All rights reserved. +// + +class HomeCoordinator: Coordinator { + + func start(with presentation: (UIViewController) -> Void) { + let nav = UINavigationController() + attach(to: nav) + let homeVC = UIViewController() + homeVC.view.backgroundColor = .red + homeVC.title = L10n.Home.title + nav.viewControllers = [homeVC] + presentation(nav) + } + +} diff --git a/PRODUCTNAME/app/PRODUCTNAME/Coordinators/OnboardingCoordinator.swift b/PRODUCTNAME/app/PRODUCTNAME/Coordinators/OnboardingCoordinator.swift index 6c30b4e..0b06249 100644 --- a/PRODUCTNAME/app/PRODUCTNAME/Coordinators/OnboardingCoordinator.swift +++ b/PRODUCTNAME/app/PRODUCTNAME/Coordinators/OnboardingCoordinator.swift @@ -7,27 +7,20 @@ // import UIKit -import Services class OnboardingCoordinator: Coordinator { - let baseController: UIViewController - var childCoordinator: Coordinator? weak var delegate: Delegate? - init(_ baseController: UIViewController) { - self.baseController = baseController + static var hasOnboarded: Bool { + return UserDefaults.hasOnboarded } - func start(animated: Bool, completion: VoidClosure?) { - let vc = OnboardingPageViewController( - viewModels: OnboardingCoordinator.pageViewModels) + func start(with presentation: (UIViewController) -> Void) { + let vc = OnboardingViewController() vc.delegate = self - baseController.present(vc, animated: animated, completion: completion) - } - - func cleanup(animated: Bool, completion: VoidClosure?) { - baseController.dismiss(animated: animated, completion: completion) + attach(to: vc) + presentation(vc) } } @@ -35,37 +28,21 @@ class OnboardingCoordinator: Coordinator { extension OnboardingCoordinator: Actionable { enum Action { - case didSkipAuth - case didRequestJoin - case didRequestSignIn + case skip + case join + case signIn } } -extension OnboardingCoordinator: OnboardingPageViewControllerDelegate { +extension OnboardingCoordinator: OnboardingViewController.Delegate { - func onboardingPageViewController(_ vc: OnboardingPageViewController, didNotify action: OnboardingPageViewController.Action) { + func onboardingViewController(_ vc: OnboardingViewController, didNotify action: OnboardingViewController.Action) { switch action { - case .skipTapped: - notify(.didSkipAuth) - case .joinTapped: - notify(.didRequestJoin) - case .signInTapped: - notify(.didRequestSignIn) + case .skip: notify(.skip) + case .join: notify(.join) + case .signIn: notify(.signIn) } } } - -extension OnboardingCoordinator { - - static var pageViewModels: [OnboardingSamplePageViewModel] { - let samplePage = OnboardingSamplePageViewModel( - header: L10n.Onboarding.Pages.Sample.heading, - body: L10n.Onboarding.Pages.Sample.body, - asset: Asset.logoKennyLoggins - ) - return [samplePage, samplePage, samplePage] - } - -} diff --git a/PRODUCTNAME/app/PRODUCTNAME/Coordinators/SignInCoordinator.swift b/PRODUCTNAME/app/PRODUCTNAME/Coordinators/SignInCoordinator.swift deleted file mode 100644 index d184c03..0000000 --- a/PRODUCTNAME/app/PRODUCTNAME/Coordinators/SignInCoordinator.swift +++ /dev/null @@ -1,41 +0,0 @@ -// -// SignInCoordinator.swift -// PRODUCTNAME -// -// Created by LEADDEVELOPER on 3/27/17. -// Copyright © 2017 ORGANIZATION. All rights reserved. -// - -import UIKit -import Services - -class SignInCoordinator: Coordinator { - - let baseController: UIViewController - var childCoordinator: Coordinator? - weak var delegate: Delegate? - - init(_ baseController: UIViewController) { - self.baseController = baseController - } - - func start(animated: Bool, completion: VoidClosure?) { - // TODO - create and use SignInViewController - let vc = UIViewController() - vc.view.backgroundColor = .red - self.baseController.present(vc, animated: animated, completion: completion) - } - - func cleanup(animated: Bool, completion: VoidClosure?) { - baseController.dismiss(animated: animated, completion: completion) - } - -} - -extension SignInCoordinator: Actionable { - - enum Action { - case didSignIn - } - -} diff --git a/PRODUCTNAME/app/PRODUCTNAME/Resources/Generated/Actionable+AutoConformance.swift b/PRODUCTNAME/app/PRODUCTNAME/Resources/Generated/Actionable+AutoConformance.swift index 38aa207..306eef5 100644 --- a/PRODUCTNAME/app/PRODUCTNAME/Resources/Generated/Actionable+AutoConformance.swift +++ b/PRODUCTNAME/app/PRODUCTNAME/Resources/Generated/Actionable+AutoConformance.swift @@ -37,34 +37,18 @@ extension OnboardingCoordinator { } -// MARK: - OnboardingPageViewController -protocol OnboardingPageViewControllerDelegate: class { - func onboardingPageViewController(_ vc: OnboardingPageViewController, didNotify action: OnboardingPageViewController.Action) +// MARK: - OnboardingViewController +protocol OnboardingViewControllerDelegate: class { + func onboardingViewController(_ vc: OnboardingViewController, didNotify action: OnboardingViewController.Action) } -extension OnboardingPageViewController { +extension OnboardingViewController { typealias ActionType = Action - typealias Delegate = OnboardingPageViewControllerDelegate + typealias Delegate = OnboardingViewControllerDelegate func notify(_ action: ActionType) { - delegate?.onboardingPageViewController(self, didNotify: action) - } - -} - -// MARK: - SignInCoordinator -protocol SignInCoordinatorDelegate: class { - func signInCoordinator(_ coordinator: SignInCoordinator, didNotify action: SignInCoordinator.Action) -} - -extension SignInCoordinator { - - typealias ActionType = Action - typealias Delegate = SignInCoordinatorDelegate - - func notify(_ action: ActionType) { - delegate?.signInCoordinator(self, didNotify: action) + delegate?.onboardingViewController(self, didNotify: action) } } diff --git a/PRODUCTNAME/app/PRODUCTNAME/Resources/Generated/Localized.swift b/PRODUCTNAME/app/PRODUCTNAME/Resources/Generated/Localized.swift index f861bca..eba9a4b 100644 --- a/PRODUCTNAME/app/PRODUCTNAME/Resources/Generated/Localized.swift +++ b/PRODUCTNAME/app/PRODUCTNAME/Resources/Generated/Localized.swift @@ -7,6 +7,11 @@ import Foundation // swiftlint:disable explicit_type_interface identifier_name line_length nesting type_body_length type_name enum L10n { + enum Home { + /// PRODUCTNAME + static let title = L10n.tr("Localizable", "Home.Title") + } + enum Onboarding { enum Buttons { @@ -29,9 +34,14 @@ enum L10n { } } - enum Title { - /// PRODUCTNAME - static let navigation = L10n.tr("Localizable", "Title.Navigation") + enum Signin { + /// Sign In + static let title = L10n.tr("Localizable", "SignIn.Title") + } + + enum Signup { + /// Sign Up + static let title = L10n.tr("Localizable", "SignUp.Title") } } // swiftlint:enable explicit_type_interface identifier_name line_length nesting type_body_length type_name diff --git a/PRODUCTNAME/app/PRODUCTNAME/Resources/Localizable.strings b/PRODUCTNAME/app/PRODUCTNAME/Resources/Localizable.strings index 33c7b0f..f09f2db 100644 --- a/PRODUCTNAME/app/PRODUCTNAME/Resources/Localizable.strings +++ b/PRODUCTNAME/app/PRODUCTNAME/Resources/Localizable.strings @@ -1,7 +1,11 @@ -"Title.Navigation" = "PRODUCTNAME"; - "Onboarding.Buttons.Skip" = "Skip"; "Onboarding.Buttons.Join" = "Join"; "Onboarding.Buttons.SignIn" = "Already have an account? Sign in."; "Onboarding.Pages.Sample.Heading" = "HEADING TEXT"; "Onboarding.Pages.Sample.Body" = "This is body copy for the onboarding and should be replaced with real text!"; + +"Home.Title" = "PRODUCTNAME"; + +"SignIn.Title" = "Sign In"; + +"SignUp.Title" = "Sign Up"; diff --git a/PRODUCTNAME/app/PRODUCTNAME/Screens/Onboarding/OnboardingSamplePageViewController.swift b/PRODUCTNAME/app/PRODUCTNAME/Screens/Onboarding/OnboardingSamplePageViewController.swift index 410c4bc..ab3540b 100644 --- a/PRODUCTNAME/app/PRODUCTNAME/Screens/Onboarding/OnboardingSamplePageViewController.swift +++ b/PRODUCTNAME/app/PRODUCTNAME/Screens/Onboarding/OnboardingSamplePageViewController.swift @@ -10,9 +10,9 @@ import Anchorage class OnboardingSamplePageViewController: UIViewController { - fileprivate let imageView = UIImageView() + let imageView = UIImageView() - fileprivate let headerLabel: UILabel = { + let headerLabel: UILabel = { let label = UILabel() label.font = UIFont.systemFont(ofSize: 28) label.textColor = Colors.darkGray @@ -20,7 +20,7 @@ class OnboardingSamplePageViewController: UIViewController { return label }() - fileprivate let bodyLabel: UILabel = { + let bodyLabel: UILabel = { let label = UILabel() label.font = UIFont.systemFont(ofSize: 20) label.textColor = Colors.darkGray @@ -29,11 +29,8 @@ class OnboardingSamplePageViewController: UIViewController { return label }() - init(viewModel: OnboardingSamplePageViewModel) { + init() { super.init(nibName: nil, bundle: nil) - imageView.image = viewModel.asset?.image - headerLabel.text = viewModel.header - bodyLabel.text = viewModel.body } @available(*, unavailable) required init?(coder aDecoder: NSCoder) { diff --git a/PRODUCTNAME/app/PRODUCTNAME/Screens/Onboarding/OnboardingPageViewController.swift b/PRODUCTNAME/app/PRODUCTNAME/Screens/Onboarding/OnboardingViewController.swift similarity index 84% rename from PRODUCTNAME/app/PRODUCTNAME/Screens/Onboarding/OnboardingPageViewController.swift rename to PRODUCTNAME/app/PRODUCTNAME/Screens/Onboarding/OnboardingViewController.swift index b7dda21..e0a821d 100644 --- a/PRODUCTNAME/app/PRODUCTNAME/Screens/Onboarding/OnboardingPageViewController.swift +++ b/PRODUCTNAME/app/PRODUCTNAME/Screens/Onboarding/OnboardingViewController.swift @@ -1,5 +1,5 @@ // -// OnboardingPageViewController.swift +// OnboardingViewController.swift // PRODUCTNAME // // Created by LEADDEVELOPER on 3/29/17. @@ -9,8 +9,8 @@ import Anchorage import Swiftilities -// MARK: OnboardingPageViewController -class OnboardingPageViewController: UIViewController { +// MARK: OnboardingViewController +class OnboardingViewController: UIViewController { fileprivate let viewControllers: [UIViewController] @@ -44,12 +44,23 @@ class OnboardingPageViewController: UIViewController { }() weak var delegate: Delegate? - init(viewModels: [OnboardingSamplePageViewModel]) { + init() { + let samplePage = OnboardingSamplePageViewModel( + header: L10n.Onboarding.Pages.Sample.heading, + body: L10n.Onboarding.Pages.Sample.body, + asset: Asset.logoKennyLoggins + ) + let viewModels = [samplePage, samplePage, samplePage] + self.viewControllers = viewModels.map { - OnboardingSamplePageViewController(viewModel: $0) + let vc = OnboardingSamplePageViewController() + vc.imageView.image = $0.asset?.image + vc.headerLabel.styledText = $0.header + vc.bodyLabel.styledText = $0.body + return vc } - super.init(nibName: nil, bundle: nil) + super.init(nibName: nil, bundle: nil) } @available(*, unavailable) required init?(coder aDecoder: NSCoder) { @@ -65,18 +76,18 @@ class OnboardingPageViewController: UIViewController { } // MARK: Actionable -extension OnboardingPageViewController: Actionable { +extension OnboardingViewController: Actionable { enum Action { - case skipTapped - case joinTapped - case signInTapped + case skip + case join + case signIn } } // MARK: Private -private extension OnboardingPageViewController { +private extension OnboardingViewController { func configureView() { view.backgroundColor = .white @@ -91,7 +102,7 @@ private extension OnboardingPageViewController { pageController.didMove(toParentViewController: self) let pageControlAppearance = UIPageControl.appearance( - whenContainedInInstancesOf: [OnboardingPageViewController.self]) + whenContainedInInstancesOf: [OnboardingViewController.self]) pageControlAppearance.pageIndicatorTintColor = Colors.lightGray pageControlAppearance.currentPageIndicatorTintColor = Colors.darkGray @@ -135,23 +146,23 @@ private extension OnboardingPageViewController { } // MARK: Actions -private extension OnboardingPageViewController { +private extension OnboardingViewController { @objc func skipTapped() { - notify(.skipTapped) + notify(.skip) } @objc func joinTapped() { - notify(.joinTapped) + notify(.join) } @objc func signInTapped() { - notify(.signInTapped) + notify(.signIn) } } // MARK: UIPageViewControllerDataSource -extension OnboardingPageViewController: UIPageViewControllerDataSource { +extension OnboardingViewController: UIPageViewControllerDataSource { func pageViewController(_ pageViewController: UIPageViewController, viewControllerBefore viewController: UIViewController) -> UIViewController? { guard let index = viewControllers.index(of: viewController), index > 0 else { From 8c57c63ffdc525eb5866ddce4df81bf30cba98e7 Mon Sep 17 00:00:00 2001 From: Jay Clark Date: Wed, 21 Mar 2018 14:38:32 -0400 Subject: [PATCH 2/5] Remove `CoordinatorProtocol` --- PRODUCTNAME/app/PRODUCTNAME/Coordinators/Coordinator.swift | 7 +------ 1 file changed, 1 insertion(+), 6 deletions(-) diff --git a/PRODUCTNAME/app/PRODUCTNAME/Coordinators/Coordinator.swift b/PRODUCTNAME/app/PRODUCTNAME/Coordinators/Coordinator.swift index 5fcfd0a..836c271 100644 --- a/PRODUCTNAME/app/PRODUCTNAME/Coordinators/Coordinator.swift +++ b/PRODUCTNAME/app/PRODUCTNAME/Coordinators/Coordinator.swift @@ -8,12 +8,7 @@ import ObjectiveC.runtime -protocol CoordinatorProtocol: class, NSObjectProtocol { - func attach(to child: AnyObject) - func detatch(from child: AnyObject) -} - -class Coordinator: NSObject, CoordinatorProtocol {} +class Coordinator: NSObject {} extension Coordinator { From 51509fb948bee9d26f9bf4df81ff1b3a5e7b6a19 Mon Sep 17 00:00:00 2001 From: Jay Clark Date: Wed, 21 Mar 2018 14:51:16 -0400 Subject: [PATCH 3/5] update readme --- PRODUCTNAME/README.md | 3 +++ 1 file changed, 3 insertions(+) diff --git a/PRODUCTNAME/README.md b/PRODUCTNAME/README.md index 0e4f214..0dd0797 100644 --- a/PRODUCTNAME/README.md +++ b/PRODUCTNAME/README.md @@ -21,6 +21,9 @@ To get started, see [Contributing](#contributing) - Coordinators should: - Manage view controller transitions + - Deinit when the views they manage Deinit + - `attach()` to the lifecycle of their children + - **NOT** hold strong references to their children - View Controllers should: - Delegate 'final' actions to a Coordinator - **Not** access the navigation controller or present view controllers From bc90d6dd948e44f1ef14e7ae4e9f791212fe8f79 Mon Sep 17 00:00:00 2001 From: Jay Clark Date: Wed, 21 Mar 2018 14:52:50 -0400 Subject: [PATCH 4/5] generate CookieCutter Template --- .../README.md | 3 + .../project.pbxproj | 20 +- .../Application/AppDelegate.swift | 7 +- .../Coordinators/AppCoordinator.swift | 87 ++++---- .../Coordinators/AuthCoordinator.swift | 110 ++-------- .../Coordinators/Coordinator.swift | 47 ++--- .../Coordinators/HomeCoordinator.swift | 21 ++ .../Coordinators/OnboardingCoordinator.swift | 51 ++--- .../Actionable+AutoConformance.swift | 28 +-- .../Resources/Generated/Localized.swift | 16 +- .../Resources/Localizable.strings | 8 +- .../OnboardingSamplePageViewController.swift | 11 +- .../Onboarding/OnboardingViewController.swift | 193 ++++++++++++++++++ 13 files changed, 366 insertions(+), 236 deletions(-) create mode 100644 {{ cookiecutter.project_name | replace(' ', '') }}/app/{{ cookiecutter.project_name | replace(' ', '') }}/Coordinators/HomeCoordinator.swift create mode 100644 {{ cookiecutter.project_name | replace(' ', '') }}/app/{{ cookiecutter.project_name | replace(' ', '') }}/Screens/Onboarding/OnboardingViewController.swift diff --git a/{{ cookiecutter.project_name | replace(' ', '') }}/README.md b/{{ cookiecutter.project_name | replace(' ', '') }}/README.md index 0e4f214..0dd0797 100644 --- a/{{ cookiecutter.project_name | replace(' ', '') }}/README.md +++ b/{{ cookiecutter.project_name | replace(' ', '') }}/README.md @@ -21,6 +21,9 @@ To get started, see [Contributing](#contributing) - Coordinators should: - Manage view controller transitions + - Deinit when the views they manage Deinit + - `attach()` to the lifecycle of their children + - **NOT** hold strong references to their children - View Controllers should: - Delegate 'final' actions to a Coordinator - **Not** access the navigation controller or present view controllers diff --git a/{{ cookiecutter.project_name | replace(' ', '') }}/app/{{ cookiecutter.project_name | replace(' ', '') }}.xcodeproj/project.pbxproj b/{{ cookiecutter.project_name | replace(' ', '') }}/app/{{ cookiecutter.project_name | replace(' ', '') }}.xcodeproj/project.pbxproj index 7d9b553..2fd809a 100644 --- a/{{ cookiecutter.project_name | replace(' ', '') }}/app/{{ cookiecutter.project_name | replace(' ', '') }}.xcodeproj/project.pbxproj +++ b/{{ cookiecutter.project_name | replace(' ', '') }}/app/{{ cookiecutter.project_name | replace(' ', '') }}.xcodeproj/project.pbxproj @@ -18,9 +18,8 @@ 2D75C6C71E8EEAE900F2EB1D /* OnboardingCoordinator.swift in Sources */ = {isa = PBXBuildFile; fileRef = 2DFF1CEC1E8944B900B1AD70 /* OnboardingCoordinator.swift */; }; 2D75C6C81E8EEB1900F2EB1D /* AuthCoordinator.swift in Sources */ = {isa = PBXBuildFile; fileRef = 2D4FA8F81E8577B5006C38ED /* AuthCoordinator.swift */; }; 2DAB59F81E95336100310ABF /* OnboardingSamplePageViewModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = 2DAB59F71E95336100310ABF /* OnboardingSamplePageViewModel.swift */; }; - 2DFF1CEF1E89493300B1AD70 /* SignInCoordinator.swift in Sources */ = {isa = PBXBuildFile; fileRef = 2DFF1CEE1E89493300B1AD70 /* SignInCoordinator.swift */; }; - 2DFF1CF11E8950F000B1AD70 /* ContentCoordinator.swift in Sources */ = {isa = PBXBuildFile; fileRef = 2DFF1CF01E8950F000B1AD70 /* ContentCoordinator.swift */; }; - 2DFF1D121E8BE27B00B1AD70 /* OnboardingPageViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 2DFF1D111E8BE27B00B1AD70 /* OnboardingPageViewController.swift */; }; + 2DFF1CF11E8950F000B1AD70 /* HomeCoordinator.swift in Sources */ = {isa = PBXBuildFile; fileRef = 2DFF1CF01E8950F000B1AD70 /* HomeCoordinator.swift */; }; + 2DFF1D121E8BE27B00B1AD70 /* OnboardingViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 2DFF1D111E8BE27B00B1AD70 /* OnboardingViewController.swift */; }; 2DFF1D161E8BF38F00B1AD70 /* UIColor+Extensions.swift in Sources */ = {isa = PBXBuildFile; fileRef = 2DFF1D151E8BF38F00B1AD70 /* UIColor+Extensions.swift */; }; 2DFF1D181E8BF82A00B1AD70 /* Colors.swift in Sources */ = {isa = PBXBuildFile; fileRef = 2DFF1D171E8BF82A00B1AD70 /* Colors.swift */; }; 2DFF1D1C1E8C424800B1AD70 /* OnboardingSamplePageViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 2DFF1D1B1E8C424800B1AD70 /* OnboardingSamplePageViewController.swift */; }; @@ -135,9 +134,8 @@ 2D4FA8F81E8577B5006C38ED /* AuthCoordinator.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = AuthCoordinator.swift; sourceTree = ""; }; 2DAB59F71E95336100310ABF /* OnboardingSamplePageViewModel.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = OnboardingSamplePageViewModel.swift; sourceTree = ""; }; 2DFF1CEC1E8944B900B1AD70 /* OnboardingCoordinator.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = OnboardingCoordinator.swift; sourceTree = ""; }; - 2DFF1CEE1E89493300B1AD70 /* SignInCoordinator.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = SignInCoordinator.swift; sourceTree = ""; }; - 2DFF1CF01E8950F000B1AD70 /* ContentCoordinator.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = ContentCoordinator.swift; sourceTree = ""; }; - 2DFF1D111E8BE27B00B1AD70 /* OnboardingPageViewController.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = OnboardingPageViewController.swift; sourceTree = ""; }; + 2DFF1CF01E8950F000B1AD70 /* HomeCoordinator.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = HomeCoordinator.swift; sourceTree = ""; }; + 2DFF1D111E8BE27B00B1AD70 /* OnboardingViewController.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = OnboardingViewController.swift; sourceTree = ""; }; 2DFF1D151E8BF38F00B1AD70 /* UIColor+Extensions.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = "UIColor+Extensions.swift"; sourceTree = ""; }; 2DFF1D171E8BF82A00B1AD70 /* Colors.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = Colors.swift; sourceTree = ""; }; 2DFF1D1B1E8C424800B1AD70 /* OnboardingSamplePageViewController.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = OnboardingSamplePageViewController.swift; sourceTree = ""; }; @@ -272,10 +270,9 @@ children = ( 2D4FA8F41E8574F9006C38ED /* AppCoordinator.swift */, 2D4FA8F81E8577B5006C38ED /* AuthCoordinator.swift */, - 2DFF1CF01E8950F000B1AD70 /* ContentCoordinator.swift */, + 2DFF1CF01E8950F000B1AD70 /* HomeCoordinator.swift */, 2D4FA8F61E85752B006C38ED /* Coordinator.swift */, 2DFF1CEC1E8944B900B1AD70 /* OnboardingCoordinator.swift */, - 2DFF1CEE1E89493300B1AD70 /* SignInCoordinator.swift */, ); path = Coordinators; sourceTree = ""; @@ -283,7 +280,7 @@ 2DFF1D131E8BE3D400B1AD70 /* Onboarding */ = { isa = PBXGroup; children = ( - 2DFF1D111E8BE27B00B1AD70 /* OnboardingPageViewController.swift */, + 2DFF1D111E8BE27B00B1AD70 /* OnboardingViewController.swift */, 2DFF1D1B1E8C424800B1AD70 /* OnboardingSamplePageViewController.swift */, 2DAB59F71E95336100310ABF /* OnboardingSamplePageViewModel.swift */, ); @@ -982,7 +979,7 @@ 2D75C6C81E8EEB1900F2EB1D /* AuthCoordinator.swift in Sources */, 2D75C6C71E8EEAE900F2EB1D /* OnboardingCoordinator.swift in Sources */, 2DFF1D1C1E8C424800B1AD70 /* OnboardingSamplePageViewController.swift in Sources */, - 2DFF1CF11E8950F000B1AD70 /* ContentCoordinator.swift in Sources */, + 2DFF1CF11E8950F000B1AD70 /* HomeCoordinator.swift in Sources */, ABF84F8D1DC99CFD002DB24D /* TextStyle.swift in Sources */, 8662C00A1FA2AA9900ADCBA9 /* SimpleTableCellItem.swift in Sources */, 861732F61FA2984600C14354 /* ViewRepresentable.swift in Sources */, @@ -990,7 +987,6 @@ BE4C21EB1E81970A00645143 /* DebugMenuConfiguration.swift in Sources */, 2D4FA8F71E85752B006C38ED /* Coordinator.swift in Sources */, 2D4FA8F51E8574F9006C38ED /* AppCoordinator.swift in Sources */, - 2DFF1CEF1E89493300B1AD70 /* SignInCoordinator.swift in Sources */, 2DAB59F81E95336100310ABF /* OnboardingSamplePageViewModel.swift in Sources */, 861732F41FA2984600C14354 /* TableCellItem.swift in Sources */, 861732F81FA2989100C14354 /* TableViewContainerCell.swift in Sources */, @@ -1021,7 +1017,7 @@ 2DFF1D181E8BF82A00B1AD70 /* Colors.swift in Sources */, 861732D51FA28DB600C14354 /* ModalDismissBehavior.swift in Sources */, BE28092A1E802893006E50D5 /* Analytics.swift in Sources */, - 2DFF1D121E8BE27B00B1AD70 /* OnboardingPageViewController.swift in Sources */, + 2DFF1D121E8BE27B00B1AD70 /* OnboardingViewController.swift in Sources */, ); runOnlyForDeploymentPostprocessing = 0; }; diff --git a/{{ cookiecutter.project_name | replace(' ', '') }}/app/{{ cookiecutter.project_name | replace(' ', '') }}/Application/AppDelegate.swift b/{{ cookiecutter.project_name | replace(' ', '') }}/app/{{ cookiecutter.project_name | replace(' ', '') }}/Application/AppDelegate.swift index 8463a00..3b04819 100644 --- a/{{ cookiecutter.project_name | replace(' ', '') }}/app/{{ cookiecutter.project_name | replace(' ', '') }}/Application/AppDelegate.swift +++ b/{{ cookiecutter.project_name | replace(' ', '') }}/app/{{ cookiecutter.project_name | replace(' ', '') }}/Application/AppDelegate.swift @@ -41,13 +41,14 @@ class AppDelegate: UIResponder, UIApplicationDelegate { let window = UIWindow(frame: UIScreen.main.bounds) self.window = window - self.coordinator = AppCoordinator(window: window) - coordinator.start(animated: true, completion: { + coordinator.start { rootViewController in + window.rootViewController = rootViewController + window.makeKeyAndVisible() for config in self.rootViewControllerDependentConfigurations where config.isEnabled { config.onDidLaunch(application: application, launchOptions: launchOptions) } - }) + } return true } diff --git a/{{ cookiecutter.project_name | replace(' ', '') }}/app/{{ cookiecutter.project_name | replace(' ', '') }}/Coordinators/AppCoordinator.swift b/{{ cookiecutter.project_name | replace(' ', '') }}/app/{{ cookiecutter.project_name | replace(' ', '') }}/Coordinators/AppCoordinator.swift index 724d09f..a924dfa 100644 --- a/{{ cookiecutter.project_name | replace(' ', '') }}/app/{{ cookiecutter.project_name | replace(' ', '') }}/Coordinators/AppCoordinator.swift +++ b/{{ cookiecutter.project_name | replace(' ', '') }}/app/{{ cookiecutter.project_name | replace(' ', '') }}/Coordinators/AppCoordinator.swift @@ -7,63 +7,78 @@ // import UIKit -import Services class AppCoordinator: Coordinator { private let window: UIWindow - fileprivate let rootController: UIViewController - var childCoordinator: Coordinator? init(window: UIWindow) { self.window = window - let rootController = UIViewController() - rootController.view.backgroundColor = .white - self.rootController = rootController } - func start(animated: Bool, completion: VoidClosure?) { - // Configure window/root view controller - window.setRootViewController(rootController, animated: false, completion: { - self.window.makeKeyAndVisible() + func start(with presentation: (UIViewController) -> Void) { + guard OnboardingCoordinator.hasOnboarded else { + let onboardingCoordinator = OnboardingCoordinator() + onboardingCoordinator.delegate = self + attach(to: onboardingCoordinator) + onboardingCoordinator.start(with: presentation) + return + } - // Spin off auth coordinator - let authCoordinator = AuthCoordinator(self.rootController) + let authCoordinator = AuthCoordinator() + guard authCoordinator.isAuthenticated else { authCoordinator.delegate = self - self.childCoordinator = authCoordinator - authCoordinator.start(animated: animated, completion: completion) - }) - } + attach(to: authCoordinator) + authCoordinator.startSignIn(with: presentation) + return + } - func cleanup(animated: Bool, completion: VoidClosure?) { - completion?() + let homeCoordinator = HomeCoordinator() + attach(to: homeCoordinator) + homeCoordinator.start(with: presentation) } } -extension AppCoordinator: AuthCoordinatorDelegate { +extension AppCoordinator: OnboardingCoordinator.Delegate { - func authCoordinator(_ coordinator: AuthCoordinator, didNotify action: AuthCoordinator.Action) { + func onboardingCoordinator(_ coordinator: OnboardingCoordinator, didNotify action: OnboardingCoordinator.Action) { switch action { - case .didSignIn: - guard let authCoordinator = childCoordinator as? AuthCoordinator else { - preconditionFailure("Upon signing in, AppCoordinator should have an AuthCoordinator as a child.") + case .skip: + let homeCoordinator = HomeCoordinator() + attach(to: homeCoordinator) + homeCoordinator.start { + window.setRootViewController($0, animated: true) } - childCoordinator = nil - authCoordinator.cleanup(animated: true) { - let contentCoordinator = ContentCoordinator(self.rootController) - self.childCoordinator = contentCoordinator - contentCoordinator.start(animated: true, completion: nil) + + case .join: + let authCoordinator = AuthCoordinator() + attach(to: authCoordinator) + authCoordinator.startSignUp { + window.rootViewController?.present($0, animated: true, completion: nil) } - case .didSkipAuth: - guard let authCoordinator = childCoordinator as? AuthCoordinator else { - preconditionFailure("Upon signing in, AppCoordinator should have an AuthCoordinator as a child.") + + case .signIn: + let authCoordinator = AuthCoordinator() + attach(to: authCoordinator) + authCoordinator.startSignIn { + window.rootViewController?.present($0, animated: true, completion: nil) } - childCoordinator = nil - authCoordinator.cleanup(animated: false) { - let contentCoordinator = ContentCoordinator(self.rootController) - self.childCoordinator = contentCoordinator - contentCoordinator.start(animated: true, completion: nil) + + } + } + +} + +extension AppCoordinator: AuthCoordinator.Delegate { + + func authCoordinator(_ coordinator: AuthCoordinator, didNotify action: AuthCoordinator.Action) { + switch action { + case .signedIn: + let homeCoordinator = HomeCoordinator() + attach(to: homeCoordinator) + homeCoordinator.start { + window.setRootViewController($0, animated: true) } } } diff --git a/{{ cookiecutter.project_name | replace(' ', '') }}/app/{{ cookiecutter.project_name | replace(' ', '') }}/Coordinators/AuthCoordinator.swift b/{{ cookiecutter.project_name | replace(' ', '') }}/app/{{ cookiecutter.project_name | replace(' ', '') }}/Coordinators/AuthCoordinator.swift index 3168a99..56cea72 100644 --- a/{{ cookiecutter.project_name | replace(' ', '') }}/app/{{ cookiecutter.project_name | replace(' ', '') }}/Coordinators/AuthCoordinator.swift +++ b/{{ cookiecutter.project_name | replace(' ', '') }}/app/{{ cookiecutter.project_name | replace(' ', '') }}/Coordinators/AuthCoordinator.swift @@ -7,58 +7,36 @@ // import UIKit -import Services - -private enum State { - - case authenticated - case onboarded - case needsOnboarding - -} class AuthCoordinator: Coordinator { - var childCoordinator: Coordinator? - let baseController: UIViewController weak var delegate: Delegate? - private let client = APIClient.shared - private var state: State { - if client.oauthClient.isAuthenticated { - return .authenticated - } - else if UserDefaults.hasOnboarded { - return .onboarded - } - else { - return .needsOnboarding - } + var isAuthenticated: Bool { + // TODO: inject and check from services + return false } - init(_ baseController: UIViewController) { - self.baseController = baseController - } + func startSignIn(with presentation: (UIViewController) -> Void) { + let nav = UINavigationController() + attach(to: nav) - func start(animated: Bool, completion: VoidClosure?) { - switch state { - case .authenticated: - notify(.didSignIn) - case .onboarded: - let signInCoordinator = SignInCoordinator(baseController) - signInCoordinator.delegate = self - signInCoordinator.start(animated: animated, completion: completion) - childCoordinator = signInCoordinator - case .needsOnboarding: - let onboardCoordinator = OnboardingCoordinator(baseController) - onboardCoordinator.delegate = self - onboardCoordinator.start(animated: animated, completion: completion) - childCoordinator = onboardCoordinator - } + let signInViewController = UIViewController() + signInViewController.view.backgroundColor = .yellow + signInViewController.title = L10n.Signin.title + nav.viewControllers = [signInViewController] + presentation(nav) } - func cleanup(animated: Bool, completion: VoidClosure?) { - childCoordinator?.cleanup(animated: animated, completion: completion) + func startSignUp(with presentation: (UIViewController) -> Void) { + let nav = UINavigationController() + attach(to: nav) + + let signUpViewController = UIViewController() + signUpViewController.view.backgroundColor = .orange + signUpViewController.title = L10n.Signup.title + nav.viewControllers = [signUpViewController] + presentation(nav) } } @@ -66,53 +44,7 @@ class AuthCoordinator: Coordinator { extension AuthCoordinator: Actionable { enum Action { - case didSignIn - case didSkipAuth - } - -} - -extension AuthCoordinator: SignInCoordinatorDelegate { - - func signInCoordinator(_ coordinator: SignInCoordinator, didNotify action: SignInCoordinator.Action) { - switch action { - case .didSignIn: - notify(.didSignIn) - } - } - -} - -extension AuthCoordinator: OnboardingCoordinatorDelegate { - - func onboardingCoordinator(_ coordinator: OnboardingCoordinator, didNotify action: OnboardingCoordinator.Action) { - switch action { - case .didSkipAuth: - notify(.didSkipAuth) - case .didRequestJoin: - guard let onboardCoordinator = childCoordinator as? OnboardingCoordinator else { - preconditionFailure("Upon signing in, AppCoordinator should have an AuthCoordinator as a child.") - } - childCoordinator = nil - onboardCoordinator.cleanup(animated: true) { - let signInCoordinator = SignInCoordinator(self.baseController) - signInCoordinator.delegate = self - self.childCoordinator = signInCoordinator - // TODO - signInCoordinator move from signIn to register here - signInCoordinator.start(animated: true, completion: nil) - } - case .didRequestSignIn: - guard let onboardCoordinator = childCoordinator as? OnboardingCoordinator else { - preconditionFailure("Upon signing in, AppCoordinator should have an AuthCoordinator as a child.") - } - childCoordinator = nil - onboardCoordinator.cleanup(animated: true) { - let signInCoordinator = SignInCoordinator(self.baseController) - signInCoordinator.delegate = self - self.childCoordinator = signInCoordinator - signInCoordinator.start(animated: true, completion: nil) - } - } + case signedIn } } diff --git a/{{ cookiecutter.project_name | replace(' ', '') }}/app/{{ cookiecutter.project_name | replace(' ', '') }}/Coordinators/Coordinator.swift b/{{ cookiecutter.project_name | replace(' ', '') }}/app/{{ cookiecutter.project_name | replace(' ', '') }}/Coordinators/Coordinator.swift index 8f3e4e8..75b7c3d 100644 --- a/{{ cookiecutter.project_name | replace(' ', '') }}/app/{{ cookiecutter.project_name | replace(' ', '') }}/Coordinators/Coordinator.swift +++ b/{{ cookiecutter.project_name | replace(' ', '') }}/app/{{ cookiecutter.project_name | replace(' ', '') }}/Coordinators/Coordinator.swift @@ -6,30 +6,27 @@ // Copyright © 2017 {{ cookiecutter.company_name }}. All rights reserved. // -import UIKit -import Services - -protocol Coordinator { - - /// A child coordinator spun off by this coordinator. - /// Important to keep a reference to prevent deallocation. - var childCoordinator: Coordinator? { get set } - - /// Start any action this coordinator should take. Often, this is - /// presenting/pushing a new controller, or starting up a - /// child coordinator. - /// - /// - Parameters: - /// - animated: whether to animate any transitions. - /// - completion: a completion block. - func start(animated: Bool, completion: VoidClosure?) - - /// Clean up after this coordinator. Should get the app back to the - /// state it was in when this coordinator started. - /// - /// - Parameters: - /// - animated: whether to animate any transitions. - /// - completion: a completion block. - func cleanup(animated: Bool, completion: VoidClosure?) +import ObjectiveC.runtime + +class Coordinator: NSObject {} + +extension Coordinator { + + private static var key = 0 + + func attach(to child: AnyObject) { + var attached = objc_getAssociatedObject(child, &Coordinator.key) as? NSMutableArray + + if attached == nil { + attached = NSMutableArray() + objc_setAssociatedObject(child, &Coordinator.key, attached, .OBJC_ASSOCIATION_RETAIN) + } + + attached?.add(self) + } + + func detatch(from child: AnyObject) { + (objc_getAssociatedObject(child, &Coordinator.key) as? NSMutableArray)?.remove(self) + } } diff --git a/{{ cookiecutter.project_name | replace(' ', '') }}/app/{{ cookiecutter.project_name | replace(' ', '') }}/Coordinators/HomeCoordinator.swift b/{{ cookiecutter.project_name | replace(' ', '') }}/app/{{ cookiecutter.project_name | replace(' ', '') }}/Coordinators/HomeCoordinator.swift new file mode 100644 index 0000000..f065c36 --- /dev/null +++ b/{{ cookiecutter.project_name | replace(' ', '') }}/app/{{ cookiecutter.project_name | replace(' ', '') }}/Coordinators/HomeCoordinator.swift @@ -0,0 +1,21 @@ +// +// HomeCoordinator.swift +// {{ cookiecutter.project_name | replace(' ', '') }} +// +// Created by {{ cookiecutter.lead_dev }} on 3/27/17. +// Copyright © 2017 {{ cookiecutter.company_name }}. All rights reserved. +// + +class HomeCoordinator: Coordinator { + + func start(with presentation: (UIViewController) -> Void) { + let nav = UINavigationController() + attach(to: nav) + let homeVC = UIViewController() + homeVC.view.backgroundColor = .red + homeVC.title = L10n.Home.title + nav.viewControllers = [homeVC] + presentation(nav) + } + +} diff --git a/{{ cookiecutter.project_name | replace(' ', '') }}/app/{{ cookiecutter.project_name | replace(' ', '') }}/Coordinators/OnboardingCoordinator.swift b/{{ cookiecutter.project_name | replace(' ', '') }}/app/{{ cookiecutter.project_name | replace(' ', '') }}/Coordinators/OnboardingCoordinator.swift index 23224bd..673f8a7 100644 --- a/{{ cookiecutter.project_name | replace(' ', '') }}/app/{{ cookiecutter.project_name | replace(' ', '') }}/Coordinators/OnboardingCoordinator.swift +++ b/{{ cookiecutter.project_name | replace(' ', '') }}/app/{{ cookiecutter.project_name | replace(' ', '') }}/Coordinators/OnboardingCoordinator.swift @@ -7,27 +7,20 @@ // import UIKit -import Services class OnboardingCoordinator: Coordinator { - let baseController: UIViewController - var childCoordinator: Coordinator? weak var delegate: Delegate? - init(_ baseController: UIViewController) { - self.baseController = baseController + static var hasOnboarded: Bool { + return UserDefaults.hasOnboarded } - func start(animated: Bool, completion: VoidClosure?) { - let vc = OnboardingPageViewController( - viewModels: OnboardingCoordinator.pageViewModels) + func start(with presentation: (UIViewController) -> Void) { + let vc = OnboardingViewController() vc.delegate = self - baseController.present(vc, animated: animated, completion: completion) - } - - func cleanup(animated: Bool, completion: VoidClosure?) { - baseController.dismiss(animated: animated, completion: completion) + attach(to: vc) + presentation(vc) } } @@ -35,37 +28,21 @@ class OnboardingCoordinator: Coordinator { extension OnboardingCoordinator: Actionable { enum Action { - case didSkipAuth - case didRequestJoin - case didRequestSignIn + case skip + case join + case signIn } } -extension OnboardingCoordinator: OnboardingPageViewControllerDelegate { +extension OnboardingCoordinator: OnboardingViewController.Delegate { - func onboardingPageViewController(_ vc: OnboardingPageViewController, didNotify action: OnboardingPageViewController.Action) { + func onboardingViewController(_ vc: OnboardingViewController, didNotify action: OnboardingViewController.Action) { switch action { - case .skipTapped: - notify(.didSkipAuth) - case .joinTapped: - notify(.didRequestJoin) - case .signInTapped: - notify(.didRequestSignIn) + case .skip: notify(.skip) + case .join: notify(.join) + case .signIn: notify(.signIn) } } } - -extension OnboardingCoordinator { - - static var pageViewModels: [OnboardingSamplePageViewModel] { - let samplePage = OnboardingSamplePageViewModel( - header: L10n.Onboarding.Pages.Sample.heading, - body: L10n.Onboarding.Pages.Sample.body, - asset: Asset.logoKennyLoggins - ) - return [samplePage, samplePage, samplePage] - } - -} diff --git a/{{ cookiecutter.project_name | replace(' ', '') }}/app/{{ cookiecutter.project_name | replace(' ', '') }}/Resources/Generated/Actionable+AutoConformance.swift b/{{ cookiecutter.project_name | replace(' ', '') }}/app/{{ cookiecutter.project_name | replace(' ', '') }}/Resources/Generated/Actionable+AutoConformance.swift index 38aa207..306eef5 100644 --- a/{{ cookiecutter.project_name | replace(' ', '') }}/app/{{ cookiecutter.project_name | replace(' ', '') }}/Resources/Generated/Actionable+AutoConformance.swift +++ b/{{ cookiecutter.project_name | replace(' ', '') }}/app/{{ cookiecutter.project_name | replace(' ', '') }}/Resources/Generated/Actionable+AutoConformance.swift @@ -37,34 +37,18 @@ extension OnboardingCoordinator { } -// MARK: - OnboardingPageViewController -protocol OnboardingPageViewControllerDelegate: class { - func onboardingPageViewController(_ vc: OnboardingPageViewController, didNotify action: OnboardingPageViewController.Action) +// MARK: - OnboardingViewController +protocol OnboardingViewControllerDelegate: class { + func onboardingViewController(_ vc: OnboardingViewController, didNotify action: OnboardingViewController.Action) } -extension OnboardingPageViewController { +extension OnboardingViewController { typealias ActionType = Action - typealias Delegate = OnboardingPageViewControllerDelegate + typealias Delegate = OnboardingViewControllerDelegate func notify(_ action: ActionType) { - delegate?.onboardingPageViewController(self, didNotify: action) - } - -} - -// MARK: - SignInCoordinator -protocol SignInCoordinatorDelegate: class { - func signInCoordinator(_ coordinator: SignInCoordinator, didNotify action: SignInCoordinator.Action) -} - -extension SignInCoordinator { - - typealias ActionType = Action - typealias Delegate = SignInCoordinatorDelegate - - func notify(_ action: ActionType) { - delegate?.signInCoordinator(self, didNotify: action) + delegate?.onboardingViewController(self, didNotify: action) } } diff --git a/{{ cookiecutter.project_name | replace(' ', '') }}/app/{{ cookiecutter.project_name | replace(' ', '') }}/Resources/Generated/Localized.swift b/{{ cookiecutter.project_name | replace(' ', '') }}/app/{{ cookiecutter.project_name | replace(' ', '') }}/Resources/Generated/Localized.swift index 4ede53b..70bdbb9 100644 --- a/{{ cookiecutter.project_name | replace(' ', '') }}/app/{{ cookiecutter.project_name | replace(' ', '') }}/Resources/Generated/Localized.swift +++ b/{{ cookiecutter.project_name | replace(' ', '') }}/app/{{ cookiecutter.project_name | replace(' ', '') }}/Resources/Generated/Localized.swift @@ -7,6 +7,11 @@ import Foundation // swiftlint:disable explicit_type_interface identifier_name line_length nesting type_body_length type_name enum L10n { + enum Home { + /// {{ cookiecutter.project_name | replace(' ', '') }} + static let title = L10n.tr("Localizable", "Home.Title") + } + enum Onboarding { enum Buttons { @@ -29,9 +34,14 @@ enum L10n { } } - enum Title { - /// {{ cookiecutter.project_name | replace(' ', '') }} - static let navigation = L10n.tr("Localizable", "Title.Navigation") + enum Signin { + /// Sign In + static let title = L10n.tr("Localizable", "SignIn.Title") + } + + enum Signup { + /// Sign Up + static let title = L10n.tr("Localizable", "SignUp.Title") } } // swiftlint:enable explicit_type_interface identifier_name line_length nesting type_body_length type_name diff --git a/{{ cookiecutter.project_name | replace(' ', '') }}/app/{{ cookiecutter.project_name | replace(' ', '') }}/Resources/Localizable.strings b/{{ cookiecutter.project_name | replace(' ', '') }}/app/{{ cookiecutter.project_name | replace(' ', '') }}/Resources/Localizable.strings index ff5e5a0..08a4c0c 100644 --- a/{{ cookiecutter.project_name | replace(' ', '') }}/app/{{ cookiecutter.project_name | replace(' ', '') }}/Resources/Localizable.strings +++ b/{{ cookiecutter.project_name | replace(' ', '') }}/app/{{ cookiecutter.project_name | replace(' ', '') }}/Resources/Localizable.strings @@ -1,7 +1,11 @@ -"Title.Navigation" = "{{ cookiecutter.project_name | replace(' ', '') }}"; - "Onboarding.Buttons.Skip" = "Skip"; "Onboarding.Buttons.Join" = "Join"; "Onboarding.Buttons.SignIn" = "Already have an account? Sign in."; "Onboarding.Pages.Sample.Heading" = "HEADING TEXT"; "Onboarding.Pages.Sample.Body" = "This is body copy for the onboarding and should be replaced with real text!"; + +"Home.Title" = "{{ cookiecutter.project_name | replace(' ', '') }}"; + +"SignIn.Title" = "Sign In"; + +"SignUp.Title" = "Sign Up"; diff --git a/{{ cookiecutter.project_name | replace(' ', '') }}/app/{{ cookiecutter.project_name | replace(' ', '') }}/Screens/Onboarding/OnboardingSamplePageViewController.swift b/{{ cookiecutter.project_name | replace(' ', '') }}/app/{{ cookiecutter.project_name | replace(' ', '') }}/Screens/Onboarding/OnboardingSamplePageViewController.swift index 9a07ec1..2433b51 100644 --- a/{{ cookiecutter.project_name | replace(' ', '') }}/app/{{ cookiecutter.project_name | replace(' ', '') }}/Screens/Onboarding/OnboardingSamplePageViewController.swift +++ b/{{ cookiecutter.project_name | replace(' ', '') }}/app/{{ cookiecutter.project_name | replace(' ', '') }}/Screens/Onboarding/OnboardingSamplePageViewController.swift @@ -10,9 +10,9 @@ import Anchorage class OnboardingSamplePageViewController: UIViewController { - fileprivate let imageView = UIImageView() + let imageView = UIImageView() - fileprivate let headerLabel: UILabel = { + let headerLabel: UILabel = { let label = UILabel() label.font = UIFont.systemFont(ofSize: 28) label.textColor = Colors.darkGray @@ -20,7 +20,7 @@ class OnboardingSamplePageViewController: UIViewController { return label }() - fileprivate let bodyLabel: UILabel = { + let bodyLabel: UILabel = { let label = UILabel() label.font = UIFont.systemFont(ofSize: 20) label.textColor = Colors.darkGray @@ -29,11 +29,8 @@ class OnboardingSamplePageViewController: UIViewController { return label }() - init(viewModel: OnboardingSamplePageViewModel) { + init() { super.init(nibName: nil, bundle: nil) - imageView.image = viewModel.asset?.image - headerLabel.text = viewModel.header - bodyLabel.text = viewModel.body } @available(*, unavailable) required init?(coder aDecoder: NSCoder) { diff --git a/{{ cookiecutter.project_name | replace(' ', '') }}/app/{{ cookiecutter.project_name | replace(' ', '') }}/Screens/Onboarding/OnboardingViewController.swift b/{{ cookiecutter.project_name | replace(' ', '') }}/app/{{ cookiecutter.project_name | replace(' ', '') }}/Screens/Onboarding/OnboardingViewController.swift new file mode 100644 index 0000000..6369ed2 --- /dev/null +++ b/{{ cookiecutter.project_name | replace(' ', '') }}/app/{{ cookiecutter.project_name | replace(' ', '') }}/Screens/Onboarding/OnboardingViewController.swift @@ -0,0 +1,193 @@ +// +// OnboardingViewController.swift +// {{ cookiecutter.project_name | replace(' ', '') }} +// +// Created by {{ cookiecutter.lead_dev }} on 3/29/17. +// Copyright © 2017 {{ cookiecutter.company_name }}. All rights reserved. +// + +import Anchorage +import Swiftilities + +// MARK: OnboardingViewController +class OnboardingViewController: UIViewController { + + fileprivate let viewControllers: [UIViewController] + + fileprivate let skipButton: UIButton = { + let button = UIButton() + button.setTitle(L10n.Onboarding.Buttons.skip, for: .normal) + button.setTitleColor(Colors.darkGray, for: .normal) + button.setTitleColor(Colors.darkGray.highlighted, for: .highlighted) + button.titleLabel?.font = UIFont.systemFont(ofSize: 18) + return button + }() + fileprivate let pageController = UIPageViewController( + transitionStyle: .scroll, navigationOrientation: .horizontal, options: nil) + fileprivate let firstHairline = HairlineView(axis: .horizontal) + fileprivate let joinButton: UIButton = { + let button = UIButton() + button.setTitle(L10n.Onboarding.Buttons.join, for: .normal) + button.setTitleColor(Colors.green, for: .normal) + button.setTitleColor(Colors.green.highlighted, for: .highlighted) + button.titleLabel?.font = UIFont.systemFont(ofSize: 17) + return button + }() + fileprivate let secondHairline = HairlineView(axis: .horizontal) + fileprivate let signInButton: UIButton = { + let button = UIButton() + button.setTitle(L10n.Onboarding.Buttons.signIn, for: .normal) + button.setTitleColor(Colors.darkGray, for: .normal) + button.setTitleColor(Colors.darkGray.highlighted, for: .highlighted) + button.titleLabel?.font = UIFont.systemFont(ofSize: 16) + return button + }() + weak var delegate: Delegate? + + init() { + let samplePage = OnboardingSamplePageViewModel( + header: L10n.Onboarding.Pages.Sample.heading, + body: L10n.Onboarding.Pages.Sample.body, + asset: Asset.logoKennyLoggins + ) + let viewModels = [samplePage, samplePage, samplePage] + + self.viewControllers = viewModels.map { + let vc = OnboardingSamplePageViewController() + vc.imageView.image = $0.asset?.image + vc.headerLabel.styledText = $0.header + vc.bodyLabel.styledText = $0.body + return vc + } + + super.init(nibName: nil, bundle: nil) + } + + @available(*, unavailable) required init?(coder aDecoder: NSCoder) { + fatalError("init(coder:) has not been implemented") + } + + override func viewDidLoad() { + super.viewDidLoad() + configureView() + configureLayout() + } + +} + +// MARK: Actionable +extension OnboardingViewController: Actionable { + + enum Action { + case skip + case join + case signIn + } + +} + +// MARK: Private +private extension OnboardingViewController { + + func configureView() { + view.backgroundColor = .white + view.addSubview(skipButton) + skipButton.addTarget(self, action: #selector(skipTapped), for: .touchUpInside) + + pageController.setViewControllers( + [viewControllers[0]], direction: .forward, animated: false, completion: nil) + pageController.dataSource = self + addChildViewController(pageController) + view.addSubview(pageController.view) + pageController.didMove(toParentViewController: self) + + let pageControlAppearance = UIPageControl.appearance( + whenContainedInInstancesOf: [OnboardingViewController.self]) + pageControlAppearance.pageIndicatorTintColor = Colors.lightGray + pageControlAppearance.currentPageIndicatorTintColor = Colors.darkGray + + view.addSubview(firstHairline) + joinButton.addTarget(self, action: #selector(joinTapped), for: .touchUpInside) + view.addSubview(joinButton) + view.addSubview(secondHairline) + signInButton.addTarget(self, action: #selector(signInTapped), for: .touchUpInside) + view.addSubview(signInButton) + } + + struct Layout { + static let skipButtonTrailingInset = CGFloat(20) + static let skipButtonTopInset = CGFloat(22) + static let pageViewTopSpace = CGFloat(20) + static let joinVerticalSpace = CGFloat(8) + static let signInVerticalSpace = CGFloat(18) + } + + func configureLayout() { + skipButton.topAnchor == view.topAnchor + Layout.skipButtonTopInset + skipButton.trailingAnchor == view.trailingAnchor - Layout.skipButtonTrailingInset + + pageController.view.topAnchor == skipButton.bottomAnchor + Layout.pageViewTopSpace + pageController.view.horizontalAnchors == view.horizontalAnchors + + firstHairline.topAnchor == pageController.view.bottomAnchor + firstHairline.horizontalAnchors == view.horizontalAnchors + + joinButton.horizontalAnchors == view.horizontalAnchors + joinButton.topAnchor == firstHairline.bottomAnchor + Layout.joinVerticalSpace + joinButton.bottomAnchor == secondHairline.topAnchor - Layout.joinVerticalSpace + + secondHairline.horizontalAnchors == view.horizontalAnchors + + signInButton.horizontalAnchors == view.horizontalAnchors + signInButton.topAnchor == secondHairline.bottomAnchor + Layout.signInVerticalSpace + signInButton.bottomAnchor == view.bottomAnchor - Layout.signInVerticalSpace + } + +} + +// MARK: Actions +private extension OnboardingViewController { + @objc func skipTapped() { + notify(.skip) + } + + @objc func joinTapped() { + notify(.join) + } + + @objc func signInTapped() { + notify(.signIn) + } + +} + +// MARK: UIPageViewControllerDataSource +extension OnboardingViewController: UIPageViewControllerDataSource { + + func pageViewController(_ pageViewController: UIPageViewController, viewControllerBefore viewController: UIViewController) -> UIViewController? { + guard let index = viewControllers.index(of: viewController), index > 0 else { + return nil + } + return viewControllers[index - 1] + } + + func pageViewController(_ pageViewController: UIPageViewController, viewControllerAfter viewController: UIViewController) -> UIViewController? { + guard let index = viewControllers.index(of: viewController), + index < viewControllers.count - 1 else { + return nil + } + return viewControllers[index + 1] + } + + func presentationCount(for pageViewController: UIPageViewController) -> Int { + return viewControllers.count + } + + func presentationIndex(for pageViewController: UIPageViewController) -> Int { + guard let current = pageViewController.viewControllers?.first else { + return 0 + } + return viewControllers.index(of: current) ?? 0 + } + +} From 56dc48bcdccfa15c1dcb3dfc60cfd2f87e15e1f2 Mon Sep 17 00:00:00 2001 From: Jay Clark Date: Wed, 21 Mar 2018 15:46:52 -0400 Subject: [PATCH 5/5] add missing delegate methods --- PRODUCTNAME/app/PRODUCTNAME/Coordinators/AppCoordinator.swift | 2 ++ .../Coordinators/AppCoordinator.swift | 2 ++ 2 files changed, 4 insertions(+) diff --git a/PRODUCTNAME/app/PRODUCTNAME/Coordinators/AppCoordinator.swift b/PRODUCTNAME/app/PRODUCTNAME/Coordinators/AppCoordinator.swift index 8c13db4..eb73118 100644 --- a/PRODUCTNAME/app/PRODUCTNAME/Coordinators/AppCoordinator.swift +++ b/PRODUCTNAME/app/PRODUCTNAME/Coordinators/AppCoordinator.swift @@ -53,6 +53,7 @@ extension AppCoordinator: OnboardingCoordinator.Delegate { case .join: let authCoordinator = AuthCoordinator() + authCoordinator.delegate = self attach(to: authCoordinator) authCoordinator.startSignUp { window.rootViewController?.present($0, animated: true, completion: nil) @@ -60,6 +61,7 @@ extension AppCoordinator: OnboardingCoordinator.Delegate { case .signIn: let authCoordinator = AuthCoordinator() + authCoordinator.delegate = self attach(to: authCoordinator) authCoordinator.startSignIn { window.rootViewController?.present($0, animated: true, completion: nil) diff --git a/{{ cookiecutter.project_name | replace(' ', '') }}/app/{{ cookiecutter.project_name | replace(' ', '') }}/Coordinators/AppCoordinator.swift b/{{ cookiecutter.project_name | replace(' ', '') }}/app/{{ cookiecutter.project_name | replace(' ', '') }}/Coordinators/AppCoordinator.swift index a924dfa..fc3f935 100644 --- a/{{ cookiecutter.project_name | replace(' ', '') }}/app/{{ cookiecutter.project_name | replace(' ', '') }}/Coordinators/AppCoordinator.swift +++ b/{{ cookiecutter.project_name | replace(' ', '') }}/app/{{ cookiecutter.project_name | replace(' ', '') }}/Coordinators/AppCoordinator.swift @@ -53,6 +53,7 @@ extension AppCoordinator: OnboardingCoordinator.Delegate { case .join: let authCoordinator = AuthCoordinator() + authCoordinator.delegate = self attach(to: authCoordinator) authCoordinator.startSignUp { window.rootViewController?.present($0, animated: true, completion: nil) @@ -60,6 +61,7 @@ extension AppCoordinator: OnboardingCoordinator.Delegate { case .signIn: let authCoordinator = AuthCoordinator() + authCoordinator.delegate = self attach(to: authCoordinator) authCoordinator.startSignIn { window.rootViewController?.present($0, animated: true, completion: nil)