Link Storyboard to UIViewController with Generic

Storyboards have a problem with having a generic class. The thing is that Interface Builder communicates to the ViewController through the Objective-C runtime. Because of this, InterfaceBuilder is limited to the features that Objective-C provides. In this case, generics are not supported.

A workaround for this is using the .load() method of NSObject.

For example, if you have the mentioned ViewController class:

class APIModelDetailsVC<T where T: APIModel>: UIViewController {...}

You should create a "dummy" ViewController such as:

class StartWasModelDetailsVC: APIModelDetailsVC<StarWarsPerson> {...}

and set this last ViewController in the storyboard. Afterwards, in order to make this work in the Objective-c runtime, you should add the following in your AppDelegate or somewhere before this controller is loaded.

StartWasModelDetailsVC.load()

Hope it helps!


I just ran into the same problem and came up with another workaround:

I create my generic UIViewController in code only. In IB I then create a UIView in a XIB file that has all the controls I need along with a class called "mainView" that has all the outlets.

In viewDidLoad() of the generic UIViewController I then load the view from the XIB, wire it up and insert it into the view controller's main view like this:

class RSListViewController<T: RSObjectProtocol>: UIViewController, UITableViewDelegate {

override func viewDidLoad() {

    super.viewDidLoad()

    guard let mainView = Bundle.main.loadNibNamed("mainView", owner: self, options: nil)?.first as? MainView else { return }

    self.mainView = mainView
    self.mainView.tableView.delegate = self
    self.mainView.segControl.addTarget(self, action: #selector(RSListViewController.listTypeChanged(_:)), for: .valueChanged)

    self.view.addSubview(mainView)

}

The only caveat that I can see so far is that you cannot address the outlets directly like

self.buttonAdd

but need to go through the view like

self.mainView.buttonAdd

Instead of actions I wire the the events through code with the addTarget like in the code sample above.

It's kind of a mix if you don't want to do "code only" but in my case it seems to work fine. And it does prevent the creation of many dummy ViewControllers which in my view kind of defies the idea of generics. But this is just a matter of personal taste and does not mean to criticize in any way Federico Ojeda's solution.


There is another really nice option for this problem.

My solution was inspired by: List all subclasses of one class

Generic UIViewcontrollers work in storyboards provided you register them in the Obj-C runtime before using them. By using a generic base we can make this work. All UIViewControllers are NSObjects, because of this we can retrieve all sublcasses. Therefore we can autoregister all subclasses of UIViewController, even generic ones, and automatically call load() on them in the AppDelegate.

public func address(of object: Any?) -> UnsafeMutableRawPointer {
    return Unmanaged.passUnretained(object as AnyObject).toOpaque()
}

public func subclasses<T>(of theClass: T) -> [T] {
    var count: UInt32 = 0, result: [T] = []
    let allClasses = objc_copyClassList(&count)!
    let classPtr = address(of: theClass)

    for n in 0 ..< count {
        let someClass: AnyClass = allClasses[Int(n)]
        guard let someSuperClass = class_getSuperclass(someClass), address(of: someSuperClass) == classPtr else { continue }
        result.append(someClass as! T)
    }

    return result
}

After this in applicationDidFinishLaunchingWithOptions we can simply do this:

    for subclass in subclasses(of: UIViewController.self) {
        subclass.load()
    }

Say we have the following viewcontrollers:

class BaseViewController<CompletionResult> : UIViewController {
    var onComplete: ((CompletionResult) -> ())?


}

class TestViewController : BaseViewController<String> {


    override func viewDidLoad() {
        super.viewDidLoad()

        self.onComplete = {
            value in
            print(value)
        }

        onComplete?("TEST")
    }


}

We can now safely use TestViewController inside a storyboard!