Swift IBInspectable didSet против get/set

Я относительно новичок в 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
}

Но в некоторых случаях они используют, например, get и set

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

Может кто-нибудь объяснить: что происходит в каждом из этих случаев и как выбрать, какой из них использовать?


person Saik    schedule 20.07.2018    source источник
comment
Я думаю, это может помочь stackoverflow.com/questions/33091926/   -  person Sh_Khan    schedule 21.07.2018


Ответы (4)


Я вижу два вопроса. Первый — «Что происходит в каждом из этих случаев», и на него лучше всего ответить, прочитав глава "Свойства" документа Язык программирования 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

Во-первых, то, о чем вы спрашиваете, не имеет ничего общего с @IBInspectable или @IBDesignable. Это просто директивы для XCode, которые нужно использовать с Interface Builder, когда вы создаете свои собственные View/ViewController. Любое свойство с @IBInspectable также отображается в инспекторе атрибутов в интерфейсном конструкторе. И @IBDesignable для отображения пользовательского представления в построителе интерфейсов. Теперь, чтобы добраться до didSet и get/set

  • didSet Это то, что вы называете наблюдателем за недвижимостью. Вы можете определить наблюдателей свойств для сохраненного свойства, чтобы отслеживать изменения в свойстве. Существует 2 варианта отслеживания изменений willSet и didSet, которые можно определить. Таким образом, вы определяете наблюдателей для выполнения некоторого блока кода, в котором происходит изменение этого свойства. Если вы определите willSet, этот код будет вызываться до того, как свойство будет установлено. Аналогично, didSet — это блок, выполняемый после установки свойства. Поэтому в зависимости от того, что вам нужно сделать, вы можете реализовать любого из наблюдателей.

  • get/set Помимо сохраненных свойств вы можете определить так называемые вычисляемые свойства. Как следует из названия, вычисляемые свойства сами по себе не создают и не хранят никаких значений. Эти значения вычисляются по мере необходимости. Таким образом, эти свойства нуждаются в коде get и set для вычисления свойства при необходимости. Если есть только get, это означает, что это свойство доступно только для чтения.

Надеюсь это поможет. Прочитайте книгу Swift и пройдите несколько первых лекций CS193p в iTunesU.

person Azzaknight    schedule 21.07.2018

didSet означает «сделать следующее, когда переменная установлена». В вашем случае, если вы измените buttonBorderWidth, будет вызвана функция updateView().

get и set - это то, что вы на самом деле получаете, когда запрашиваете саму переменную. Если я установлю shadowOpacity, он передаст его коду set. Если я получу shadowOpacity, на самом деле я получу layer.shadowOpacity.

person Schemetrical    schedule 20.07.2018

@IBInspectable var buttonBorderWidth: CGFloat = 1.0 

В этом примере buttonBorderWidth является фактическим свойством представления. Инспектор атрибутов может писать в него и читать напрямую. Наблюдатель didSet нужен только для того, чтобы что-то происходило в ответ на изменение этого свойства.

Это полностью отличается от другого примера:

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

В этом примере цель состоит в том, чтобы сделать слой shadowOpacity доступным для проверки. Но вы не можете этого сделать, потому что это не свойство представления. Поэтому мы помещаем фасад перед свойством слоя в форме вычисляемого «свойства» вида; инспектор атрибутов не может видеть layer.shadowOpacity, но он может видеть представления shadowOpacity, которые, без его ведома, являются просто способом доступа к shadowOpacity слоя.

person matt    schedule 21.07.2018