36

I'm trying to make this extension:

extension UIViewController
{
    class func initialize(storyboardName: String, storyboardId: String) -> Self
    {
        let storyboad = UIStoryboard(name: storyboardName, bundle: nil)
        let controller = storyboad.instantiateViewControllerWithIdentifier(storyboardId) as! Self

        return controller
    }
}

But I get compile error:

error: cannot convert return expression of type 'UIViewController' to return type 'Self'

Is it possible? Also I want to make it as init(storyboardName: String, storyboardId: String)

Rizier123
  • 56,111
  • 16
  • 85
  • 130
ChikabuZ
  • 9,226
  • 5
  • 60
  • 79

4 Answers4

67

Similar as in Using 'self' in class extension functions in Swift, you can define a generic helper method which infers the type of self from the calling context:

extension UIViewController
{
    class func instantiateFromStoryboard(storyboardName: String, storyboardId: String) -> Self
    {
        return instantiateFromStoryboardHelper(storyboardName, storyboardId: storyboardId)
    }

    private class func instantiateFromStoryboardHelper<T>(storyboardName: String, storyboardId: String) -> T
    {
        let storyboard = UIStoryboard(name: storyboardName, bundle: nil)
        let controller = storyboard.instantiateViewControllerWithIdentifier(storyboardId) as! T
        return controller
    }
}

Then

let vc = MyViewController.instantiateFromStoryboard("name", storyboardId: "id")

compiles, and the type is inferred as MyViewController.


Update for Swift 3:

extension UIViewController
{
    class func instantiateFromStoryboard(storyboardName: String, storyboardId: String) -> Self
    {
        return instantiateFromStoryboardHelper(storyboardName: storyboardName, storyboardId: storyboardId)
    }

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

Another possible solution, using unsafeDowncast:

extension UIViewController
{
    class func instantiateFromStoryboard(storyboardName: String, storyboardId: String) -> Self
    {
        let storyboard = UIStoryboard(name: storyboardName, bundle: nil)
        let controller = storyboard.instantiateViewController(withIdentifier: storyboardId)
        return unsafeDowncast(controller, to: self)
    }
}
Martin R
  • 488,667
  • 78
  • 1,132
  • 1,248
  • 9
    Hi Martin, Could you please explain how the compiler be able to identify the generic type T and be able to cast it to Self? – Adithya Sep 10 '16 at 06:13
  • people who say "private class func" rather than "static func" have a lot of style. :) – Fattie Feb 05 '17 at 15:21
  • it's interesting that if you just have it return the base type - don't bother with a generic - it works perfectly at **runtime** (returning the actual subclass) but you'd have to cast results in code, at "editor time". example: stackoverflow.com/a/42053648/294884 – Fattie Feb 05 '17 at 15:34
  • @JoeBlow: Yes, the goal here was just to avoid the cast. – "static" is "class + final" and therefore unrelated to "private". – Martin R Feb 05 '17 at 16:29
  • quite. thanks @MartinR. It took me days to figure the answer to this conceptually similar issue http://stackoverflow.com/q/42041150/294884 – Fattie Feb 05 '17 at 19:48
  • whoa @MartinR - it appears that: http://stackoverflow.com/questions/42161710/can-only-return-not-assign-self – Fattie Feb 10 '17 at 14:25
  • MartinR, thank you for this great implementation. Since @Adithya's comment asking for clarification had 8 upvotes, I took the liberty to expand a little your response with a quick Further Explanation of your snippet. Feel free to disregard it or edit further. – luizv Dec 30 '17 at 05:55
  • I found that this compiles using `static` as well as `class`. I wonder if `class` adds anything. – ThomasW May 01 '18 at 16:29
  • @ThomasW: `static == class + final`, i.e. a type method which cannot be overridden in a subclass. So it won't make a difference unless you intend to override it in a subclass. – Martin R May 01 '18 at 16:32
  • @Adithya I think it's because you return `T` in helper method, and return `Self` in instantiate method, so the compiler automatically understand that `T` is `Self`. It's like a chain. – kientux May 04 '18 at 17:06
15

Self is determined at compile-time, not runtime. In your code, Self is exactly equivalent to UIViewController, not "the subclass that happens to be calling this." This is going to return UIViewController and the caller will have to as it into the right subclass. I assume that's what you were trying to avoid (though it is the "normal Cocoa" way to do it, so just returning UIViewController is probably the best solution).

Note: You should not name the function initialize in any case. That's an existing class function of NSObject and would cause confusion at best, bugs at worst.

But if you want to avoid the caller's as, subclassing is not usually the tool to add functionality in Swift. Instead, you usually want generics and protocols. In this case, generics are all you need.

func instantiateViewController<VC: UIViewController>(storyboardName: String, storyboardId: String) -> VC {
    let storyboad = UIStoryboard(name name: storyboardName, bundle: nil)
    let controller = storyboad.instantiateViewControllerWithIdentifier(storyboardId) as! VC

    return controller
}

This isn't a class method. It's just a function. There's no need for a class here.

let tvc: UITableViewController = instantiateViewController(name: name, storyboardId: storyboardId)
Rob Napier
  • 250,948
  • 34
  • 393
  • 528
  • 2
    This is a great answer. I had a very similar problem a while back. I wanted a protocol that classes that could be instantiated from a storyboard could implement, so that the caller didn't have to know the exact type of view controller being instantiated. I gave up on that approach. Maybe I'll try it again, but with a generic in the protocol. – NRitH Oct 18 '15 at 16:30
1

A cleaner solution (at least visually tidier):

Swift 5.1

class func initialize(storyboardName: String, storyboardId: String) -> Self {
    return UIStoryboard(name: storyboardName, bundle: nil)
        .instantiateViewController(withIdentifier: storyboardId).view as! Self
}
mojuba
  • 10,604
  • 6
  • 46
  • 65
0

Another way is to use a protocol, which also allows you to return Self.

protocol StoryboardGeneratable {

}

extension UIViewController: StoryboardGeneratable {

}

extension StoryboardGeneratable where Self: UIViewController
{
    static func initialize(storyboardName: String, storyboardId: String) -> Self
    {
        let storyboad = UIStoryboard(name: storyboardName, bundle: nil)
        let controller = storyboad.instantiateViewController(withIdentifier: storyboardId) as! Self
        return controller
    }
}
ukim
  • 2,185
  • 12
  • 19