Reputation: 1946
I'm trying to create a UIViewController
extension that I can use to initialise new instances. For each view controller in my project I have a corresponding storyboard.
i.e.
EditSomethingViewController.swift
EditSomethingViewController.storyboard
This is what I have so far:
extension UIViewController {
static func initalize() -> UIViewController? {
let name = String(self)
let storyboard = UIStoryboard(name: name, bundle: nil)
return storyboard.instantiateInitialViewController()
}
}
However this means that when I use it, I still have to cast the response.
i.e.
if let viewController = EditSomethingViewController.initalize() as? EditSomethingViewController {
// do something with view controller here
}
Is it possible to create the extension in such a way that means I don't have to cast the response?
p.s. Working on an old project written in Swift 2.3 so would appreciate answers that are supported.
Upvotes: 8
Views: 2131
Reputation: 3742
Approach 2
enum Storyboards: String {
case main = "Main"
case home = "Home"
func instantiateVC<T>(_ identifier: T.Type) -> T? {
let storyboard = UIStoryboard(name: rawValue, bundle: nil)
guard let viewcontroller = storyboard.instantiateViewController(withIdentifier: String(describing: identifier)) as? T else { return nil}
return viewcontroller
}
}
Usage: (Make sure Storyboard Id and class-name are the same.)
if let vc = Storyboards.main.instantiateVC(ViewController.self) {
self.present(vc, animated: true, completion: nil)
}
Approach 1
Storyboard Extension:
extension UIStoryboard {
enum Name: String {
case main = "Main"
case home = "Home"
}
static func instantiateViewController<T>(storyboard name: Name = .main, ofType type: T.Type) -> T? {
return UIStoryboard(name: name.rawValue, bundle: nil).instantiateViewController(withIdentifier: String(describing: type.self)) as? T
}
}
Usage: (Make sure Storyboard Id and class-name are the same.)
if let vc = UIStoryboard.instantiateViewController(storyboard: .home, ofType: HomeVC.self) {
self.present(vc, animated: true, completion: nil)
}
Upvotes: 2
Reputation: 152
This is a heavy-duty solution, but it will be really nice if you decide to invest in it.
SwiftGen can generate code to programmatically initialize your view controllers from your storyboards. Docs
// You can instantiate scenes using the `instantiate` method:
let vc = StoryboardScene.Dependency.dependent.instantiate()
Upvotes: 0
Reputation: 1029
Adding to @Chikabuz answer, I could add his snippet to something like this
extension UIViewController
{
class func instantiateFromStoryboard(_ name: String = "Main", identifier: String) -> Self
{
return instantiateFromStoryboardHelper(name, identifier: identifier)
}
fileprivate class func instantiateFromStoryboardHelper<T>(_ name: String, identifier: String) -> T
{
let storyboard = UIStoryboard(name: name, bundle: nil)
let controller = storyboard.instantiateViewController(withIdentifier: identifier) as! T
return controller
}
}
And then, in my table view controller, something like this (i'm trying to automate each cell's row to its own vc):
struct MenuItem {
let title: String
let subtitle: String
let `class`: AnyClass
}
class MenuViewController: UITableViewController {
private var menu: [MenuItem] = [
MenuItem(title: "Some View",
subtitle: "The good old description",
class: FirstViewController.self),
MenuItem(title: "Another View",
subtitle: "Demo of this view",
class: SecondViewController.self)
]
...
override func tableView(_ tableView: UITableView, didSelectRowAt indexPath:
IndexPath) {
let item = menu[indexPath.row]
let vcClass = item.class as! UIViewController.Type
let vc = vcClass.instantiateFromStoryboard(identifier: String(describing: item.class))
self.navigationController?.pushViewController(vc, animated: true)
tableView.deselectRow(at: indexPath, animated: true)
}
}
and there you go, magic!
Your storyboard doesn't have to be exactly the same as vc's file name but default to Main
.
PS: If you got some crashes, it's probably that you forgot to add identifier
that must be exactly the same as your vc name,... or that vc is not inside Main.storyboard
Upvotes: 0
Reputation: 13296
You can change the return type to be Self
which will match the type you are calling the method on.
This is a method I've used to do this. It will need to be put into a protocol extension instead.
static func loadFromStoryboard() -> Self? {
let storyboard = UIStoryboard(name: NSStringFromClass(self),
bundle: Bundle(for: self))
return storyboard.instantiateInitialViewController() as? Self
}
Upvotes: 0
Reputation: 272770
I assume that you don't want to make every one of your VCs conform to a protocol manually. That would be too much work :)
I haven't tested this but this should work:
protocol Initializable {
static func initalize() -> Self?
}
extension UIViewController: Initializable {
static func initalize() -> Self? {
let name = NSStringFromClass(self as! AnyClass)
let storyboard = UIStoryboard(name: name, bundle: nil)
return storyboard.getInitialVC(type: self)
}
}
extension UIStoryboard {
func getInitialVC<T: UIViewController>(type: T.Type) -> T? {
return instantiateInitialViewController() as? T
}
}
Upvotes: 1
Reputation: 10205
I use this extension:
extension UIViewController
{
class func instantiateFromStoryboard(_ name: String = "Main") -> Self
{
return instantiateFromStoryboardHelper(name)
}
fileprivate class func instantiateFromStoryboardHelper<T>(_ name: String) -> T
{
let storyboard = UIStoryboard(name: name, bundle: nil)
let controller = storyboard.instantiateViewController(withIdentifier: String(describing: self)) as! T
return controller
}
}
Usage:
let controller = MyViewController.instantiateFromStoryboard()
Upvotes: 8