17

I'm trying to write a little extension in Swift to handle instantiation of a UIViewController from a storyboard.

My idea is the following: Since UIStoryboard's method instantiateViewControllerWithIdentifier needs an identifier to instantiate a given storyboard's view controller, why don't assign every view controller in my storyboard an identifier equal to its exact class name (i.e a UserDetailViewController would have an identifier of "UserDetailViewController"), and, create a class method on UIViewController that would:

  • accept a UIStoryboard instance as a unique parameter
  • get the current class name as a string
  • call instantiateViewControllerWithIdentifier on the storyboard instance with the class name as a parameter
  • get the newly created UIViewController instance, and return it

So, instead of (which repeats the class name as a string, not very nice)

let vc = self.storyboard?.instantiateViewControllerWithIdentifier("UserDetailViewController") as UserDetailViewController

it would be:

let vc = UserDetailViewController.instantiateFromStoryboard(self.storyboard!)

I used to do it in Objective-C with the following category:

+ (instancetype)instantiateFromStoryboard:(UIStoryboard *)storyboard
{
    return [storyboard instantiateViewControllerWithIdentifier:NSStringFromClass([self class])];
}

But I'm completely stuck with the Swift version. I hope is that there is some kind of way to do it. I tried the following:

extension UIViewController {
    class func instantiateFromStoryboard(storyboard: UIStoryboard) -> Self {
        return storyboard.instantiateViewControllerWithIdentifier(NSStringFromClass(Self))
    }
}

Returning Self instead of AnyObject allows the type inference to work. Otherwise, I would have to cast every single return of this method, which is annoying, but maybe you have a better solution?

This gives me the error: Use of unresolved identifier 'Self' The NSStringFromClass part seems to be the problem.

What do you think?

  • Is there any way to return Self from class functions?

  • How would you get this working without the need to cast the return value every time? (i.e keeping -> Self as return value)

Cristik
  • 30,989
  • 25
  • 91
  • 127
Nicolas B.
  • 1,318
  • 1
  • 11
  • 20
  • I can see the utility, but realize that this "solution" precludes having two instances of the same view controller class in a given storyboard. – Caleb Sep 29 '14 at 15:30
  • 1
    Of course, but that is something that is known to be impossible (because fixed by convention) in my app. I made sure two view controllers in my storyboard cannot have the same class name. – Nicolas B. Oct 01 '14 at 07:30
  • 1
    Echoing @Caleb, this is probably a bad idea. You're imposing an unnecessary constraint with very little benefit. Convention is not particularly reliable in the long run. – mattt Oct 02 '14 at 02:15
  • Why don't you implement that with Objective-C? – rintaro Oct 02 '14 at 14:23
  • If you keep a single controller per storyboard (as recommended for complex layouts/multiple developers), then you could skip the storyboard parameter as well. – Rivera Oct 03 '14 at 02:47
  • Yes, but I do use storyboard and their segue features :) – Nicolas B. Oct 03 '14 at 07:10

11 Answers11

31

How about writing an extension to UIStoryboard instead of UIViewController?

extension UIStoryboard {
    func instantiateVC<T: UIViewController>() -> T? {
        // get a class name and demangle for classes in Swift
        if let name = NSStringFromClass(T.self)?.componentsSeparatedByString(".").last {
            return instantiateViewControllerWithIdentifier(name) as? T
        }
        return nil
    }

}

Even adopting this approach, cost of an use side is low as well.

let vc: UserDetailViewController? = aStoryboard.instantiateVC()
findall
  • 2,176
  • 2
  • 17
  • 21
  • What if I don't specify the type of vc. Would then T be inferred at compile time wile bing used in T.self? – BangOperator Jun 21 '16 at 12:05
  • 1
    @BangOperator I cannot try it for now, but it must cause a compile error, I think. Because the compiler could not substitute any type to the T so that corresponding machine codes could not be undeterminable as well. – findall Jun 21 '16 at 12:19
  • so when we do `let vc: UserDetailViewController = aStoryboard.instantiateVC()` is `T` here equal to `UserDetailViewController`? Can you elaborate it bit more on what's happening under the hood? I mean when doing so you are benefiting from Type Safety.right? – mfaani Sep 19 '16 at 16:12
  • @Honey, Yes the extension infers the type as 'UserDetailViewController' when we provide explicit type to 'vc'. In case we do not provide explicit type, this code will through a compile time error, as T type cannot be inferred. – BangOperator May 17 '17 at 12:32
