简体   繁体   中英

UIViewController extension to instantiate from storyboard

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 (ie 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? (ie keeping -> Self as return value)

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()

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()

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)

}

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) .

Or, you can do so

  func instantiateViewControllerWithIdentifier<T>(_ identifier: T.Type) -> T {
            let identifier = String(describing: identifier)
            return instantiateViewController(withIdentifier: identifier) as! T
        }

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

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)!

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)

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.

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))
    }
}

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''

The technical post webpages of this site follow the CC BY-SA 4.0 protocol. If you need to reprint, please indicate the site URL or the original address.Any question please contact:yoyou2525@163.com.

 
粤ICP备18138465号  © 2020-2024 STACKOOM.COM