【问题标题】:How to implement two inits with same content without code duplication in Swift?如何在 Swift 中实现两个具有相同内容且不重复代码的 init?
【发布时间】:2014-06-03 19:29:20
【问题描述】:

假设从UIView 派生的类如下:

class MyView: UIView {
    var myImageView: UIImageView

    init(frame: CGRect) {
        super.init(frame: frame)
    }

    init(coder aDecoder: NSCoder!) {
        super.init(coder: aDecoder)
    }

    ...

如果我想在两个初始化器中使用相同的代码,比如

self.myImageView = UIImageView(frame: CGRectZero)
self.myImageView.contentMode = UIViewContentMode.ScaleAspectFill

并且不要在类实现中重复该代码两次,我将如何构造 init 方法?

尝试过的方法:

  • 创建了一个在super.init 之后调用的方法func commonInit() -> Swift 编译器在调用super.init 之前给出了一个关于未初始化变量myImageView 的错误
  • super.init 之前调用func commonInit() 显然会失败,并出现编译器错误“'self' used before super.init call”

【问题讨论】:

  • 我手边没有,但是 swift book 中有关于这个的讨论。搜索指定的初始化或初始化程序,您应该会找到它。
  • 题目大概叫“类继承与初始化”。
  • 这无疑是 Swift 中最烦人的事情之一。我在第一天就遇到了一个问题,并且一直困扰着我。

标签: ios swift


【解决方案1】:

我们需要的是在调用任何超类的初始化程序之前放置我们的初始化代码的公共位置,所以我目前使用的是,如下面的代码所示。 (它还涵盖了默认值之间相互依赖的情况并保持它们不变。)

import UIKit

class MyView: UIView {
        let value1: Int
        let value2: Int

        enum InitMethod {
                case coder(NSCoder)
                case frame(CGRect)
        }

        override convenience init(frame: CGRect) {
                self.init(.frame(frame))!
        }

        required convenience init?(coder aDecoder: NSCoder) {
                self.init(.coder(aDecoder))
        }

        private init?(_ initMethod: InitMethod) {
                value1 = 1
                value2 = value1 * 2 //interdependence among defaults

                switch initMethod {
                case let .coder(coder): super.init(coder: coder)
                case let .frame(frame): super.init(frame: frame)
                }
        }
}

【讨论】:

  • 非常漂亮和干净。唯一保留let 属性并将属性初始化和进一步设置保持在一起的解决方案。您可以通过删除myImageViewInitMethod.Default 并将InitMethod 移动到MyView 来进一步清理它以更好地说明解决方案。
  • 无可选/IUO/var。这太棒了。
  • 不错!但由于this issue,它不适用于 Xcode 10.2。我添加了另一个包含解决方法的答案。
  • 除了 Xcode 10.2 问题,enum InitMethod 可以标记为private
【解决方案2】:

我刚收到same problem

正如 GoZoner 所说,将变量标记为可选将起作用。这不是一种非常优雅的方式,因为您每次想要访问它时都必须解包该值。

我将向 Apple 提交增强请求,也许我们可以获得类似“beforeInit”的方法,该方法在每次初始化之前调用,我们可以在其中分配变量,因此我们不必使用可选变量。

在那之前,我将把所有的赋值放到一个 commonInit 方法中,该方法从专用的初始化程序中调用。例如:

class GradientView: UIView {
    var gradientLayer: CAGradientLayer?  // marked as optional, so it does not have to be assigned before super.init
    
    func commonInit() {
        gradientLayer = CAGradientLayer()
        gradientLayer!.frame = self.bounds
        // more setup 
    }
     
    init(coder aDecoder: NSCoder!)  {
        super.init(coder: aDecoder)
        commonInit()
    }
    
     init(frame: CGRect) {
        super.init(frame: frame)
        commonInit()
    }