8

Thanks to MartinR and his answer, I know the answer:

UPDATE: rewritten with a protocol.

Instantiable

protocol StringConvertible {
    var rawValue: String {get}
}

protocol Instantiable: class {
    static var storyboardName: StringConvertible {get}
}

extension Instantiable {
    static func instantiateFromStoryboard() -> Self {
        return instantiateFromStoryboardHelper()
    }

    private static func instantiateFromStoryboardHelper<T>() -> T {
        let identifier = String(describing: self)
        let storyboard = UIStoryboard(name: storyboardName.rawValue, bundle: nil)
        return storyboard.instantiateViewController(withIdentifier: identifier) as! T
    }
}

//MARK: -

extension String: StringConvertible { // allow string as storyboard name
    var rawValue: String {
        return self
    }
}

StoryboardName

enum StoryboardName: String, StringConvertible {
    case main = "Main"
    //...
}

Usage:

class MyViewController: UIViewController, Instantiable {

    static var storyboardName: StringConvertible {
        return StoryboardName.main //Or you can use string value "Main"
    }
}

let viewController = MyController.instantiateFromStoryboard()
ChikabuZ
  • 10,031
  • 5
  • 63
  • 86
5

you can create UIViewController Instance like this:

Create enum with all your storyboard name.

enum AppStoryboard: String {
   case main = "Main"
   case profile = "Profile"
}

Then, here is the extension for instantiate UIViewController

extension UIViewController {

    class func instantiate<T: UIViewController>(appStoryboard: AppStoryboard) -> T {

        let storyboard = UIStoryboard(name: appStoryboard.rawValue, bundle: nil)
        let identifier = String(describing: self)
        return storyboard.instantiateViewController(withIdentifier: identifier) as! T
    }
}

Usage:

let profileVC: ProfileVC = ProfileVC.instantiate(appStoryboard: .profile)
self.navigationController?.pushViewController(profileVC,animated:true)
Rumit Patel
  • 8,830
  • 18
  • 51
  • 70
Abhi Makadia
  • 128
  • 1
  • 8
  • 1
    we can change method signature to something like this. ```class func instantiate(from storyboard: iSecureMeStoryboard) -> T where T : UIViewController``` – Zubair Apr 28 '19 at 04:30
3

