【问题标题】:Swift IBInspectable didSet versus get/setSwift IBInspectable didSet 与 get/set
【发布时间】:2018-07-20 21:28:03
【问题描述】:

我对@9​​87654322@ 和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
}

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

@IBInspectable
var shadowOpacity: Float {
    get {
        return layer.shadowOpacity
    }
    set {
        layer.shadowOpacity = newValue
    }
}

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

【问题讨论】:

标签: swift ibdesignable ibinspectable


【解决方案1】:

我看到两个问题。第一个是“在每种情况下发生了什么”,最好阅读the “Properties” chapter of The Swift Programming Language 来回答。已经发布了其他三个回答第一个问题的答案,但没有一个回答第二个更有趣的问题。

第二个问题是“如何选择使用哪一个”。

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

  • 所有与shadowOpacity 相关的代码都在一个地方,因此更容易理解它的工作原理。 buttonBorderWidth 代码分布在 didSetupdateViews 之间。在实际程序中,这些函数更有可能相距更远,正如您所说,“通常每个 IBInspectable 都有更多条目”。这使得查找和理解实现buttonBorderWidth 所涉及的所有代码变得更加困难。

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

这里单独的updateViews 有一个优势,但它很微妙。请注意,buttonBorderWidth 的默认值为 1.0。这与layer.borderWidth 的默认值0 不同。不知何故,我们需要在视图初始化时让layer.borderWidth 匹配buttonBorderWidth,即使buttonBorderWidth 从未被修改过。由于设置layer.borderWidth 的代码在updateViews 中,我们可以确保在视图显示之前的某个时间点调用updateViews(例如在initlayoutSubviewswillMove(toWindow:) 中)。

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

