Paulo Andrade

Keeper of Secrets.

twitter github stackoverflow linkedin email
Generic View Controllers with Storyboards
Feb 5, 2021
2 minutes read

Trying to use storyboards with generic view controllers as always been problematic. The problem is the storyboard also encodes the class of the view controller for each “scene”. But you don’t know the class beforehand since it can it’s a generic class and can be parameterised with various types. For example, take this class:

class Foo<V: Equatable>: UIViewController {
    // outlets, actions, business logic
}

What would write for “Custom Class” in the storyboard? Now you see the problem.

You could make a non-generic superclass and use that in the storyboard. You’d make all outlet and action connections on the superclass but leave the business logic on the subclass:

class StoryboardFoo: UIViewController {
    // outlets and actions
}
class Foo<V: Equatable>: StoryboardFoo {
    // business logic
}

This sounds like it should work. But there’s a problem… when calling UIStoryboard’s instantiateViewController(withIdentifier:) it will still create an instance of StoryboardFoo because that what’s specified on the storyboard!

Well, iOS 13 introduced a couple of new methods on UIStoryboard to instantiate view controllers that give us access to the underlying NSCoder object. This not also allows us to initialise the view controller with required parameters but use generics as well.

We can change the code above to use one of these new methods and instantiate our generic subclass instead of the one specified on the storyboard.

class StoryboardFoo: UIViewController {
    // outlets and actions
}
class Foo<V: Equatable>: StoryboardFoo {
    class func instantiate(param: V) -> Foo<V> {
        let storyboard = UIStoryboard(name: "Example", bundle: nil)
        return storyboard.instantiateViewController(identifier: "Foo") {
            Foo(coder: $0, param: param)
        }
    }

    let param: V
    init?(coder: NSCoder, param: V) {
        self.param = param
        super.init(coder: coder)
    }
    
    required init?(coder: NSCoder) { fatalError("use instantiate(param:) instead")}
}

It’s not ideal since you still need to separate outlets and actions into a superclass but I think it’s worth it when a view controller could really be generic.



Back to posts