Skip to content

Instantly share code, notes, and snippets.

@AliSoftware
Last active May 4, 2023 12:26
Show Gist options
  • Star 13 You must be signed in to star a gist
  • Fork 2 You must be signed in to fork a gist
  • Save AliSoftware/4840a8a042c08a6d3ac8ecb3a059cf53 to your computer and use it in GitHub Desktop.
Save AliSoftware/4840a8a042c08a6d3ac8ecb3a059cf53 to your computer and use it in GitHub Desktop.
Our implementation of the Coordinators pattern — V2, more strict than V1
// Coordinator.swift
import Foundation
public protocol Coordinator: class {
var components: CoordinatorComponents { get }
/// Set up here everything that needs to happen just before the Coordinator is presented
///
/// - Parameter modalSetup: A parameter you can use to customize the default mainViewController's
/// presentation style (e.g. `modalPresentationStyle`, etc), and to set the initial
/// ViewController of the Coordinator's UINavigationController (if you didn't
/// already set it in the init() when instantiating CoordinatorComponents).
///
/// - Attention: Don't call this method directly. It will be called automatically when you call
/// `present(childCoordinator:animated:completion:)`
func start(modalSetup: ViewControllerInitialSetup & ViewControllerModalSetup)
/// ⬆️ Present a child Coordinator modally
/// - Note: You can pass an overrideModalSetup closure if you need to customize the
/// modalPresentationStyle/modalTransitionStyle etc for that modal presentation
func present(childCoordinator coordinator: Coordinator, animated: Bool,
overrideModalSetup: ((ViewControllerModalSetup) -> Void)?, completion: (() -> Void)?)
/// ⬇️ Dismiss the top modal child Coordinator
func dismissChildCoordinator(animated: Bool, completion: (() -> Void)?)
/// ➡️ Push a ViewController on the current coordinator's NavigationController
func pushViewController(_ viewController: UIViewController, animated: Bool)
/// ⬅️ Pop the top ViewController from the current coordinator's NavigationController
func popViewController(animated: Bool)
/// ⏮ Rewind to the root ViewController of the current coordinator's NavigationController
func popToRootViewController(animated: Bool)
}
/// Exposes what is allowed to be changed on the mainVC during setup in the `start(modalSetup:)` implementation
public protocol ViewControllerModalSetup: class {
var definesPresentationContext: Bool { get set }
var providesPresentationContextTransitionStyle: Bool { get set }
var modalTransitionStyle: UIModalTransitionStyle { get set }
var modalPresentationStyle: UIModalPresentationStyle { get set }
var modalPresentationCapturesStatusBarAppearance: Bool { get set }
// We might want to add some other stuff later, like transitioningDelegate and stuff, this might not be exhaustive yet
}
public protocol ViewControllerInitialSetup: class {
func setInitialViewController(_ vc: UIViewController)
}
/**
Struct used for gathering Coordinator's properties and setting their default value.
*/
public class CoordinatorComponents {
fileprivate let mainViewController = UINavigationController()
fileprivate var childCoordinators = [Coordinator]()
public init() {}
}
// MARK: Default Implementation
extension UINavigationController: ViewControllerInitialSetup, ViewControllerModalSetup {
public func setInitialViewController(_ vc: UIViewController) {
self.viewControllers = [vc]
}
}
public extension Coordinator {
func present(childCoordinator coordinator: Coordinator, animated: Bool = true, overrideModalSetup: ((ViewControllerModalSetup) -> Void)? = nil, completion: (() -> Void)? = nil) {
self.components.childCoordinators.append(coordinator)
coordinator.start(modalSetup: self.components.mainViewController)
overrideModalSetup?(self.components.mainViewController)
self.components.mainViewController.present(coordinator.components.mainViewController, animated: animated, completion: completion)
}
func dismissChildCoordinator(animated: Bool = true, completion: (() -> Void)? = nil) {
self.components.mainViewController.dismiss(animated: animated, completion: completion)
self.components.childCoordinators.removeAll()
}
func pushViewController(_ viewController: UIViewController, animated: Bool) {
self.components.mainViewController.pushViewController(viewController, animated: animated)
}
func popViewController(animated: Bool) {
self.components.mainViewController.popViewController(animated: animated)
}
func popToRootViewController(animated: Bool) {
self.components.mainViewController.popToRootViewController(animated: animated)
}
}
extension UITabBarController {
public func configureTabs(using tabsCoordinators: [Coordinator]) {
tabsCoordinators.forEach { $0.start(modalSetup: $0.components.mainViewController) }
self.viewControllers = tabsCoordinators.map { $0.components.mainViewController }
}
}
protocol TutorialCoordinatorDelegate {
func dismissTutorial()
}
class TutorialCoordinator: Coordinator {
let components = CoordinatorComponents()
weak var delegate: TutorialCoordinatorDelegate?
func start(modalSetup: ViewControllerInitialSetup & ViewControllerModalSetup) {
let step1VC = TutoStep1ViewController.instantiate()
modalSetup.setInitialViewController(step1VC)
modalSetup.modalPresentationStyle = .formSheet
}
}
extension TutorialCoordinator: TutoStep1ViewControllerNavigation {
func showTutorialStep2() {
let step2VC = TutoStep2ViewController.instantiate()
self.pushViewController(step2VC)
}
}
extension TutorialCoordinator: TutoStep2ViewControllerNavigation {
func closeTutorial() {
self.delegate.dismissTutorial()
}
}
import UIKit
protocol TutoStep1ViewControllerNavigation: class {
// Add here functions to navigate away from this screen, to ask the Coordinator to show another screen
func showTutorialStep2()
}
final class TutoStep1ViewController: UIViewController {
weak var coordinator: TutoStep1ViewControllerNavigation?
// MARK: - Private Properties
// MARK: - Setup
static func instantiate(/* dependencies: Dependencies */) -> TutoStep1ViewController {
let vc = StoryboardScene.Tutorial.tutoStep1ViewController.instantiate() // Constants generated by SwiftGen
// vc.dependences = dependencies
return vc
}
// MARK: - Internal Funcs
@IBAction func goNext() {
self.coordinator?.showTutorialStep2()
}
}
import UIKit
protocol TutoStep2ViewControllerNavigation: class {
// Add here functions to navigate away from this screen, to ask the Coordinator to show another screen
func closeTutorial()
}
final class TutoStep2ViewController: UIViewController {
weak var coordinator: TutoStep2ViewControllerNavigation?
// MARK: - Private Properties
// MARK: - Setup
static func instantiate(/* dependencies: Dependencies */) -> TutoStep2ViewController {
let vc = StoryboardScene.Tutorial.tutoStep2ViewController.instantiate() // Constants generated by SwiftGen
// vc.dependences = dependencies
return vc
}
// MARK: - Internal Funcs
@IBAction func close() {
self.coordinator?.closeTutorial()
}
}
@samirGuerdah
Copy link

samirGuerdah commented Nov 18, 2018

Hi Olivier, Thanks for sharing this pattern.
I am planning to use Coordinators in my current projet and I have a question :) :

Does the coordinator responsable for the Data (Web services calls for example) ? Or do you use an other dedicated class for this ?
Thanks

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment