Swift IBInspectable didSet 与 get/set

Sai*_*aik 2 swift ibdesignable ibinspectable

我对IBDesignables和比较陌生IBInspectable,我注意到很多教程都IBInspectable以这种方式使用。

@IBInspectable var buttonBorderWidth: CGFloat = 1.0 {
    didSet {
        updateView()
    }
}

func updateView() {
    // Usually there are more entries here for each IBInspectable
    self.layer.borderWidth = buttonBorderWidth
}
Run Code Online (Sandbox Code Playgroud)

但在某些情况下,他们会像这样使用 get 和 set

@IBInspectable
var shadowOpacity: Float {
    get {
        return layer.shadowOpacity
    }
    set {
        layer.shadowOpacity = newValue
    }
}
Run Code Online (Sandbox Code Playgroud)

有人可以解释一下:每种情况下发生了什么以及如何选择使用哪一种?

rob*_*off 5

我看到两个问题。第一个是 \xe2\x80\x9c 在每种情况下发生的情况\xe2\x80\x9d,最好的答案是阅读The Swift的 \xe2\x80\x9cProperties\xe2\x80\x9d\xc2\xa0chapter编程语言。还已经发布了其他三个答案来解决第一个问题,但没有一个答案回答第二个更有趣的问题。

\n\n

第二个问题是\xe2\x80\x9chow选择使用哪一个\xe2\x80\x9d。

\n\n

您的示例(这是一个计算属性)比您的示例(这是一个带有观察者的存储属性)shadowOpacity具有以下优点:buttonBorderWidth

\n\n
    \n
  • 所有shadowOpacity相关代码都位于一处,因此更容易理解其工作原理。代码buttonBorderWidth分布在didSet和之间updateViews。在真实的程序中,这些函数更有可能相距较远,正如您所说,\xe2\x80\x9c通常每个 IBInspectable\xe2\x80\x9d 这里有更多条目。这使得查找和理解涉及实现的所有代码变得更加困难buttonBorderWidth

  • \n
  • 由于视图的shadowOpacity属性 getter 和 setter 只是转发到图层的属性,因此视图的属性不会在视图的内存布局中占用任何额外的空间。视图buttonBorderWidth作为存储的属性,确实在视图的内存布局中占用了额外的空间。

  • \n
\n\n

此处分离有一个优点updateViews,但很微妙。请注意,buttonBorderWidth默认值为 1.0。这与 的默认值layer.borderWidth(0)不同。我们需要以某种方式在视图初始化时进行layer.borderWidth匹配,即使从未被修改过。由于设置的代码位于 in 中,因此我们可以确保在显示视图之前的某个时刻调用(例如 in或 in或 in )。buttonBorderWidthbuttonBorderWidthlayer.borderWidthupdateViewsupdateViewsinitlayoutSubviewswillMove(toWindow:)

\n\n

如果我们想让buttonBorderWidthbe 成为一个计算属性,我们要么必须在某处强制设置buttonBorderWidth为现有值,要么复制在某处设置的代码layer.borderWidth。也就是说,我们要么必须做这样的事情:

