All Classes Conforming to Protocol Inherit Default

2019-07-15 06:49发布

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.

2条回答
啃猪蹄的小仙女
2楼-- · 2019-07-15 07:27

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


  1. 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.

  2. 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.

查看更多
老娘就宠你
3楼-- · 2019-07-15 07:42

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 {
  ....
查看更多
登录 后发表回答