Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Learning Topic: Coordinator Pattern #4

Open
wants to merge 15 commits into
base: main
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
8 changes: 8 additions & 0 deletions .github/scripts/test_app.sh
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
#!/bin/bash

set -eo pipefail

xcodebuild -project DinkleBot/DinkleBot.xcodeproj \
-scheme DinkleBot \
-destination platform=iOS\ Simulator,OS=14.4,name=iPhone\ 11 \
clean test | xcpretty
9 changes: 6 additions & 3 deletions .github/workflows/ios.yml
Original file line number Diff line number Diff line change
Expand Up @@ -12,8 +12,11 @@ jobs:
XC_PROJECT: ${{ 'Dinklebot/Dinklebot.xcodeproj' }}
XC_SCHEME: ${{ 'Dinklebot' }}
steps:
- name: Select latest Xcode
- name: Select Xcode 12.4
run: "sudo xcode-select -s /Applications/Xcode_$XC_VERSION.app"
- uses: actions/checkout@v2
- name: Checkout repository
uses: actions/checkout@v2
- name: Testing iOS app
run: exec ./.github/scripts/test_app.sh
- name: Build and Test
run: /usr/bin/xcodebuild test -project "$XC_PROJECT" -scheme "$XC_SCHEME" -destination 'platform=iOS Simulator,name=iPhone 11' clean test | xcpretty
run: /usr/bin/xcodebuild test -project "$XC_PROJECT" -list
910 changes: 910 additions & 0 deletions DinkleBot/DinkleBot.xcodeproj/project.pbxproj

Large diffs are not rendered by default.

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
<plist version="1.0">
<dict>
<key>IDEDidComputeMac32BitWarning</key>
<true/>
</dict>
</plist>
48 changes: 48 additions & 0 deletions DinkleBot/DinkleBot/Application/AppDelegate.swift
Original file line number Diff line number Diff line change
@@ -0,0 +1,48 @@
//
// AppDelegate.swift
// DinkleBot
//
// Created by Iain Smith on 13/06/2021.
//

@UIApplicationMain
class AppDelegate: UIResponder, UIApplicationDelegate {

var window: UIWindow?
var rootController: UINavigationController {
return self.window!.rootViewController as! UINavigationController
}

private lazy var applicationCoordinator: Coordinator = self.makeCoordinator()

func application(_ application: UIApplication,
didFinishLaunchingWithOptions launchOptions: [UIApplication.LaunchOptionsKey: Any]?) -> Bool {
let notification = launchOptions?[.remoteNotification] as? [String: AnyObject]
let deepLink = DeepLinkOption.build(with: notification)
applicationCoordinator.start(with: deepLink)
return true
}

private func makeCoordinator() -> Coordinator {
return ApplicationCoordinator(
router: RouterImp(rootController: self.rootController),
coordinatorFactory: CoordinatorFactoryImp()
)
}

//MARK: Handle push notifications and deep links
func application(_ application: UIApplication,
didReceiveRemoteNotification userInfo: [AnyHashable : Any]) {
let dict = userInfo as? [String: AnyObject]
let deepLink = DeepLinkOption.build(with: dict)
applicationCoordinator.start(with: deepLink)
}

func application(_ application: UIApplication, continue userActivity: NSUserActivity,
restorationHandler: @escaping ([UIUserActivityRestoring]?) -> Void) -> Bool {
let deepLink = DeepLinkOption.build(with: userActivity)
applicationCoordinator.start(with: deepLink)
return true
}
}

89 changes: 89 additions & 0 deletions DinkleBot/DinkleBot/Application/ApplicationCoordinator.swift
Original file line number Diff line number Diff line change
@@ -0,0 +1,89 @@
//
// AppCoordinator.swift
// DinkleBot
//
// Created by Iain Smith on 16/06/2021.
//

// TODO Store these somewhere else
fileprivate var onboardingWasShown = false
fileprivate var isAutorized = false

fileprivate enum LaunchInstructor {
case main, auth, onboarding

static func configure(
tutorialWasShown: Bool = onboardingWasShown,
isAutorized: Bool = isAutorized) -> LaunchInstructor {

switch (tutorialWasShown, isAutorized) {
case (true, false), (false, false): return .auth
case (false, true): return .onboarding
case (true, true): return .main
}
}
}

