我看到两个问题。第一个是“在每种情况下发生了什么”,最好阅读the “Properties” chapter of The Swift Programming Language 来回答。已经发布了其他三个回答第一个问题的答案,但没有一个回答第二个更有趣的问题。
第二个问题是“如何选择使用哪一个”。
您的shadowOpacity 示例(这是一个计算属性)比您的buttonBorderWidth 示例(这是一个带有观察者的存储属性)具有以下优势:
所有与shadowOpacity 相关的代码都在一个地方,因此更容易理解它的工作原理。 buttonBorderWidth 代码分布在 didSet 和 updateViews 之间。在实际程序中,这些函数更有可能相距更远,正如您所说,“通常每个 IBInspectable 都有更多条目”。这使得查找和理解实现buttonBorderWidth 所涉及的所有代码变得更加困难。
由于视图的 shadowOpacity 属性 getter 和 setter 只是转发到层的属性,因此视图的属性不会在视图的内存布局中占用任何额外空间。视图的buttonBorderWidth 是一个存储属性,确实会在视图的内存布局中占用额外的空间。
这里单独的updateViews 有一个优势,但它很微妙。请注意,buttonBorderWidth 的默认值为 1.0。这与layer.borderWidth 的默认值0 不同。不知何故,我们需要在视图初始化时让layer.borderWidth 匹配buttonBorderWidth,即使buttonBorderWidth 从未被修改过。由于设置layer.borderWidth 的代码在updateViews 中,我们可以确保在视图显示之前的某个时间点调用updateViews(例如在init 或layoutSubviews 或willMove(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 圆角。假设我们想要将cornerRadius 和maskedCorners 都公开为可检查的属性。我们不妨使用计算属性公开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。