Ollie
Ollie

Reputation: 1946

Creating a generic UViewController initialiser

I'm trying to create a UIViewController extension that I can use to initialise new instances. For each view controller in my project I have a corresponding storyboard.

i.e.

EditSomethingViewController.swift
EditSomethingViewController.storyboard

This is what I have so far:

extension UIViewController {

    static func initalize() -> UIViewController? {
        let name = String(self)

        let storyboard = UIStoryboard(name: name, bundle: nil)

        return storyboard.instantiateInitialViewController()
    }

}

However this means that when I use it, I still have to cast the response.

i.e.

if let viewController = EditSomethingViewController.initalize() as? EditSomethingViewController {
     // do something with view controller here  
}

Is it possible to create the extension in such a way that means I don't have to cast the response?

p.s. Working on an old project written in Swift 2.3 so would appreciate answers that are supported.

Upvotes: 8

Views: 2131

Answers (6)

RTXGamer
RTXGamer

Reputation: 3742

Approach 2

enum Storyboards: String {
    case main = "Main"
    case home = "Home"
    
    func instantiateVC<T>(_ identifier: T.Type) -> T?  {
        let storyboard = UIStoryboard(name: rawValue, bundle: nil)
        guard let viewcontroller = storyboard.instantiateViewController(withIdentifier: String(describing: identifier)) as? T else { return nil}
        return viewcontroller
    }
}

Usage: (Make sure Storyboard Id and class-name are the same.)

if let vc = Storyboards.main.instantiateVC(ViewController.self) {
    self.present(vc, animated: true, completion: nil)
}

Approach 1

Storyboard Extension:

extension UIStoryboard {
    enum Name: String {
        case main = "Main"
        case home = "Home"
    }

    static func instantiateViewController<T>(storyboard name: Name = .main, ofType type: T.Type) -> T? {
        return UIStoryboard(name: name.rawValue, bundle: nil).instantiateViewController(withIdentifier: String(describing: type.self)) as? T
    }
}

Usage: (Make sure Storyboard Id and class-name are the same.)

if let vc = UIStoryboard.instantiateViewController(storyboard: .home, ofType: HomeVC.self) {
    self.present(vc, animated: true, completion: nil)
}

Upvotes: 2

jboulter11
jboulter11

Reputation: 152

This is a heavy-duty solution, but it will be really nice if you decide to invest in it.

SwiftGen can generate code to programmatically initialize your view controllers from your storyboards. Docs

// You can instantiate scenes using the `instantiate` method:
let vc = StoryboardScene.Dependency.dependent.instantiate()

Upvotes: 0

Vina
Vina

Reputation: 1029

Adding to @Chikabuz answer, I could add his snippet to something like this

extension UIViewController
{
    class func instantiateFromStoryboard(_ name: String = "Main", identifier: String) -> Self
    {
        return instantiateFromStoryboardHelper(name, identifier: identifier)
    }

    fileprivate class func instantiateFromStoryboardHelper<T>(_ name: String, identifier: String) -> T
    {
        let storyboard = UIStoryboard(name: name, bundle: nil)
        let controller = storyboard.instantiateViewController(withIdentifier: identifier) as! T
        return controller
    }
}

And then, in my table view controller, something like this (i'm trying to automate each cell's row to its own vc):

struct MenuItem {
    let title: String
    let subtitle: String
    let `class`: AnyClass
}
class MenuViewController: UITableViewController {

    private var menu: [MenuItem] = [
        MenuItem(title: "Some View",
                 subtitle: "The good old description",
                 class: FirstViewController.self),
        MenuItem(title: "Another View",
                 subtitle: "Demo of this view",
                 class: SecondViewController.self)
    ]
    ...
    override func tableView(_ tableView: UITableView, didSelectRowAt indexPath: 
    IndexPath) {
        let item = menu[indexPath.row]

        let vcClass = item.class as! UIViewController.Type
        let vc = vcClass.instantiateFromStoryboard(identifier: String(describing: item.class))

        self.navigationController?.pushViewController(vc, animated: true)
        tableView.deselectRow(at: indexPath, animated: true)
    }
}

and there you go, magic! Your storyboard doesn't have to be exactly the same as vc's file name but default to Main.

PS: If you got some crashes, it's probably that you forgot to add identifier that must be exactly the same as your vc name,... or that vc is not inside Main.storyboard

Upvotes: 0

Craig Siemens
Craig Siemens

Reputation: 13296

You can change the return type to be Self which will match the type you are calling the method on.

This is a method I've used to do this. It will need to be put into a protocol extension instead.

static func loadFromStoryboard() -> Self? {
    let storyboard = UIStoryboard(name: NSStringFromClass(self),
                                  bundle: Bundle(for: self))
    return storyboard.instantiateInitialViewController() as? Self
}

Upvotes: 0

Sweeper
Sweeper

Reputation: 272770

I assume that you don't want to make every one of your VCs conform to a protocol manually. That would be too much work :)

I haven't tested this but this should work:

protocol Initializable {
    static func initalize() -> Self?
}

extension UIViewController: Initializable {
    static func initalize() -> Self? {
        let name = NSStringFromClass(self as! AnyClass)

        let storyboard = UIStoryboard(name: name, bundle: nil)

        return storyboard.getInitialVC(type: self)
    }
}

extension UIStoryboard {
    func getInitialVC<T: UIViewController>(type: T.Type) -> T? {
        return instantiateInitialViewController() as? T
    }
}

Upvotes: 1

ChikabuZ
ChikabuZ

Reputation: 10205

I use this extension:

extension UIViewController
{
    class func instantiateFromStoryboard(_ name: String = "Main") -> Self
    {
        return instantiateFromStoryboardHelper(name)
    }

    fileprivate class func instantiateFromStoryboardHelper<T>(_ name: String) -> T
    {
        let storyboard = UIStoryboard(name: name, bundle: nil)
        let controller = storyboard.instantiateViewController(withIdentifier: String(describing: self)) as! T
        return controller
    }
}

Usage:

let controller = MyViewController.instantiateFromStoryboard()

Upvotes: 8

Related Questions