init(frame: CGRect) {
    ...

    super.init(frame: frame)

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

或者我们必须这样做:

init(frame: CGRect) {
    ...

    super.init(frame: frame)

    // This is the same code as in buttonBorderWidth.didSet:
    layer.borderWidth = buttonBorderWidth
}

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

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

@IBInspectable var buttonBorderWidth: CGFloat {
    get { return layer.borderWidth }
    set { layer.borderWidth = newValue }
}

那么,您希望何时将存储的属性与观察者一起使用? 特别适用于IBInspectable 的一个条件是,当可检查的属性不能简单地映射到现有图层属性时。

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

@IBInspectable var cornerRadius: CGFloat {
    get { return layer.cornerRadius }
    set { layer.cornerRadius = newValue }
}

但是maskedCorners 本质上是四个不同的布尔属性组合成一个。所以我们应该将它公开为四个独立的可检查属性。如果我们使用计算属性,它看起来像这样:

@IBInspectable var isTopLeftCornerRounded: Bool {
    get { return layer.maskedCorners.contains(.layerMinXMinYCorner) }
    set {
        if newValue { layer.maskedCorners.insert(.layerMinXMinYCorner) }
        else { layer.maskedCorners.remove(.layerMinXMinYCorner) }
    }
}

@IBInspectable var isBottomLeftCornerRounded: Bool {
    get { return layer.maskedCorners.contains(.layerMinXMaxYCorner) }
    set {
        if newValue { layer.maskedCorners.insert(.layerMinXMaxYCorner) }
        else { layer.maskedCorners.remove(.layerMinXMaxYCorner) }
    }
}

@IBInspectable var isTopRightCornerRounded: Bool {
    get { return layer.maskedCorners.contains(.layerMaxXMinYCorner) }
    set {
        if newValue { layer.maskedCorners.insert(.layerMaxXMinYCorner) }
        else { layer.maskedCorners.remove(.layerMaxXMinYCorner) }
    }
}

@IBInspectable var isBottomRightCornerRounded: Bool {
    get { return layer.maskedCorners.contains(.layerMaxXMaxYCorner) }
    set {
        if newValue { layer.maskedCorners.insert(.layerMaxXMaxYCorner) }
        else { layer.maskedCorners.remove(.layerMaxXMaxYCorner) }
    }
}

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

@IBInspectable var isTopLeftCornerRounded = true {
    didSet { updateMaskedCorners() }
}

@IBInspectable var isBottomLeftCornerRounded = true {
    didSet { updateMaskedCorners() }
}

@IBInspectable var isTopRightCornerRounded = true {
    didSet { updateMaskedCorners() }
}

@IBInspectable var isBottomRightCornerRounded = true {
    didSet { updateMaskedCorners() }
}

private func updateMaskedCorners() {
    var mask: CACornerMask = []
    if isTopLeftCornerRounded { mask.insert(.layerMinXMinYCorner) }
    if isBottomLeftCornerRounded { mask.insert(.layerMinXMaxYCorner) }
    if isTopRightCornerRounded { mask.insert(.layerMaxXMinYCorner) }
    if isBottomRightCornerRounded { mask.insert(.layerMaxXMaxYCorner) }
    layer.maskedCorners = mask
}

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

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

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

extension CGPath {
    static func polygon(in rect: CGRect, withSideCount sideCount: Int) -> CGPath {
        let path = CGMutablePath()
        guard sideCount >= 3 else {
            return path
        }

        // It's easiest to compute the vertices of a polygon inscribed in the unit circle.
        // So I'll do that, and use this transform to inscribe the polygon in `rect` instead.
        let transform = CGAffineTransform.identity
            .translatedBy(x: rect.minX, y: rect.minY) // translate to the rect's origin
            .scaledBy(x: rect.width, y: rect.height) // scale up to the rect's size
            .scaledBy(x: 0.5, y: 0.5) // unit circle fills a 2x2 box but we want a 1x1 box
            .translatedBy(x: 1, y: 1) // lower left of unit circle's box is at (-1, -1) but we want it at (0, 0)

        path.move(to: CGPoint(x: 1, y: 0), transform: transform)
        for i in 1 ..< sideCount {
            let angle = CGFloat(i) / CGFloat(sideCount) * 2 * CGFloat.pi
            print("\(i) \(angle)")
            path.addLine(to: CGPoint(x: cos(angle), y: sin(angle)), transform: transform)
        }
        path.closeSubpath()

        print("rect=\(rect) path=\(path.boundingBox)")
        return path
    }
}

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

class PolygonView: UIView {

    override class var layerClass: AnyClass { return CAShapeLayer.self }

    @IBInspectable var sideCount: Int = 3 {
        didSet {
            setNeedsLayout()
        }
    }

    override func layoutSubviews() {
        super.layoutSubviews()

        (layer as! CAShapeLayer).path = CGPath.polygon(in: bounds, withSideCount: sideCount)
    }

}

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

【讨论】:

    【解决方案2】:

    首先,您所问的与@IBInspectable@IBDesignable 无关。当您创建自己的视图/视图控制器时,这些只是 XCode 与 Interface Builder 一起使用的指令。任何带有@IBInspectable 的属性也会出现在界面生成器的属性检查器中。 @IBDesignable 用于在界面生成器中显示自定义视图。现在去didSetget/set

    • didSet 这就是你所说的财产观察员。您可以为存储的属性定义属性观察器以监视属性的更改。有 2 种风格来监视可以定义的 willSetdidSet 的变化。因此,您定义观察者以在该属性发生更改的情况下执行一些代码块。如果您定义了willSet,则将在设置属性之前调用该代码。同样didSet 是设置属性后运行的块。因此,根据您需要做什么,您可以实现任一观察者。

    • get/set 除了存储属性之外,您还可以定义称为计算属性的东西。顾名思义,计算属性本身不会创建和存储任何值。这些值是在需要时计算的。所以这些属性需要getset 代码来在需要时计算属性。如果只有get,则表示它是只读属性。

    希望这会有所帮助。阅读 Swift 书籍并在 iTunesU 上完成 CS193p 的前几节课

    【讨论】:

      【解决方案3】:

      didSet 表示“设置变量时执行以下操作”。在您的情况下,如果您更改buttonBorderWidth,将调用函数updateView()

      getset 是您在请求变量本身时实际得到的。如果我设置shadowOpacity,它将传递给set 代码。如果我得到shadowOpacity,它实际上会得到我layer.shadowOpacity

      【讨论】:

        【解决方案4】:
        @IBInspectable var buttonBorderWidth: CGFloat = 1.0 
        

        在该示例中,buttonBorderWidth 是视图的实际属性。属性检查器可以对其进行写入和直接读取。 didSet 观察者只是为了响应我们更改该属性而发生的事情。

        这与另一个例子完全不同:

        @IBInspectable
        var shadowOpacity: Float {
            get {
                return layer.shadowOpacity
            }
            set {
                layer.shadowOpacity = newValue
            }
        }
        

        在该示例中,目标是使 层的 shadowOpacity 可检查。但是你不能这样做,因为它不是视图的属性。因此,我们在图层属性前面放置了一个 façade,以视图的计算“属性”的形式;属性检查器看不到layer.shadowOpacity,但它可以看到视图的 shadowOpacity,它不知道这只是访问层的shadowOpacity 的一种方式。

        【讨论】:

          猜你喜欢
          • 1970-01-01
          • 2015-09-20
          • 2018-03-17
          • 2015-07-12
          • 1970-01-01
          • 2017-03-30
          • 1970-01-01
          • 2017-08-22
          • 1970-01-01
          相关资源
          最近更新 更多