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



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





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



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



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

Резюме


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

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

Комментарии (8)


  1. visput
    08.01.2016 20:01

    Хорошая фича, но я вот столкнулся с такой проблемой: когда я помечаю свой контрой как 'IBDesignable', захожу в Interface Builder и добавляю свой контрол в какой-либо ViewController, то Xcode начинает билдить проект, причем делает это бесконечное количество раз, то есть он сбилдил проект и сразу же начинает процесс по новой. Весь этот процесс приводит к неслабой нагрузке на процессор, через некоторое время лэптоп начинает шуметь и подтормаживать. В общем пользоваться невозможно.

    Вы с таким не сталкивались?


    1. zaitsevyan
      08.01.2016 20:30

      У меня похожая ситуация, но не уверен, что идет именно «бесконечно повторяющийся билд». У меня в проекте 10 таргетов с разными настройками и макросами. При изменении файлов, IB начинал билдить все 10 таргетов… А полный билд одного таргета идет ~10 мин… И такое ощущение, что он не использовал кеш с предыдущих билдов. В итоге — я немного поигрался и убрал все это, так как работать было невозможно.


    1. dante_photo
      08.01.2016 21:23

      Сталкивался с этой проблемой в Xcode 6. Кинул это дело до выхода 7. Сейчас поведение стабильно. Ребилдится только при изменении исходного кода приложения или лайаута текущего контроллера. И только если открыт сам билдер.


    1. egormerkushev
      11.01.2016 16:56
      +1

      Отключается руками Editor > Automatically Refresh View — снять галочку.
      Но бесит, да.


      1. visput
        11.01.2016 20:26

        Круто, помогло, спасибо.
        Правда сейчас периодически выпадает ошибка:

        error: IB Designables: Failed to update auto layout status: The agent crashed

        Но это лучше, чем было.


        1. egormerkushev
          12.01.2016 12:05

          Бывает, что кое-что из пользовательского кода не удовлетворяет каким-то требованиям этого live rendering, но я тут не особо пока разбираюсь.


  1. devnikor
    09.01.2016 10:16

    Оффтоп, наверное, но если у enum RawValue это строка, то можно явно не прописывать значение. Оно выведется автоматом. Например, .Dark.rawValue == «Dark»


    1. dante_photo
      09.01.2016 12:55
      +2

      По мне явное указание лучше и может исключить некоторые ошибки при рефакторинге. Для любителей conventions конечно будет норм.