Coordinator pattern - Using Storyboard instead of Xib

1.9k views Asked by At

It is the first time I am going for coordinator pattern. Though I have realised it's importance but there is one major concern I have.
I went through this amazing article on this pattern. As a matter of fact I was able to build a demo project on my own using this. There is one point though - use of Xib is proposed. It is not exclusively mentioned that Storyboards can't be used, but going through these lines towards the end of article, makes me think otherwise :

With great power comes great responsibility (and limitations). To use this extension, you need to create a separate storyboard for each UIViewController. The name of the storyboard must match the name of the UIViewController‘s class. This UIViewController must be set as the initial UIViewController for this storyboard.

It isa mentioned that in case of Storyboards, we should create an extension and use that in UIViewController :

extension MyViewController: StoryboardInstantiable {
}  

StoryboardInstantiable :

import UIKit

protocol StoryboardInstantiable: NSObjectProtocol {
  associatedtype MyType  // 1
  static var defaultFileName: String { get }  // 2
  static func instantiateViewController(_ bundle: Bundle?) -> MyType // 3
}

extension StoryboardInstantiable where Self: UIViewController {
  static var defaultFileName: String {
    return NSStringFromClass(Self.self).components(separatedBy: ".").last!
  }

  static func instantiateViewController(_ bundle: Bundle? = nil) -> Self {
    let fileName = defaultFileName
    let sb = UIStoryboard(name: fileName, bundle: bundle)
    return sb.instantiateInitialViewController() as! Self
  }
}

Queries :

  1. As the author mentioned that separate Storyboard would have to be created for each UIViewController, how is using Xib a better way in Coordinator pattern ?
  2. Why do we need to create a separate Storyboard for each UIViewController ? Can't we use UIViewController's storyboard identifier for that by not linking any UIViewController using segues ? That way can adjust the above extension using identifier and easily achieve the same.
4

There are 4 answers

4
Scriptable On BEST ANSWER

I have read that tutorial many times and it uses a Coordinator for each View controller which doesn't make sense to me. I thought that the purpose of a Coordinator was to move the logic of navigation away from the view controllers and into a higher level object that can manage the overall flow.

If you would like to initialise ViewControllers from the main storyboard, use this protocol and extension instead:

import UIKit

protocol Storyboarded {
    static func instantiate() -> Self
}

extension Storyboarded where Self: UIViewController {
    static func instantiate() -> Self {
        // this pulls out "MyApp.MyViewController"
        let fullName = NSStringFromClass(self)

        // this splits by the dot and uses everything after, giving "MyViewController"
        let className = fullName.components(separatedBy: ".")[1]

        // load our storyboard
        let storyboard = UIStoryboard(name: "Main", bundle: Bundle.main)

        // instantiate a view controller with that identifier, and force cast as the type that was requested
        return storyboard.instantiateViewController(withIdentifier: className) as! Self
    }
}

The only requirement is that each View controller used by it has this protocol and has a StoryboardID with the same name as the class.

You can use it this way:

private func startBlueFlow() {
    let vc = BlueViewControllerOne.instantiate()
    vc.coordinator = self
    self.navigationController.push(vc, animated: true)
}

Disclaimer: Protocol taken from this article which may also help you

UPDATE: (added reference)

Soroush Khanlou is commonly credited and referenced in other articles and tutorials regarding the coordinator pattern in iOS and Redux. He has an article here (dated 2015, code in objective-c) which you may find to be an interesting read.

0
Zonily Jame On

My way of using Coordinators with storyboards is by using multiple storyboards. One storyboard per feature/module.

Why multiple storyboards instead of one? When working with a lot of features and with a team, it's better to split your storyboard because using only one storyboard will lead to a lot of merge conflicts and fixing storyboard git conflicts is one of the pains of being an iOS developer.

Here's how I do it.

First I have a protocol called AppStoryboardType which I will implement on an enum which contains the names of all my storyboards.

protocol AppStoryboardType {
    var instance: UIStoryboard { get }

    func instantiate<T: UIViewController>(_ viewController: T.Type, function: String, line: Int, file: String) -> T

    func instantiateInitialViewController() -> UIViewController?
}

extension AppStoryboardType {
    func instantiateInitialViewController() -> UIViewController? {
        return self.instance.instantiateInitialViewController()
    }
}

extension AppStoryboardType where Self: RawRepresentable, Self.RawValue == String {
    var instance: UIStoryboard {
        return UIStoryboard(name: self.rawValue, bundle: nil)
    }

    func instantiate<T: UIViewController>(
        _ viewController: T.Type,
        function: String = #function,
        line: Int = #line,
        file: String = #file) -> T {

        let storyboardID: String = T.storyboardIdentifier

        guard let vc = self.instance.instantiateViewController(withIdentifier: storyboardID) as? T else {
            fatalError("ViewController with identifier \(storyboardID), not found in \(self.rawValue) Storyboard.\nFile : \(file) \nLine Number : \(line) \nFunction : \(function)")
        }

        return vc
    }
}

