The Problem
I really like storyboards, i like to draw my ui flow in one place without need of creating new .xib files for each controller. But there is a thing i miss from olds xibs, with xibs i can instantiate viewcontrollers in a single line of code:
1 |
let myCustomViewController = CustomViewControllerClass(nibName: "myXibName", bundle: nil) |
With storyboards, this is also possible but require a little more effort:
1 2 3 4 5 6 |
// Add an identifier to viewcontroller in storyboard, and copy in your code let viewControllerIdentifier = "CustomViewCntrollerIdentifier" let storyBoard = UIStoryboard(name: "Main", bundle: nil) // instantiateViewControllerWithIdentifier returns an UIViewController object so we need cast to right type let myCustomViewController = storyBoard.instantiateViewControllerWithIdentifier(viewControllerIdentifier) as! CustomViewControllerClass |
I don’t like this way, you have to cast always to the right class, identifiers are hard-coded strings, you have to add them in Interface Builder and then you have to remember and copy the strings everywhere in your code. Misspelling are behind the corner!
And of course if you decide to change the name of the identifier, you need to change it in every place it was hard-coded. A solution to hard-coding identifiers could be the use of enums:
1 2 3 4 5 6 7 8 9 10 |
enum StoryboardIdentifiers: String { case LoginViewController case SettingsViewController case AddressbookViewController } let storyBoard = UIStoryboard(name: "Main", bundle: nil) // instantiateViewControllerWithIdentifier returns an UIViewController object so we need cast to right type let myCustomViewController = storyBoard.instantiateViewControllerWithIdentifier(StoryboardIdentifiers.LoginViewController.rawValue) as! CustomViewControllerClass |
But that not solve type-safety, we still need cast to right type.
The Solution
I think the final use is very elegant and easy to use, you will have to design your viewcontroller in storyboard, assign a class, and use the same class name as identifier in Interface Builder. After few search, i wrote these lines of code:
First, i create a protocol called StoryboardInstantiable. Class variables or class methods are not allowed in protocols, use static instead.
1 2 3 4 |
protocol StoryboardInstantiable: class { static var storyboardIdentifier: String {get} static func instantiateFromStoryboard(storyboard: UIStoryboard) -> Self } |
Now we can extend UIViewController class to conform StoryboardInstantiable protocol. The idea is to use the view controller’s class name as identifier, so we will write it only once in Interface Builder.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 |
extension UIViewController: StoryboardInstantiable { static var storyboardIdentifier: String { // Get the name of current class let classString = NSStringFromClass(self) let components = classString.componentsSeparatedByString(".") assert(components.count > 0, "Failed extract class name from \(classString)") return components.last! } class func instantiateFromStoryboard(storyboard: UIStoryboard) -> Self { return instantiateFromStoryboard(storyboard, type: self) } } extension UIViewController { // Thanks to generics, return automatically the right type private class func instantiateFromStoryboard<T: UIViewController>(storyboard: UIStoryboard, type: T.Type) -> T { return storyboard.instantiateViewControllerWithIdentifier(self.storyboardIdentifier) as! T } } |
The next time you will need to instantiate manually your view controller, just use
1 2 |
let storyBoard = UIStoryboard(name: "Main", bundle: nil) let myCustomViewController = CustomViewControllerClass.instantiateFromStoryboard(storyBoard) |
One more tip
If you are tired of instantiate everytime the main Storyboard, this little method, return automatically the storyboard marked as main in the project settings
1 2 3 4 5 6 7 8 9 10 11 12 |
extension UIStoryboard { class func mainStoryboard() -> UIStoryboard! { guard let mainStoryboardName = NSBundle.mainBundle().infoDictionary?["UIMainStoryboardFile"] as? String else { assertionFailure("No UIMainStoryboardFile found in main bundle") return nil } return UIStoryboard(name: mainStoryboardName, bundle: NSBundle.mainBundle()) } } |
So the final code to instantiate manually a viewcontroller become
1 |
let myCustomViewController = CustomViewControllerClass.instantiateFromStoryboard(UIStoryboard.mainStoryboard()) |
You can view and download the full sample project on Github here.
Nice idea, but you still need to match the view controller identifier with the class name.
Would be nice if we could just get a reference programatically to all view controllers that conform / implement a protocol. Then you would just need to make your classes implement it.