final class ApplicationCoordinator: BaseCoordinator {

private let coordinatorFactory: CoordinatorFactory
private let router: Router

private var instructor: LaunchInstructor {
return LaunchInstructor.configure()
}

init(router: Router, coordinatorFactory: CoordinatorFactory) {
self.router = router
self.coordinatorFactory = coordinatorFactory
}

override func start(with option: DeepLinkOption?) {
//start with deepLink
if let option = option {
switch option {
case .onboarding: runOnboardingFlow()
case .signUp: runAuthFlow()
default: childCoordinators.forEach { coordinator in
coordinator.start(with: option)
}
}
// default start
} else {
switch instructor {
case .onboarding: runOnboardingFlow()
case .auth: runAuthFlow()
case .main: runMainFlow()
}
}
}

private func runAuthFlow() {
let coordinator = coordinatorFactory.makeAuthCoordinatorBox(router: router)
coordinator.finishFlow = { [weak self, weak coordinator] in
isAutorized = true
self?.start()
self?.removeDependency(coordinator)
}
addDependency(coordinator)
coordinator.start()
}

private func runOnboardingFlow() {
let coordinator = coordinatorFactory.makeOnboardingCoordinator(router: router)
coordinator.finishFlow = { [weak self, weak coordinator] in
onboardingWasShown = true
self?.start()
self?.removeDependency(coordinator)
}
addDependency(coordinator)
coordinator.start()
}

private func runMainFlow() {
let (coordinator, module) = coordinatorFactory.makeTabbarCoordinator()
addDependency(coordinator)
router.setRootModule(module, hideBar: true)
coordinator.start()
}
}
34 changes: 34 additions & 0 deletions DinkleBot/DinkleBot/Base/BaseCoordinator.swift
Original file line number Diff line number Diff line change
@@ -0,0 +1,34 @@
class BaseCoordinator: Coordinator {

var childCoordinators: [Coordinator] = []

func start() {
start(with: nil)
}

func start(with option: DeepLinkOption?) { }

// add only unique object
func addDependency(_ coordinator: Coordinator) {
guard !childCoordinators.contains(where: { $0 === coordinator }) else { return }
childCoordinators.append(coordinator)
}

func removeDependency(_ coordinator: Coordinator?) {
guard
childCoordinators.isEmpty == false,
let coordinator = coordinator
else { return }

// Clear child-coordinators recursively
if let coordinator = coordinator as? BaseCoordinator, !coordinator.childCoordinators.isEmpty {
coordinator.childCoordinators
.filter({ $0 !== coordinator })
.forEach({ coordinator.removeDependency($0) })
}
for (index, element) in childCoordinators.enumerated() where element === coordinator {
childCoordinators.remove(at: index)
break
}
}
}
6 changes: 6 additions & 0 deletions DinkleBot/DinkleBot/Extensions/NSObjectExtensions.swift
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
extension NSObject {

class var nameOfClass: String {
return NSStringFromClass(self).components(separatedBy: ".").last!
}
}
18 changes: 18 additions & 0 deletions DinkleBot/DinkleBot/Extensions/UIViewControllerExtension.swift
Original file line number Diff line number Diff line change
@@ -0,0 +1,18 @@
extension UIViewController {

private class func instantiateControllerInStoryboard<T: UIViewController>(_ storyboard: UIStoryboard, identifier: String) -> T {
return storyboard.instantiateViewController(withIdentifier: identifier) as! T
}

class func controllerInStoryboard(_ storyboard: UIStoryboard, identifier: String) -> Self {
return instantiateControllerInStoryboard(storyboard, identifier: identifier)
}

class func controllerInStoryboard(_ storyboard: UIStoryboard) -> Self {
return controllerInStoryboard(storyboard, identifier: nameOfClass)
}

class func controllerFromStoryboard(_ storyboard: Storyboards) -> Self {
return controllerInStoryboard(UIStoryboard(name: storyboard.rawValue, bundle: nil), identifier: nameOfClass)
}
}
17 changes: 17 additions & 0 deletions DinkleBot/DinkleBot/Extensions/UIViewExtensions.swift
Original file line number Diff line number Diff line change
@@ -0,0 +1,17 @@
extension UIView {

private class func viewInNibNamed<T: UIView>(_ nibNamed: String) -> T {
return Bundle.main.loadNibNamed(nibNamed, owner: nil, options: nil)!.first as! T
}

class func nib() -> Self {
return viewInNibNamed(nameOfClass)
}

class func nib(_ frame: CGRect) -> Self {
let view = nib()
view.frame = frame
view.layoutIfNeeded()
return view
}
}
5 changes: 5 additions & 0 deletions DinkleBot/DinkleBot/Factories/AuthModuleFactory.swift
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
protocol AuthModuleFactory {
func makeLoginOutput() -> LoginView
func makeSignUpHandler() -> SignUpView
func makeTermsOutput() -> TermsView
}
9 changes: 9 additions & 0 deletions DinkleBot/DinkleBot/Factories/CoordinatorFactory.swift
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
protocol CoordinatorFactory {
func makeTabbarCoordinator() -> (configurator: Coordinator, toPresent: Presentable?)
func makeAuthCoordinatorBox(router: Router) -> Coordinator & AuthCoordinatorOutput
func makeOnboardingCoordinator(router: Router) -> Coordinator & OnboardingCoordinatorOutput
func makeSettingsCoordinator() -> Coordinator
func makeSettingsCoordinator(navController: UINavigationController?) -> Coordinator
func makeHomeCoordinator() -> Coordinator
func makeHomeCoordinator(navController: UINavigationController?) -> Coordinator
}
3 changes: 3 additions & 0 deletions DinkleBot/DinkleBot/Factories/HomeModuleFactory.swift
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
protocol HomeModuleFactory {
func makeHomeOutput() -> HomeView
}
44 changes: 44 additions & 0 deletions DinkleBot/DinkleBot/Factories/Imp/CoordinatorFactoryImp.swift
Original file line number Diff line number Diff line change
@@ -0,0 +1,44 @@
final class CoordinatorFactoryImp: CoordinatorFactory {

func makeTabbarCoordinator() -> (configurator: Coordinator, toPresent: Presentable?) {
let controller = TabbarController.controllerFromStoryboard(.main)
let coordinator = TabbarCoordinator(tabbarView: controller, coordinatorFactory: CoordinatorFactoryImp())
return (coordinator, controller)
}

func makeAuthCoordinatorBox(router: Router) -> Coordinator & AuthCoordinatorOutput {
let coordinator = AuthCoordinator(router: router, factory: ModuleFactoryImp())
return coordinator
}

func makeOnboardingCoordinator(router: Router) -> Coordinator & OnboardingCoordinatorOutput {
return OnboardingCoordinator(with: ModuleFactoryImp(), router: router)
}

func makeSettingsCoordinator() -> Coordinator {
return makeSettingsCoordinator(navController: nil)
}

func makeSettingsCoordinator(navController: UINavigationController? = nil) -> Coordinator {
let coordinator = SettingsCoordinator(router: router(navController), factory: ModuleFactoryImp())
return coordinator
}

func makeHomeCoordinator() -> Coordinator {
return makeHomeCoordinator(navController: nil)
}

func makeHomeCoordinator(navController: UINavigationController? = nil) -> Coordinator {
let coordinator = HomeCoordinator(router: router(navController), factory: ModuleFactoryImp())
return coordinator
}

private func router(_ navController: UINavigationController?) -> Router {
return RouterImp(rootController: navigationController(navController))
}

private func navigationController(_ navController: UINavigationController?) -> UINavigationController {
if let navController = navController { return navController }
else { return UINavigationController.controllerFromStoryboard(.main) }
}
}
30 changes: 30 additions & 0 deletions DinkleBot/DinkleBot/Factories/Imp/ModuleFactoryImp.swift
Original file line number Diff line number Diff line change
@@ -0,0 +1,30 @@
final class ModuleFactoryImp:
AuthModuleFactory,
OnboardingModuleFactory,
SettingsModuleFactory,
HomeModuleFactory {

func makeLoginOutput() -> LoginView {
return LoginController.controllerFromStoryboard(.auth)
}

func makeSignUpHandler() -> SignUpView {
return SignUpController.controllerFromStoryboard(.auth)
}

func makeTermsOutput() -> TermsView {
return TermsController.controllerFromStoryboard(.auth)
}

func makeOnboardingModule() -> OnboardingView {
return OnboardingController.controllerFromStoryboard(.onboarding)
}

func makeSettingsOutput() -> SettingsView {
return SettingsController.controllerFromStoryboard(.settings)
}

func makeHomeOutput() -> HomeView {
return HomeController.controllerFromStoryboard(.home)
}
}
3 changes: 3 additions & 0 deletions DinkleBot/DinkleBot/Factories/OnboardingModuleFactory.swift
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
protocol OnboardingModuleFactory {
func makeOnboardingModule() -> OnboardingView
}
3 changes: 3 additions & 0 deletions DinkleBot/DinkleBot/Factories/SettingsModuleFactory.swift
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
protocol SettingsModuleFactory {
func makeSettingsOutput() -> SettingsView
}
8 changes: 8 additions & 0 deletions DinkleBot/DinkleBot/Flows/Home Flow/HomeController.swift
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
final class HomeController: UIViewController, HomeView {

override func viewDidLoad() {
super.viewDidLoad()

title = "Home"
}
}
21 changes: 21 additions & 0 deletions DinkleBot/DinkleBot/Flows/Home Flow/HomeCoordinator.swift
Original file line number Diff line number Diff line change
@@ -0,0 +1,21 @@
final class HomeCoordinator: BaseCoordinator {

private let factory: HomeModuleFactory
private let router: Router

init(router: Router, factory: HomeModuleFactory) {
self.factory = factory
self.router = router
}

override func start() {
showHome()
}

//MARK: - Run current flow's controllers

private func showHome() {
let homeFlowOutput = factory.makeHomeOutput()
router.setRootModule(homeFlowOutput)
}
}
1 change: 1 addition & 0 deletions DinkleBot/DinkleBot/Flows/Home Flow/HomeView.swift
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
protocol HomeView: BaseView { }
Loading