We are porting our objective c project to swift. We have split the project into modules. Modules have their own storyboards. We have extended your(even our's as well) problem's solution to one more level by avoiding explicit storyboard names.

// Add you modules here. Make sure rawValues refer to a stroyboard file name.
enum StoryModule : String {
    case SomeModule
    case AnotherModule = "AnotherModulesStoryBoardName"
    // and so on...
}

extension UIStoryboard {
    class func instantiateController<T>(forModule module : StoryModule) -> T {
        let storyboard = UIStoryboard.init(name: module.rawValue, bundle: nil);
        let name = String(T).componentsSeparatedByString(".").last
        return storyboard.instantiateViewControllerWithIdentifier(name!) as! T
    }
}

// Some controller whose UI is in a stroyboard named "SomeModule.storyboard",
// and whose storyboardID is the class name itself, ie "MyViewController"
class MyViewController : UIViewController {
    // Controller Code
}

// Usage
class AClass
{
    // Here we must alwasy provide explicit type
    let viewController : MyViewController = UIStoryboard.instantiateController(forModule: StoryModule.SomeModule)

}
BangOperator
  • 4,377
  • 2
  • 24
  • 38
2

Two things:

  • Class constructors in Objective-C are convenience initializers in Swift. Use convenience init rather than class func.
  • NSStringFromClass(Self) with NSStringFromClass(self.type).
mattt
  • 19,544
  • 7
  • 73
  • 84
  • Okay, I was missing the point with class constructors in Objc being convenience initializers in Swift. I think writing an `init(storyboard: UIStoryboard)` would be impossible since convenience initializers have to delegate with `self.init()`. It makes no sense in my case. – Nicolas B. Oct 02 '14 at 07:42
  • So basically, there is no elegant solution to my "problem". – Nicolas B. Oct 02 '14 at 08:10
2

Or, you can do so

func instantiateViewControllerWithIdentifier<T>(_ identifier: T.Type) -> T {
    let identifier = String(describing: identifier)
    return instantiateViewController(withIdentifier: identifier) as! T
}
pkamb
  • 33,281
  • 23
  • 160
  • 191
Alexander Khitev
  • 6,417
  • 13
  • 59
  • 115
2

Here is a modern Swift example, based on @findall's solution:

extension UIStoryboard {
    func instantiate<T>() -> T {
        return instantiateViewController(withIdentifier: String(describing: T.self)) as! T
    }

    static let main = UIStoryboard(name: "Main", bundle: nil)
}

Usage:

let userDetailViewController = UIStoryboard.main.instantiate() as UserDetailViewController

I think it is ok to fail when trying to instantiate a view controller from a storyboard as this kind of problem should be detected soon.

adauguet
  • 988
  • 8
  • 18
1

Use protocol in UIViewController to reach your thoughts

let vc = YourViewController.instantiate(from: .StoryboardName)

You can see the use of my link :D

https://github.com/JavanC/StoryboardDesignable

Javan Chen
  • 11
  • 1
0

You can add this extension :-

extension UIStoryboard{

    func instantiateViewController<T:UIViewController>(type: T.Type) -> T? {
        var fullName: String = NSStringFromClass(T.self)
        if let range = fullName.range(of:".", options:.backwards, range:nil, locale: nil){
            fullName = fullName.substring(from: range.upperBound)
        }
        return self.instantiateViewController(withIdentifier:fullName) as? T
    }
}

And can instantiate view controller like this :-

self.storyboard?.instantiateViewController(type: VC.self)!
Himanshu
  • 2,832
  • 4
  • 23
  • 51
0

In complement for the version of @ChikabuZ, here mine that takes into account which bundle the storyboard is in (for example, if your storyboads are in another bundle than your app). I added also a small func if you want to use xib instead of storyboad.

extension UIViewController {

    static func instantiate<TController: UIViewController>(_ storyboardName: String) -> TController {
        return instantiateFromStoryboardHelper(storyboardName)
    }

    static func instantiate<TController: UIViewController>(_ storyboardName: String, identifier: String) -> TController {
        return instantiateFromStoryboardHelper(storyboardName, identifier: identifier)
    }

    fileprivate static func instantiateFromStoryboardHelper<T: UIViewController>(_ name: String, identifier: String? = nil) -> T {
        let storyboard = UIStoryboard(name: name, bundle: Bundle(for: self))
        return storyboard.instantiateViewController(withIdentifier: identifier ?? String(describing: self)) as! T
    }

    static func instantiate<TController: UIViewController>(xibName: String? = nil) -> TController {
        return TController(nibName: xibName ?? String(describing: self), bundle: Bundle(for: self))
    }
}
Lapinou
  • 1,467
  • 2
  • 20
  • 39
0

I had a similar thought and settled on using the extension below. It still uses the normal instantiation process, but removes reliance on stringly typed Storyboard and View Controller names:

let myVC = UIStoryboard(.main).instantiate(MyViewController.self)

The return type above is pre-cast to MyViewController, not the standard UIViewController.

extension UIStoryboard {
    
    enum Name: String {
        case main   = "Main"
        case launch = "LaunchScreen"
        case other  = "Other"
    }
    
    convenience init(_ name: Name, bundle: Bundle? = nil) {
        self.init(name: name.rawValue, bundle: bundle)
    }
    
    func instantiate<T: UIViewController>(_ type: T.Type) -> T {
        instantiateViewController(withIdentifier: String(describing: type)) as! T
    }
    
}

Note that you must ensure that each VC's Storyboard Identifier exactly matches its class name! Failure to do so will result in the exception:

Terminating app due to uncaught exception 'NSInvalidArgumentException', reason: 'Storyboard (<UIStoryboard: 0x6000035c04e0>) doesn't contain a view controller with identifier 'MyViewController''

pkamb
  • 33,281
  • 23
  • 160
  • 191