\n\n
init(frame: CGRect) {\n    ...\n\n    super.init(frame: frame)\n\n    // This is cumbersome because:\n    // - init won\'t call buttonBorderWidth.didSet by default.\n    // - You can\'t assign a property to itself, e.g. `a = a` is banned.\n    // - Without the semicolon, the closure is treated as a trailing\n    //   closure on the above call to super.init().\n    ;{ buttonBorderWidth = { buttonBorderWidth }() }()\n}\n
Run Code Online (Sandbox Code Playgroud)\n\n

或者我们必须做这样的事情:

\n\n
init(frame: CGRect) {\n    ...\n\n    super.init(frame: frame)\n\n    // This is the same code as in buttonBorderWidth.didSet:\n    layer.borderWidth = buttonBorderWidth\n}\n
Run Code Online (Sandbox Code Playgroud)\n\n

如果我们有一堆覆盖图层属性但具有不同默认值的属性,我们必须对每个属性进行强制设置或复制。

\n\n

我对此的解决方案通常是不要为我的可检查属性设置与其所覆盖的属性不同的默认值。如果我们只是让 的默认值为buttonBorderWidth0(与 的默认值相同layer.borderWidth),那么我们就不必使这两个属性同步,因为它们永远不会不同步。所以我就buttonBorderWidth这样实现:

\n\n
@IBInspectable var buttonBorderWidth: CGFloat {\n    get { return layer.borderWidth }\n    set { layer.borderWidth = newValue }\n}\n
Run Code Online (Sandbox Code Playgroud)\n\n

那么,您什么时候想将存储的属性与观察者一起使用呢?特别适用的一种情况IBInspectable是可检查属性没有简单地映射到现有图层属性上。

\n\n

例如,在 iOS 11 和 macOS 10.13 及更高版本中,CALayer有一个maskedCorners属性控制哪些角被圆化cornerRadius。假设我们想将cornerRadius和公开maskedCorners为可检查属性。我们不妨只cornerRadius使用计算属性来公开:

\n\n
@IBInspectable var cornerRadius: CGFloat {\n    get { return layer.cornerRadius }\n    set { layer.cornerRadius = newValue }\n}\n
Run Code Online (Sandbox Code Playgroud)\n\n

maskedCorners本质上是四种不同的布尔属性合二为一。因此,我们应该将其公开为四个单独的可检查属性。如果我们使用计算属性,它看起来像这样:

\n\n
@IBInspectable var isTopLeftCornerRounded: Bool {\n    get { return layer.maskedCorners.contains(.layerMinXMinYCorner) }\n    set {\n        if newValue { layer.maskedCorners.insert(.layerMinXMinYCorner) }\n        else { layer.maskedCorners.remove(.layerMinXMinYCorner) }\n    }\n}\n\n@IBInspectable var isBottomLeftCornerRounded: Bool {\n    get { return layer.maskedCorners.contains(.layerMinXMaxYCorner) }\n    set {\n        if newValue { layer.maskedCorners.insert(.layerMinXMaxYCorner) }\n        else { layer.maskedCorners.remove(.layerMinXMaxYCorner) }\n    }\n}\n\n@IBInspectable var isTopRightCornerRounded: Bool {\n    get { return layer.maskedCorners.contains(.layerMaxXMinYCorner) }\n    set {\n        if newValue { layer.maskedCorners.insert(.layerMaxXMinYCorner) }\n        else { layer.maskedCorners.remove(.layerMaxXMinYCorner) }\n    }\n}\n\n@IBInspectable var isBottomRightCornerRounded: Bool {\n    get { return layer.maskedCorners.contains(.layerMaxXMaxYCorner) }\n    set {\n        if newValue { layer.maskedCorners.insert(.layerMaxXMaxYCorner) }\n        else { layer.maskedCorners.remove(.layerMaxXMaxYCorner) }\n    }\n}\n
Run Code Online (Sandbox Code Playgroud)\n\n

这是一堆重复的代码。如果您使用复制和粘贴来编写内容,很容易错过某些内容。(我不保证我得到的结果是正确的!)现在让我们看看使用观察者的存储属性会是什么样子:

\n\n
@IBInspectable var isTopLeftCornerRounded = true {\n    didSet { updateMaskedCorners() }\n}\n\n@IBInspectable var isBottomLeftCornerRounded = true {\n    didSet { updateMaskedCorners() }\n}\n\n@IBInspectable var isTopRightCornerRounded = true {\n    didSet { updateMaskedCorners() }\n}\n\n@IBInspectable var isBottomRightCornerRounded = true {\n    didSet { updateMaskedCorners() }\n}\n\nprivate func updateMaskedCorners() {\n    var mask: CACornerMask = []\n    if isTopLeftCornerRounded { mask.insert(.layerMinXMinYCorner) }\n    if isBottomLeftCornerRounded { mask.insert(.layerMinXMaxYCorner) }\n    if isTopRightCornerRounded { mask.insert(.layerMaxXMinYCorner) }\n    if isBottomRightCornerRounded { mask.insert(.layerMaxXMaxYCorner) }\n    layer.maskedCorners = mask\n}\n
Run Code Online (Sandbox Code Playgroud)\n\n

我认为这个具有存储属性的版本比具有计算属性的版本有几个优点:

\n\n
    \n
  • 代码中重复的部分要短得多。
  • \n
  • 每个掩码选项仅提及一次,因此更容易确保选项全部正确。
  • \n
  • 实际计算掩码的所有代码都位于一处。
  • \n
  • 掩码每次都是完全从头开始构建的,因此您不必知道掩码的先前值即可了解其新值。
  • \n
\n\n

这是我使用存储属性的另一个示例:假设您想要创建 aPolygonView并使边数可检查。我们需要代码来创建给定边数的路径,所以这里是:

\n\n
extension CGPath {\n    static func polygon(in rect: CGRect, withSideCount sideCount: Int) -> CGPath {\n        let path = CGMutablePath()\n        guard sideCount >= 3 else {\n            return path\n        }\n\n        // It\'s easiest to compute the vertices of a polygon inscribed in the unit circle.\n        // So I\'ll do that, and use this transform to inscribe the polygon in `rect` instead.\n        let transform = CGAffineTransform.identity\n            .translatedBy(x: rect.minX, y: rect.minY) // translate to the rect\'s origin\n            .scaledBy(x: rect.width, y: rect.height) // scale up to the rect\'s size\n            .scaledBy(x: 0.5, y: 0.5) // unit circle fills a 2x2 box but we want a 1x1 box\n            .translatedBy(x: 1, y: 1) // lower left of unit circle\'s box is at (-1, -1) but we want it at (0, 0)\n\n        path.move(to: CGPoint(x: 1, y: 0), transform: transform)\n        for i in 1 ..< sideCount {\n            let angle = CGFloat(i) / CGFloat(sideCount) * 2 * CGFloat.pi\n            print("\\(i) \\(angle)")\n            path.addLine(to: CGPoint(x: cos(angle), y: sin(angle)), transform: transform)\n        }\n        path.closeSubpath()\n\n        print("rect=\\(rect) path=\\(path.boundingBox)")\n        return path\n    }\n}\n
Run Code Online (Sandbox Code Playgroud)\n\n

我们可以编写接受 aCGPath并计算它绘制的线段数的代码,但直接存储边数更简单。因此,在这种情况下,将存储的属性与观察者一起使用来触发图层路径的更新是有意义的:

\n\n
class PolygonView: UIView {\n\n    override class var layerClass: AnyClass { return CAShapeLayer.self }\n\n    @IBInspectable var sideCount: Int = 3 {\n        didSet {\n            setNeedsLayout()\n        }\n    }\n\n    override func layoutSubviews() {\n        super.layoutSubviews()\n\n        (layer as! CAShapeLayer).path = CGPath.polygon(in: bounds, withSideCount: sideCount)\n    }\n\n}\n
Run Code Online (Sandbox Code Playgroud)\n\n

我更新了路径,layoutSubviews因为如果视图的大小发生变化,我还需要更新路径,并且大小变化也会触发layoutSubviews.

\n