Я вижу два вопроса. Первый — «Что происходит в каждом из этих случаев», и на него лучше всего ответить, прочитав глава "Свойства" документа Язык программирования Swift. Также уже опубликовано три других ответа на первый вопрос, но ни один из них не отвечает на второй, и более интересный вопрос.
Второй вопрос: «Как выбрать, какой из них использовать».
Ваш пример shadowOpacity
(который является вычисляемым свойством) имеет следующие преимущества по сравнению с вашим примером buttonBorderWidth
(который является сохраненным свойством с наблюдателем):
Весь код, связанный с shadowOpacity
, находится в одном месте, поэтому проще понять, как он работает. Код buttonBorderWidth
распределен между didSet
и updateViews
. В реальной программе эти функции, скорее всего, будут дальше друг от друга, и, как вы сказали, «Обычно здесь больше записей для каждого IBInspectable». Это усложняет поиск и понимание всего кода, связанного с реализацией buttonBorderWidth
.
Поскольку геттер и сеттер свойства представления shadowOpacity
просто пересылают свойство слоя, свойство представления не занимает дополнительного места в структуре памяти представления. 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
.
person
rob mayoff
schedule
21.07.2018