I've added a method to all my UIViewController subclasses that allows me to instantiate it from the class and the storyboard it's inside.
All the methods follow this format:
class func instantiateFromStoryboard() -> CameraViewController? {
let storyboard = UIStoryboard(name: "Camera", bundle: nil)
let initial = storyboard.instantiateInitialViewController()
guard let controller = initial as? CameraViewController else {
return nil
}
return controller
}
Instead, I would like to make a protocol, Instantiatable
, that requires the above method along with a variable, storyboardName: String
.
Then, I'd like to extend this Instantiatable
so it contains a similar implementation as above. My objective is that I can state that a UIViewController
adheres to this protocol, and all that I have to define is the storyboardName
.
I feel like I'm close with this implementation:
protocol Instantiatable {
var storyboardName: String { get }
func instantiateFromStoryboard() -> Self?
}
extension Instantiatable where Self: UIViewController {
func instantiateFromStoryboard() -> Self? {
let storyboard = UIStoryboard(name: storyboardName, bundle: nil)
let initial = storyboard.instantiateInitialViewController()
guard let controller = initial as? Self else {
return nil
}
return controller
}
}
However, when I try to add conformance to CameraViewController
, I get the error:
Method instantiateFromStoryboard()
in non-final class CameraViewController
must return Self
to conform to protocol Instantiatable
What am I missing?
Thanks.
Add final
The solution here was just to add final
to the subclassed UIViewController
(in my example, it was CameraViewController
).
This allows your call site to correctly infer the type of the UIViewController
without casting. In my example, the call site is:
guard let controller = CameraViewController.instantiate() else {
return
}
But, why?
Why does adding that final keyword matter?
After discussing with @AliSoftware, he explained the need for final
. (He also added a similar protocol into the Swift mixin repository, Reusable.)
The compiler cares if your custom VC is final
to ensure that the Self
requirement Instantiatable
mentions can be statically inferred or not.
In an example:
class ParentVC: UIViewController, Instantiatable {
// Because of Instantiatable, this class automatically has a method
// with this signature:
func instantiate() -> ParentVC // here, the "Self" coming from the Protocol, meaning "the conforming class", which is solved as "ParentVC"
}
class ChildVC: ParentVC {
// Error: It inherits from ParentVC, and has to conform
// to Instantiatable as a Parent
func instantiate() -> ParentVC
// but, it also has this from being a Instantiatable object itself
func instantiate() -> ChildVC
// thus, the compiler cannot solve what "Self" should resolve to here,
// either ParentVC or ChildVC.
//
// Also, it can generate problems in various contexts being "of
// both types" here, which is why it's not permitted
}
Why adding final is ok
Either your custom VC is directly inheriting from UIViewController
, but doesn't need to be subclassed any further, so you should mark it final
anyway.
Or, you are creating a parent abstract CommonVC
. You intend to make multiple children (class CustomVC1: CommonVC
, class CustomVC2: CommonVC
), inherit from it, but in that case CommonVC
is abstract and likely not to be instantiated directly. Therefore, it's not the one to be marked as Instantiatable
, and you should mark the CustomVC1
etc. you intend to instantiate as final
+ Instantiatable
instead.
You can use generics to achieve what you are after. Something like this:
protocol Instantiatable {
static func instantiateFromStoryboard<T: UIViewController>() -> T?
}
extension Instantiatable where Self: UIViewController {
static func instantiateFromStoryboard<T: UIViewController>() -> T? {
let storyboard = UIStoryboard(name: self.description(), bundle: nil)
let initial = storyboard.instantiateInitialViewController() as? T
guard let _ = initial as? Self else {
return nil
}
return initial
}
}
So, if VCA is instantiatable
you can say let vCA = VCA.InstantiateFromStoryboard()
I changed the function to be a class function, so that you can call it on the class, rather than needing an instance of the view controller. This code uses the class name to retrieve the storyboard file, but this means that your storyboard needs to be called projectname.classname.storyboard, which is a bit ugly.
Another approach is to require your view controller classes to implement a function that returns the name of the storyboard:
protocol Instantiatable {
// var storyboardName: String { get }
static func instantiateFromStoryboard<T: UIViewController>() -> T?
static func storyboardName() -> String
}
extension Instantiatable where Self: UIViewController {
static func instantiateFromStoryboard<T: UIViewController>() -> T? {
let storyboard = UIStoryboard(name: self.storyboardName(), bundle: nil)
let initial = storyboard.instantiateInitialViewController() as? T
guard let _ = initial as? Self else {
return nil
}
return initial
}
}
Then in each Instantiatable
you would need to implement:
static func storyboardName() -> String {
return "ViewController" // (or whatever the storyboard name is)
}
EDIT
The third (and probably best) alternative is to make your UIViewController
subclasses final
, as per your answer and @AliSoftware's comments.
This lets you use
protocol Instantiatable {
// var storyboardName: String { get }
static func instantiateFromStoryboard() -> Self?
static func storyboardName() -> String
}
extension Instantiatable where Self: UIViewController {
static func instantiateFromStoryboard() -> Self? {
let storyboard = UIStoryboard(name: self.storyboardName(), bundle: nil)
let initial = storyboard.instantiateInitialViewController() as? Self
return initial
}
}
as long as you declare your view controllers to be:
final class ViewController: UIViewController, Instantiatable {
....