    override func layoutSubviews() {
        super.layoutSubviews()
        gradientLayer!.frame = self.bounds  // unwrap explicitly because the var is marked optional
    }
}

感谢 David,我再次查看了这本书,我发现了一些可能对我们的重复数据删除工作有所帮助的内容,而无需使用可选的变量 hack。可以使用闭包来初始化变量。

使用闭包或函数设置默认属性值

如果存储属性的默认值需要一些自定义或设置,您可以使用闭包或全局函数为该属性提供自定义默认值。每当初始化属性所属类型的新实例时,就会调用闭包或函数,并将其返回值分配为属性的默认值。这些类型的闭包或函数通常会创建一个与属性相同类型的临时值,修改该值以表示所需的初始状态,然后返回该临时值以用作属性的默认值。

以下是如何使用闭包提供默认属性值的概要:

class SomeClass {
let someProperty: SomeType = {
    // create a default value for someProperty inside this closure
    // someValue must be of the same type as SomeType
    return someValue
    }() 
}

请注意,闭包的结尾大括号后面是一对空括号。这告诉 Swift 立即执行闭包。如果你省略这些括号,你试图将闭包本身分配给属性,而不是闭包的返回值。

注意

如果您使用闭包来初始化属性,请记住实例的其余部分在执行闭包时尚未初始化。这意味着您不能从闭包中访问任何其他属性值,即使这些属性具有默认值。您也不能使用隐式 self 属性,或调用任何实例的方法。

摘自:Apple Inc.“Swift 编程语言”。电子书。 https://itun.es/de/jEUH0.l

这是我从现在开始使用的方式,因为它不会规避不允许在变量上使用 nil 的有用特性。对于我的示例,它看起来像这样:

class GradientView: UIView {
    var gradientLayer: CAGradientLayer = {
        return CAGradientLayer()
    }()
    
    func commonInit() {
        gradientLayer.frame = self.bounds
        /* more setup */
    }
    
    init(coder aDecoder: NSCoder!)  {
        super.init(coder: aDecoder)
        commonInit()
    }
    
    init(frame: CGRect) {
        super.init(frame: frame)
        commonInit()
    }
}

【讨论】:

  • 所以如果我理解您的出色解释,您可以在使用闭包完成初始化时省略 @lazy 关键字?
  • 闭包与commonInit() 的结合正是我想要的,谢谢。或者只是使用更短的形式:var gradientLayer: CAGradientLayer = CAGradientLayer()(或者更短的var gradientLayer = CAGradientLayer()),如果你只想分配对象。
【解决方案3】:

这个怎么样?

public class MyView : UIView
{
    var myImageView: UIImageView = UIImageView()

    private func setup()
    {
        myImageView.contentMode = UIViewContentMode.ScaleAspectFill
    }

    override public init(frame: CGRect)
    {
        super.init(frame: frame)
        setup()
    }

    required public init(coder aDecoder: NSCoder)
    {
        super.init(coder: aDecoder)
        setup()
    }
}

【讨论】:

【解决方案4】:

它一定要在前面吗?我认为这是隐式展开的可选项可用于的事情之一:

class MyView: UIView {
    var myImageView: UIImageView!

    init(frame: CGRect) {
        super.init(frame: frame)
        self.commonInit()
    }

    init(coder aDecoder: NSCoder!) {
        super.init(coder: aDecoder)
        self.commonInit()
    }

    func commonInit() {
        self.myImageView = UIImageView(frame: CGRectZero)
        self.myImageView.contentMode = UIViewContentMode.ScaleAspectFill
    }

    ...
}

隐式展开的选项允许您在调用 super 之前跳过变量赋值。但是,您仍然可以像普通变量一样访问它们:

var image: UIImageView = self.myImageView // no error

【讨论】:

  • 不幸的是,隐式展开的可选项扼杀了无法将 nil 或可选项分配给原本不应该是可选的变量的好处。
  • 嗯,好点子。但是 IIRC,这样的事情是 Swift 书中给出的示例之一。
  • 如果代码演变并且有人忘记初始化属性,IUO 也可能导致意外崩溃。
【解决方案5】:

另一个使用静态方法的选项(添加“otherView”以突出显示可伸缩性)

class MyView: UIView {

