UIView控制器扩展,用于从情节提要实例化



我正在尝试在 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保留为返回值(

如何编写扩展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()

感谢马丁和他的回答,我知道答案:

更新:用协议重写。

可实例化

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

您可以像这样创建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)

我们正在将我们的目标 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)
}

两件事:

  • Objective-C 中的类构造函数是 Swift 中的方便初始值设定项。使用convenience init而不是class func
  • NSStringFromClass(Self)NSStringFromClass(self.type).

下面是一个基于 @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)
}

用法:

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

我认为尝试从故事板实例化视图控制器时失败是可以的,因为应该很快就会检测到此类问题。

或者

,你可以这样做

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

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

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

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

https://github.com/JavanC/StoryboardDesignable

您可以添加此扩展:-

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

作为@ChikabuZ版本的补充,这里是我的,它考虑了故事板在哪个捆绑包中(例如,如果你的故事在另一个捆绑包中而不是你的应用程序(。如果你想使用 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))
    }
}

我也有类似的想法,并决定使用下面的扩展。它仍然使用正常的实例化过程,但消除了对字符串类型情节提要和视图控制器名称的依赖:

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"而终止应用,原因:"情节提要 (<UIStoryboard:>( 不包含标识符为"MyViewController"的视图控制器">

最新更新