UIViewController 扩展从情节提要中实例化

Posted

技术标签:

【中文标题】UIViewController 扩展从情节提要中实例化【英文标题】:UIViewController extension to instantiate from storyboard 【发布时间】:2014-09-26 09:15:43 【问题描述】:

我正在尝试在 Swift 中编写一个小扩展来处理来自情节提要的 UIViewController 的实例化。

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

接受UIStoryboard 实例作为唯一参数 获取当前类名作为字符串 在情节提要实例上调用instantiateViewControllerWithIdentifier,并以类名作为参数 获取新创建的UIViewController 实例,并返回它

所以,而不是(将类名作为字符串重复,不是很好)

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

应该是:

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

我曾经在 Objective-C 中使用以下类别:

+ (instancetype)instantiateFromStoryboard:(UIStoryboard *)storyboard

    return [storyboard instantiateViewControllerWithIdentifier:NSStringFromClass([self class])];

但我完全坚持使用 Swift 版本。我希望有某种方法可以做到这一点。 我尝试了以下方法:

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

返回 Self 而不是 AnyObject 允许类型推断工作。否则,我将不得不强制转换此方法的每个返回,这很烦人,但也许你有更好的解决方案?

这给了我错误:Use of unresolved identifier 'Self' NSStringFromClass 部分似乎是问题所在。

你怎么看?

有没有办法从类函数中返回Self

如何在不需要每次都转换返回值的情况下完成这项工作? (即保持-> Self作为返回值)

【问题讨论】:

我可以看到该实用程序,但意识到此“解决方案”排除了在给定情节提要中具有相同视图控制器类的两个实例。 当然,但这在我的应用程序中是已知不可能的(因为按惯例固定)。我确保我的故事板中的两个视图控制器不能具有相同的类名。 呼应@Caleb,这可能是个坏主意。你强加了一个不必要的约束,几乎没有什么好处。从长远来看,约定并不是特别可靠。 为什么不用 Objective-C 来实现呢? 如果您为每个故事板保留一个控制器(推荐给复杂布局/多个开发人员),那么您也可以跳过故事板参数。 【参考方案1】:

写一个扩展名到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
    


即使采用这种方式,使用方的成本也很低。

let vc: UserDetailViewController? = aStoryboard.instantiateVC()

【讨论】:

如果我不指定vc的类型怎么办。那么在 T.self 中使用 bing 时会在编译时推断出 T 吗? @BangOperator 我现在无法尝试,但我认为它一定会导致编译错误。因为编译器无法将任何类型替换为 T,因此相应的机器代码也无法确定。 所以当我们做let vc: UserDetailViewController = aStoryboard.instantiateVC()T 等于UserDetailViewController?你能详细说明一下幕后发生的事情吗?我的意思是当你这样做时,你会从 Type Safety 中受益。对吧? @Honey,是的,当我们为“vc”提供显式类型时,扩展将类型推断为“UserDetailViewController”。如果我们不提供显式类型,此代码将通过编译时错误,因为无法推断 T 类型。【参考方案2】:

感谢MartinR和他的answer,我知道答案了:

更新:用协议重写。

可实例化

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
    

故事板名称

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

用法:

class MyViewController: UIViewController, Instantiable 

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


let viewController = MyController.instantiateFromStoryboard()

【讨论】:

【参考方案3】:

您可以像这样创建UIViewController 实例:

使用您所有的故事板名称创建enum

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

那么,这里是实例化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
    

用法:

let profileVC: ProfileVC = ProfileVC.instantiate(appStoryboard: .profile)
self.navigationController?.pushViewController(profileVC,animated:true)

【讨论】:

我们可以把方法签名改成这样。 class func instantiate&lt;T&gt;(from storyboard: iSecureMeStoryboard) -&gt; T where T : UIViewController【参考方案4】:

我们正在将我们的目标 c 项目移植到 swift。我们已将项目拆分为模块。模块有自己的故事板。我们通过避免使用明确的故事板名称,将您(甚至我们的)问题的解决方案扩展到了一个更高的级别。

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


【讨论】:

【参考方案5】:

两件事:

Objective-C 中的类构造函数是 Swift 中的便利初始化器。使用convenience init 而不是class funcNSStringFromClass(Self)NSStringFromClass(self.type)

【讨论】:

好吧,我忽略了 Objc 中的类构造函数是 Swift 中的便利初始化器这一点。我认为编写init(storyboard: UIStoryboard) 是不可能的,因为便利初始化程序必须委托self.init()。这对我来说毫无意义。 所以基本上,我的“问题”没有优雅的解决方案。【参考方案6】:

或者,你可以这样做

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

【讨论】:

【参考方案7】:

在 UIViewController 中使用协议来达成你的想法

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

你可以看到我的链接的使用:D

https://github.com/JavanC/StoryboardDesignable

【讨论】:

【参考方案8】:

这是一个现代 Swift 示例,基于 @findall 的解决方案:

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

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

用法:

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

我认为尝试从故事板实例化视图控制器时失败是可以的,因为这种问题应该很快就会被发现。

【讨论】:

【参考方案9】:

你可以添加这个扩展:-

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
    

并且可以像这样实例化视图控制器:-

self.storyboard?.instantiateViewController(type: VC.self)!

【讨论】:

【参考方案10】:

作为@ChikabuZ 版本的补充,这里我的考虑了情节提要所在的捆绑包(例如,如果您的情节提要位于与您的应用程序不同的捆绑包中)。如果你想使用 xib 而不是故事板,我还添加了一个小函数。

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

【讨论】:

【参考方案11】:

我有类似的想法并决定使用下面的扩展程序。它仍然使用正常的实例化过程,但消除了对字符串类型的 Storyboard 和 View Controller 名称的依赖:

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

上面的返回类型预先转换为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
    
    

请注意,您必须确保每个 VC 的 Storyboard Identifier 与其类名完全匹配!否则将导致异常:

由于未捕获的异常“NSInvalidArgumentException”而终止应用,原因:“Storyboard () 不包含标识符为“MyViewController”的视图控制器”

【讨论】:

以上是关于UIViewController 扩展从情节提要中实例化的主要内容,如果未能解决你的问题,请参考以下文章

如何从情节提要以编程方式加载 UIViewController?

没有情节提要的 UIViewController 状态恢复不起作用

如何以编程方式使用另一个视图中的按钮从情节提要中打开 UIViewController?

未从 UITableViewController 调用 didDeselectRowAtIndexPath 从情节提要添加到 UIViewController

使用情节提要时切换 UIViewController

如何从情节提要属性为 nil 的动态创建的 UIViewController 执行SegueWithIdentifier()