    var myImageView: UIImageView
    var otherView: UIView

    override init(frame: CGRect) {
        (myImageView,otherView) = MyView.commonInit()
        super.init(frame: frame)
    }

    required init(coder aDecoder: NSCoder) {
        (myImageView, otherView) = MyView.commonInit()
        super.init(coder: aDecoder)!
    }

    private static func commonInit() -> (UIImageView, UIView) {
        //do whatever initialization stuff is required here
        let someImageView = UIImageView(frame: CGRectZero)
        someImageView.contentMode = UIViewContentMode.ScaleAspectFill
        let someView = UIView(frame: CGRect(x: 0, y: 0, width: 30, height: 30))
        return (someImageView, someView)
    }
}

【讨论】:

    【解决方案6】:

    在基于单个图像创建函数的init() 方法中分配myImageView。因此:

    self.myImageView = self.createMyImageView ();
    

    例如,像这样:

    class Bar : Foo {
        var x : Int?
        func createX () -> Int { return 1 }
        init () {
            super.init ()
            self.x = self.createX ()
        }   
    }
    

    注意Int? 处的“可选”用法

    【讨论】:

    • 可选是重要的部分。通过方法分配值是多余的。 self.x = 1 也可以。
    • 重点是定义一个函数createX(),它可以完成多个init()方法所需的复杂工作;这个例子只做了最简单的事情——返回1
    • 我会将 Matthias Bauch 的答案标记为正确,因为将变量标记为可选需要在每次您想要访问它时解开该值。
    【解决方案7】:

    此外,如果打算只分配一次myImageView,它应该是let,而不是var。这排除了一些仅适用于 var 的解决方案。

    另一个复杂因素是多个实例变量之间存在依赖关系。这排除了调用静态方法的内联初始化程序。

    这些要求可以通过覆盖方便初始化器来解决,它委托给一个指定的初始化器:

    import UIKit
    
    class MyView: UIView {
        let myImageView: UIImageView
        // Just to illustrate dependencies...
        let myContainerView: UIView
        
        override convenience init(frame: CGRect) {
            self.init(frame: frame, coder: nil)!
        }
        
        required convenience init?(coder aDecoder: NSCoder) {
            // Dummy value for `frame`
            self.init(frame: CGRect(), coder: aDecoder)
        }
        
        @objc private init?(frame: CGRect, coder aDecoder: NSCoder?) {
            // All `let`s must be assigned before
            // calling `super.init`...
            myImageView = UIImageView(frame: CGRect.zero)
            myImageView.contentMode = .scaleAspectFill
            
            // Just to illustrate dependencies...
            myContainerView = UIView()
            myContainerView.addSubview(myImageView)
            
            if let aDecoderNonNil = aDecoder {
                super.init(coder: aDecoderNonNil)
            } else {
                super.init(frame: frame)
            }
            
            // After calling `super.init`, can safely reference
            // `self` for more common setup...
            self.someMethod()
        }
    
        ...    
    }
    

    这是基于ylin0x81's answer,我非常喜欢但现在不起作用(使用 Xcode 10.2 构建),因为来自 nib 的加载会崩溃:

    This coder requires that replaced objects be returned from initWithCoder:
    

    这个问题在一个单独的问题中讨论,iuriimoz's answer 建议将@objc 添加到指定的初始化程序中。这需要避免 ylin0x81 使用的仅限 Swift 的枚举。

    【讨论】:

      猜你喜欢
      • 1970-01-01
      • 2021-08-05
      • 1970-01-01
      • 2019-09-24
      • 2020-05-10
      • 1970-01-01
      • 1970-01-01
      • 1970-01-01
      • 1970-01-01
      相关资源
      最近更新 更多