UIViewController extension to instantiate from sto

2020-05-23 08:58发布

问题:

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)

Thanks.

回答1:

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


回答2:

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


回答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)

}


回答4:

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


回答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)


回答6:

Or, you can do so

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


回答7:

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



回答8:

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.



回答9:

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


回答10:

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