Skip to content

Instantly share code, notes, and snippets.

@AliSoftware
Last active July 10, 2022 14:32
Show Gist options
  • Save AliSoftware/6d2d146f7baccb0099cc to your computer and use it in GitHub Desktop.
Save AliSoftware/6d2d146f7baccb0099cc to your computer and use it in GitHub Desktop.
Coordinators & StateMachine - Concept
struct Coordinator {
let window: UIWindow
let navCtrl: UINavigationController?
func start() {
presentWelcomeScreen()
}
private func presentWelcomeScreen() {
let vc = WelcomeScreenViewController() // Instanciate from code, XIB, Storyboard, whatever your jam is
vc.navTransitions = WelcomeScreenTransitions(
showHome: self.pushHomeScreen, // see private func below
showCreateAccountScreen: self.pushCreateAccountScreen
)
window.rootViewController = vc // or whatever
}
private func pushHomeScreen(user: User?) {
let vc = HomeScreenViewController() // Instanciate from code, XIB, Storyboard, whatever your jam is
vc.user = user // or inject a ViewModel here if you're doing MVVM, you get the idea
vc.navTransitions = HomeScreenTransitions(
showUserInfoScreen: {
// From Home, we *push* the UserInfoVC
self.navigationController.pushViewController(instanciateUserInfoScreen(user), animated: true, completion: nil)
}
showProductDetail: self.pushProductDetail // extracted in private function below
disconnectAndShowLoginScreen: self.presentLoginScreen // same
)
navCtrl = UINavigationController(rootViewController: vc)
window.rootViewController = navCtrl
}
private func pushProductDetail(product: Product) {
let vc = ProductDetailViewController() // Instanciate from code, XIB, Storyboard, whatever your jam is
vc.product = product // inject any object to the VC, like a ViewModel or whatever
vc.navTransitions = ProductDetailTransitions(
showUserInfoScreen: {
// But from ProductDetail VC, we *present* the UserInfoVC *modally*
self.presentViewController(instanciateUserInfoScreen(product.owner), animated: true, completion: nil)
}
back: { navCtrl?.popViewController(animated: true) }
)
navCtrl?.pushViewController(vc, animated: true) { _ in }
}
private func pushCreateAccountScreen() {
// Create and push a CreateAccountViewController and its CreateAccountTransitions struct
...
}
private func instanciateUserInfoScreen(user: User) -> UIViewController {
// Create, configure and return an UserDetailsViewController
...
}
}
struct HomeScreenTransitions {
let showUserInfoScreen: Void -> Void
let showProductDetail: Product -> Void
let disconnectAndShowLoginScreen: Void -> Void
}
class HomeScreenViewController: UIViewController {
var navTransitions: HomeScreenTransitions!
@IBAction func profileAction(_: AnyObject) {
navTransitions.showUserInfoScreen()
}
@IBAction func disconnectAction(_: AnyObject) {
confirm("Are you sure?") {
navTransitions.disconnectAndShowLoginScreen()
}
}
@objc func tableView(tableView: UITableView, didSelectRowAtIndexPath: NSIndexPath) {
let product = productsDataSource[indexPath.row]
navTransitions.showProductDetail(product)
}
}
struct Product {
let id: Int
let owner: User
...
}
struct ProductDetailTransitions {
let showUserInfoScreen: Void -> Void
let back: Void -> Void
}
class ProductDetailViewController: UIViewController {
var navTransitions: ProductDetailTransitions!
@IBAction func showProductOwner(_: AnyObject) {
navTransitions.showUserInfoScreen()
}
...
}
struct WelcomeScreenTransitions {
let showHome: User? -> Void
let showCreateAccountScreen: Void -> Void
}
class WelcomeScreenViewController: UIViewController {
var navTransitions: WelcomeScreenTransitions!
var webService: WebServiceAPI!
@IBAction func loginAction(_: AnyObject) {
webService.login(userTextField.text, passwordTextField.text) { (user: User?) in
navTransitions.showHome(user)
}
}
@IBAction func createAccountAction(_: AnyObject) {
navTransitions.showCreateAccountScreen()
}
}
@AliSoftware
Copy link
Author

Main concept:

  • Each screen provides a struct listing all the possible transitions from that state. Like the struct WelcomeScreenTransitions which lists all the possible screens to go to from the WelcomeScreenViewController
    • These structs are only containers for closures. They don't carry any implementation details. (They act a bit like a wrapper around a list of completionBlock, one for each possible outcome / transition)
    • These structs don't have any dependency with the Coordinator. Separation of concerns is important.
  • Then the Coordinator is responsible for handling the transition between screens. To do so, each time it needs to present a screen, it is responsible for:
    • Instantiating the proper UIViewController
    • Create the …Transitions struct instance, providing the the implementation detail for each possible action, and pass that struct to the VC
    • Presenting the VC (push, modal, or whatever, it's the Coordinator's responsibility to decide)
  • The Coordinator is the only one knowing how to present each ViewController, and what are the presenting actions to do for this ViewController for its own next steps (the closures implementations). So all the navigation logic is handled by the Coordinator alone, and isn't responsible for anything else (except maybe passing ViewModels from VC to VC)

Other interesting links on a similar subject:

@NinoScript
Copy link

Remember to check if Coordinator -> Window -> VC -> closure that captures coordinator that has a Window inside causes a retain cycle or not ;)

@AliSoftware
Copy link
Author

yeah I think we may need to add [unowned window] in those closures to break the reference cycle, good point 👍

@AliSoftware
Copy link
Author

Mmmh… In that kind of code:

    vc.navTransitions = WelcomeScreenTransitions(
      showHome: self.pushHomeScreen, // see private func below
      showCreateAccountScreen: self.pushCreateAccountScreen
    )

I don't think we can use [unowned self] in the closure, as self here is the struct Coordinator so as a struct it's a value type and can't be marked unowned (doesn't have any sense for anything non-reference type)
But we can't either [unowned window] as the window isn't even referenced / captured in that closure (as we point the closures to private functions and not providing the implementations inline)

So I'm a bit puzzled here… is there really a reference cycle here (even with value types, though the struct Coordinator contains reference types like window: UIWindow), or is the cycle broken thanks to value types… and if it's not, how to break it as I believe we can't use unowned on a struct?

Will definitely have to try that in an actual playground at some point…

@AliSoftware
Copy link
Author

Ok there definitely are issues with retain cycles, and a struct/value type containing reference types doesn't help making things clear on this front.

The right solution might be to make Coordinator a class instead of a struct (which actually makes sense, as we don't really want independent copies all over), and be sure then to use [unowned self] in closures when appropriate.

@remirobert
Copy link

that's really awesome ! Good job AliSoftware 👽 .

@remirobert
Copy link

How to do you handle the cas with UITabBarController with severals UIViewController ?

  1. create and instance the UITabBarController directly in the appCoordinator.
  2. group all the transitions in the UITabBarController

@AliSoftware
Copy link
Author

@remirobert good question never had that use case until know. Since that gist using closure I've finally switched back to the delegates version of that Coordinator pattern anyway, which avoids the risk of hidden retain cycles we have with closures everywhere here

@sirvon
Copy link

sirvon commented May 31, 2017

@remirobert @AliSoftware any word or example code on using uitabcontroller with coordinator pattern?

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