【问题标题】:All Classes Conforming to Protocol Inherit Default Implementation所有符合协议的类继承默认实现
【发布时间】:2016-07-09 23:45:49
【问题描述】:

我为我的所有 UIViewController 子类添加了一个方法,允许我从类和它所在的情节提要中实例化它。

所有方法都遵循这种格式:

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
}

相反,我想创建一个协议Instantiatable,它需要上述方法以及一个变量storyboardName: String

然后,我想扩展此Instantiatable,使其包含与上述类似的实现。我的目标是我可以声明 UIViewController 遵守此协议,而我所要定义的只是 storyboardName

我觉得我已经接近这个实现了:

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
    }
}

但是,当我尝试添加对 CameraViewController 的一致性时,我收到了错误:

非final类CameraViewController中的方法instantiateFromStoryboard()必须返回Self以符合协议Instantiatable

我错过了什么?

谢谢。

【问题讨论】:

  • 为什么你声明你的函数返回 Self 而不是 CameraViewController ?如果你希望它是通用的,那么它应该返回 UIViewController?
  • 抱歉,我将对其进行更明确的编辑。我希望此方法返回符合 Instantiatable 协议的 UIViewController 的任何子类。
  • 然后你可以声明它返回Instantiatable?或者UIViewController?
  • 太棒了。我希望返回的对象与子类相同,而不是UIViewController?。我试过Instantiatable? 并得到错误:在类型'Self'上使用实例成员'instantiateFromStoryboard';您的意思是改用“Self”类型的值吗?
  • 您将遇到的另一个问题是您正在添加一个实例函数,因此您已经需要一个您想要的视图控制器实例才能调用instantiateFromStoryboard,所以您会不得不说像let camVC = CameraViewController(); camVC.storyboard = "somestoryboard"; let realCamVC = camVC.instantiateFromStoryboard() 这样的话——这比它需要的要复杂得多。如果每个故事板只需要一个 VC,则可以使用 nib 文件。

标签: ios swift protocols swift-protocols


【解决方案1】:

添加final


这里的解决方案只是将final 添加到子类UIViewController(在我的示例中,它是CameraViewController)。

这允许您的调用站点正确推断 UIViewController 的类型,而无需强制转换。在我的示例中,调用站点是:

guard let controller = CameraViewController.instantiate() else {
    return
}

但是,为什么?


为什么添加 final 关键字很重要?

@AliSoftware讨论后,他解释了final的必要性。 (他还在 Swift mixin 存储库中添加了一个类似的协议,Reusable。)

编译器会关心您的自定义 VC 是否为 final,以确保 Self 要求 Instantiatable 提及是否可以静态推断。

举个例子:

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
}

为什么添加final是可以的


  1. 您的自定义 VC 直接继承自 UIViewController,但不需要进一步子类化,因此无论如何您都应该将其标记为 final

  2. 或者,您正在创建父摘要CommonVC。您打算创建多个孩子(class CustomVC1: CommonVCclass CustomVC2: CommonVC),从它继承,但在这种情况下,CommonVC 是抽象的,可能不会直接实例化。因此,不是要标记为Instantiatable,而是应将要实例化的CustomVC1等标记为final + Instantiatable

【讨论】:

    【解决方案2】:

    您可以使用泛型来实现您所追求的。像这样的:

    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
        }
    }
    

    所以,如果 VCA 是 instantiatable,你可以说 let vCA = VCA.InstantiateFromStoryboard()

    我将函数更改为类函数,以便您可以在类上调用它,而不需要视图控制器的实例。这段代码使用类名来检索storyboard文件,但这意味着你的storyboard需要被命名为projectname.classname.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
        }
    }
    

    然后在每个Instantiatable 中你需要实现:

    static func storyboardName() -> String {
        return "ViewController" // (or whatever the storyboard name is)
    }
    

    编辑

    根据您的回答和@AliSoftware 的cmets,第三个(可能是最好的)替代方法是使您的UIViewController 子类final

    这让你可以使用

    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
        }
    }
    

    只要你声明你的视图控制器是:

    final class ViewController: UIViewController, Instantiatable {
      ....
    

    【讨论】:

    • 这太棒了!剩下的一个问题是我仍然返回 UIViewController 而不是子类版本。因此,当我调用它时,我必须这样做 let controller = CameraViewController.instantiateFromStoryboard() as!以 CameraViewController 为例。是否有可能摆脱“as!” ?
    • 不,你不能因为里氏替换原则。如果您让每个视图控制器子类实现该功能而不是使用扩展,那么您可以避免向下转换。
    • 我在这里学到了很多,保罗,谢谢!我发布了我收到的解决方案,这正是我正在寻找的。非常感谢您的帮助。
    • 你的评论是完全正确的@Paulw11 ? 解决方案是摆脱 LSP 约束......通过创建类 final ? 这实际上在 @HunterMaximillionMonk 的案例中完全有意义 - 因为您实例化的 VC 通常从不子类化(如果是,它们通常是抽象的,并且您可以使子类成为最终的并符合协议而不是父类) - 作为奖励,它提供了额外的编译器优化,所以这是一个胜利方式! — 更多解释:github.com/AliSoftware/Reusable#tip-make-your-subclasses-final
    猜你喜欢
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 2013-02-07
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    相关资源
    最近更新 更多