简体   繁体   English

从故事板实例化的 UIViewController 扩展

[英]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.我正在尝试在 Swift 中编写一个小扩展来处理故事板中UIViewController实例化。

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:我的想法如下:由于UIStoryboard的方法instantiateViewControllerWithIdentifier需要一个标识符来实例化给定故事板的视图控制器,为什么不在我的故事板中为每个视图控制器分配一个等于其确切类名的标识符(即UserDetailViewController将有一个标识符“UserDetailViewController”),并在 UIViewController 上创建一个类方法,它将:

  • accept a UIStoryboard instance as a unique parameter接受UIStoryboard实例作为唯一参数
  • get the current class name as a string以字符串形式获取当前类名
  • call instantiateViewControllerWithIdentifier on the storyboard instance with the class name as a parameter以类名作为参数调用 storyboard 实例上的instantiateViewControllerWithIdentifier
  • get the newly created UIViewController instance, and return it获取新创建的UIViewController实例,并返回

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:我曾经在 Objective-C 中使用以下类别进行操作:

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

But I'm completely stuck with the Swift version.但我完全坚持使用 Swift 版本。 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.返回Self而不是AnyObject允许类型推断工作。 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.这给了我错误: Use of unresolved identifier 'Self' NSStringFromClass部分似乎是问题所在。

What do you think?你怎么认为?

  • Is there any way to return Self from class functions?有没有办法从类函数中返回Self

  • How would you get this working without the need to cast the return value every time?您如何在不需要每次都转换返回值的情况下使其工作? (ie keeping -> Self as return value) (即保持-> Self作为返回值)

How about writing an extension to UIStoryboard instead of UIViewController ? 如何编写扩展到UIStoryboard而不是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: 感谢MartinR和他的回答 ,我知道答案:

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 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. 我们正在将我们的客观c项目移植到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. Objective-C中的类构造函数是Swift中的便捷初始化器。 Use convenience init rather than class func . 使用convenience init而不是class func
  • NSStringFromClass(Self) with NSStringFromClass(self.type) . NSStringFromClass(Self)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 在UIViewController中使用协议来实现您的想法

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

You can see the use of my link :D 你可以看到我链接的使用:D

https://github.com/JavanC/StoryboardDesignable 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: 你可以像这样创建UIViewController实例:

Create enum with all your storyboard name. 使用您的所有故事板名称创建enum

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

Then, here is the extension for instantiate UIViewController 然后,这是实例化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: 这是一个基于@ findall解决方案的现代Swift示例:

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). 作为@ChikabuZ版本的补充,我在这里考虑了故事板所在的捆绑包(例如,如果你的故事板在另一个捆绑包中而不是你的应用程序)。 I added also a small func if you want to use xib instead of storyboad. 如果你想使用xib而不是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:它仍然使用正常的实例化过程,但消除了对字符串类型 Storyboard 和 View Controller 名称的依赖:

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

The return type above is pre-cast to MyViewController , not the standard UIViewController .上面的返回类型是预先转换为MyViewController ,而不是标准的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!请注意,您必须确保每个 VC 的Storyboard Identifier与其类名完全匹配! 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''由于未捕获的异常“NSInvalidArgumentException”而终止应用程序,原因:“故事板(<UIStoryboard:0x6000035c04e0>)不包含标识符为“MyViewController”的视图控制器

声明:本站的技术帖子网页,遵循CC BY-SA 4.0协议,如果您需要转载,请注明本站网址或者原文地址。任何问题请咨询:yoyou2525@163.com.

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