enum AppStoryboard: String, AppStoryboardType {
    case Main /* ... Insert your other storyboards here. */

    // These are the refactored modules that use coordinator pattern.
    case PasswordRecovery, Registration
}

extension UIViewController {
    public static var defaultNibName: String {
        return self.description().components(separatedBy: ".").dropFirst().joined(separator: ".")
    }

    static var storyboardIdentifier: String {
        return "\(self)"
    }

    static func instantiate(fromAppStoryboard appStoryboard: AppStoryboard) -> Self {
        return appStoryboard.instantiate(self)
    }
}

Now that I've shown you the base that I use, here's how it's implemented on code.

let viewController = AppStoryboard.Login.instantiate(LoginViewController.self)
viewController./// set properties if ever you need to set them
presenter.present(viewController, animated: true, completion: nil)

PS: Most of the time for each module/feature I have it's own Storyboard and Coordinator, but that depends on the reusability of the UIViewController that you will use.

EDIT:

1 year later I have now stopped using my AppStoryboard approach and now use the Reusable library instead. The reason behind this is that it's cleaner and less prone to human error.

Now instead of the programmer (us) needing to know which storyboard a specific VC is attached to, we can now just simply subclass a viewcontroller to StoryboardSceneBased, supply the storyboard of that viewcontroller and the instantiate it simply by doing CustomViewController.instantiate()

// From this code
let viewController = AppStoryboard.Login.instantiate(LoginViewController.self)

// To this code
let viewController = LoginViewController.instantiate()
1
Leszek Szary On

You actually asked two questions so I will also split my answer into two parts:

About XIB vs Storyboard

I see a very little reason why would you use xib instead of storyboard for view controllers when using coordinator pattern. One advantage for xibs that comes to my mind is that when using xib you can use later a different subclass for given view controller and use same xib with it. For example lets say if you create a xib for EmployeesViewController class you can later create for example AdministratorsViewControllers subclass with modified functionality and init it with the same xib that you created before. With storyboard you can't do that because the class of the view controller is already set on the storyboard and cannot be changed. Something like that might be useful for example if you are creating a framework and you want to give users the possibility to subclass your base class while keeping your UI. However in most cases probably you will not need to do anything like that. On the other hand using storyboards will give you access to features such as table view cells prototypes on storyboards, static cells in table view controllers and other things not available when using xibs. So while there are cases when xibs are better, probably in most cases storyboards will be more useful.

About creating a separate storyboard for each UIViewController

As you noticed you could use storyboard identifier and not split every view controller into a separate storyboard (as also showed in other answer here). Putting each view controller into a separate storyboard might not look very typical, however it is actually not as pointless as it might initially seems to be. Probably the biggest advantage is that when you put each view controller in a separate storyboard you will typically get less merge conflicts on git when working in a team (especially because sometimes xcode changes some values of some properties in other view controllers in storyboard even if you do not modify them). This also makes code review quicker and more pleasant for your team. Beside that it is also easier to copy those storyboards into a different project if they have some common UI. This might be useful especially if for example you are working in a company that creates specific types of apps for various clients. So as you can see there are some advantages here but the choice is up to you. I would not say that this approach is bad or good. I think both are fine and it is more a matter of preferences. Just pick the one that you prefer and better suits you.

1
alpamys.dosbol On

I used enum and changed instanciate() method. Everything working fine for me

enum OurStoryboards: String{
    case MainPage = "MainPage"
    case Catalog = "Catalog"
    case Search = "Search"
    case Info = "Info"
    case Cart = "Cart"
}

protocol Storyboarded {
    static func instantiate(_ storyboardId: OurStoryboards) -> Self
}

extension Storyboarded where Self: UIViewController {
    static func instantiate(_ storyboardId: OurStoryboards) -> Self {

        let id = String(describing: self)
        // load our storyboard
        var storyboard = UIStoryboard()
        switch storyboardId {
        case .MainPage:
            storyboard = UIStoryboard(name: OurStoryboards.MainPage.rawValue ,bundle: Bundle.main)
        case .Catalog:
            storyboard = UIStoryboard(name: OurStoryboards.Catalog.rawValue ,bundle: Bundle.main)
        case .Search:
            storyboard = UIStoryboard(name: OurStoryboards.Search.rawValue ,bundle: Bundle.main)
        case .Info:
            storyboard = UIStoryboard(name: OurStoryboards.Info.rawValue ,bundle: Bundle.main)
        case .Cart:
            storyboard = UIStoryboard(name: OurStoryboards.Cart.rawValue ,bundle: Bundle.main)
        }
        // instantiate a view controller with that identifier, and force cast as the type that was requested
        return storyboard.instantiateViewController(withIdentifier: id) as! Self
    }

}