[Из песочницы] Магия IBDesignable или расширяем функциональность Interface Builder в Xcode

1932de56faf84c8290b56e28940f0a50.png

Interface Builder в Xcode с некоторого времени экономит мне много времени в работе по стандартному лайауту элементов интерфейса и иногда помогает в задаче прототипирования. С версии 6 в Xcode добавили возможность рендера кастомных вьюшек, помеченных атрибутом IBDesignable, а также отображение в билдере полей класса, помеченных атрибутом IBInspectable.

С версии Xcode 7 этой фичей стало более-менее возможно пользоваться, поэтому мне захотелось проверить её возможности.

Почитать про IBDesignable/IBInspectable можно тут и тут.

Стандартный кейс


Давайте создадим кастомную кнопку с возможностью настраивать цвет, толщину и радиус скругления border, причем чтобы все эти параметры можно было контролировать через Interface Builder.

@IBDesignable class BorderedButton : UIButton {
    /// Толщина границы
    @IBInspectable var borderWidth: CGFloat {
        set { layer.borderWidth = newValue }
        get { return layer.borderWidth }
    }
    /// Цвет границы
    @IBInspectable var borderColor: UIColor? {
        set { layer.borderColor = newValue?.CGColor }
        get { return layer.borderColor?.UIColor }
    }
    /// Радиус границы
    @IBInspectable var cornerRadius: CGFloat {
        set { layer.cornerRadius = newValue }
        get { return layer.cornerRadius  }
    }
}

extension CGColor {
    private var UIColor: UIKit.UIColor {
        return UIKit.UIColor(CGColor: self)
    }
}


8da65a8b53b047d9b10293db23db50ee.png

Все работает, билдер обновляет рендер при изменении параметров.

b05046ebcf8c410089927a31eaa831f0.png

feff635002254fcf823c0a22efc4e1c3.png

Но ведь такие параметры наверное могут быть не только у нашего класса кнопки, а у любых других кнопок. Почему бы не сделать расширение базового класса UIButton.

extension UIButton {
    /// Радиус гараницы
    @IBInspectable var cornerRadius: CGFloat {
        set { layer.cornerRadius = newValue  }
        get { return layer.cornerRadius }
    }
    /// Толщина границы
    @IBInspectable var borderWidth: CGFloat {
        set { layer.borderWidth = newValue }
        get { return layer.borderWidth }
    }
    /// Цвет границы
    @IBInspectable var borderColor: UIColor? {
        set { layer.borderColor = newValue?.CGColor  }
        get { return layer.borderColor?.UIColor }
    }
}


Сотрём IBInspectable поля класса кастомной кнопки, так как они уже прописаны в расширении. В результате класс останется пустым.

@IBDesignable class BorderedButton : UIButton {}


Добавим еще одну кнопку рядом с нашей кастомной кнопкой, но не будем назначать ей класса (будет стандартный UIButton).

83822bfbc59a4963bf8eb7b194f588f2.png

Как видно из результата, Interface Builder сохранил возможность ввода IBInspectable полей даже у базового класса UIButton, однако не рендерит его, так как он не помечен атрибутом IBDesignable.

Расширяем дальше


Похожим образом можно расширить базовый класс UIView.

extension UIView {
    
   /// Радиус гараницы
    @IBInspectable var cornerRadius: CGFloat {
        set { layer.cornerRadius = newValue  }
        get { return layer.cornerRadius }
    }
    /// Толщина границы
    @IBInspectable var borderWidth: CGFloat {
        set { layer.borderWidth = newValue }
        get { return layer.borderWidth }
    }
    /// Цвет границы
    @IBInspectable var borderColor: UIColor? {
        set { layer.borderColor = newValue?.CGColor  }
        get { return layer.borderColor?.UIColor }
    }
    /// Смещение тени
    @IBInspectable var shadowOffset: CGSize {
        set { layer.shadowOffset = newValue  }
        get { return layer.shadowOffset }
    }
    /// Прозрачность тени
    @IBInspectable var shadowOpacity: Float {
        set { layer.shadowOpacity = newValue }
        get { return layer.shadowOpacity }
    }
    /// Радиус блура тени
    @IBInspectable var shadowRadius: CGFloat {
        set {  layer.shadowRadius = newValue }
        get { return layer.shadowRadius }
    }
    /// Цвет тени
    @IBInspectable var shadowColor: UIColor? {
        set { layer.shadowColor = newValue?.CGColor }
        get { return layer.shadowColor?.UIColor }
    }
    /// Отсекание по границе
    @IBInspectable var _clipsToBounds: Bool {
        set { clipsToBounds = newValue }
        get { return clipsToBounds }
    }
}


0039f288b3b14625ac2717c6688497b7.png

Теперь параметрами слоя любой вьюшки можно управлять через билдер. Для возможности live-рендера только одно условие — у вьюшки в билдере должен быть указан кастомные класс с атрибутом IBDesignable.

Нестандартный кейс


Допустим, у нас в приложении есть светлая и темная темы. Попробуем стилизовать кнопки с помощью перечисления.

/// Стиль кнопки
enum ButtonStyle: String {
    
    /// Светлый стиль
    case Light  = "light"
    /// Темный стиль
    case Dark   = "dark"
    
    /// Оттенок
    var tintColor: UIColor {
        switch self {
        case .Light:    return UIColor.blackColor()
        case .Dark:     return UIColor.lightGrayColor()
        }
    }
    /// Цвет границы
    var borderColor:        UIColor { return tintColor }
    /// Цвет фона
    var backgroundColor:    UIColor { return UIColor.clearColor() }
    /// Толщина границы
    var borderWidth:        CGFloat { return 1 }
    /// Радиус границы
    var cornerRadius:       CGFloat { return 4 }
}


Напишем соответствующее расширение для класса UIButton, которое позволяет выбирать и применять стили к кнопкам:

extension UIButton {
     /// Стиль кнопки
    @IBInspectable var style: String? {
        set { setupWithStyleNamed(newValue) }
        get { return nil }
    }
    /// Применение стиля по его строковому названию
    private func setupWithStyleNamed(named: String?){
        if let styleName = named, style = ButtonStyle(rawValue: styleName) {
            setupWithStyle(style)
        }
    }
    /// Применение стиля по его идентификатору
    func setupWithStyle(style: ButtonStyle){
        backgroundColor = style.backgroundColor
        tintColor       = style.tintColor
        borderColor     = style.borderColor
        borderWidth     = style.borderWidth
        cornerRadius    = style.cornerRadius
    }
}


Теперь добавляем, а билдере еще две кнопки, и в новом поле Style прописываем стили «dark» и «light» соответственно.

e0cb83e776114ba28d438308f35714a1.png

0c87a891458a4b028c8287d56629135a.png

634a0a158f4940ccaea71da0f7a8576e.png

Теперь мы можем применять стили к кнопкам одним полем в билдере и наблюдать их реальное отображение. Если ограничится только первым, то нам даже не придется создавать свой IBDesignable класс (который по сути пустой). Ничто не мешает добавить еще несколько стилей, а также расширить тип стиля и сделать динамический выбор применяемых значений в зависимости от класса вьюшки.

Резюме


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

Исходники можно найти на гите.

© Habrahabr.ru