Last active
May 4, 2023 12:26
-
-
Save AliSoftware/4840a8a042c08a6d3ac8ecb3a059cf53 to your computer and use it in GitHub Desktop.
Our implementation of the Coordinators pattern — V2, more strict than V1
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
// 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 } | |
} | |
} |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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() | |
} | |
} |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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() | |
} | |
} |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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() | |
} | |
} |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
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