Магия IBDesignable или расширяем функциональность Interface Builder в Xcode

в 10:26, , рубрики: interface builder, iOS, swift, uikit, xcode, интерфейсы, прототипирование, разработка под iOS, стилизация

Магия IBDesignable или расширяем функциональность Interface Builder в Xcode - 1

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)
    }
}

Магия IBDesignable или расширяем функциональность Interface Builder в Xcode - 2

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

Магия IBDesignable или расширяем функциональность Interface Builder в Xcode - 3

Магия IBDesignable или расширяем функциональность Interface Builder в Xcode - 4

Но ведь такие параметры наверное могут быть не только у нашего класса кнопки, а у любых других кнопок. Почему бы не сделать расширение базового класса 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).

Магия IBDesignable или расширяем функциональность Interface Builder в Xcode - 5

Как видно из результата, 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 }
    }
}

Магия IBDesignable или расширяем функциональность Interface Builder в Xcode - 6

Теперь параметрами слоя любой вьюшки можно управлять через билдер. Для возможности 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» соответственно.

Магия IBDesignable или расширяем функциональность Interface Builder в Xcode - 7

Магия IBDesignable или расширяем функциональность Interface Builder в Xcode - 8

Магия IBDesignable или расширяем функциональность Interface Builder в Xcode - 9

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

Резюме

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

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

Автор: dante_photo

Источник


* - обязательные к заполнению поля


https://ajax.googleapis.com/ajax/libs/jquery/3.4.1/jquery.min.js