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)
devnikor
09.01.2016 10:16Оффтоп, наверное, но если у enum RawValue это строка, то можно явно не прописывать значение. Оно выведется автоматом. Например, .Dark.rawValue == «Dark»
dante_photo
09.01.2016 12:55+2По мне явное указание лучше и может исключить некоторые ошибки при рефакторинге. Для любителей conventions конечно будет норм.
visput
Хорошая фича, но я вот столкнулся с такой проблемой: когда я помечаю свой контрой как 'IBDesignable', захожу в Interface Builder и добавляю свой контрол в какой-либо ViewController, то Xcode начинает билдить проект, причем делает это бесконечное количество раз, то есть он сбилдил проект и сразу же начинает процесс по новой. Весь этот процесс приводит к неслабой нагрузке на процессор, через некоторое время лэптоп начинает шуметь и подтормаживать. В общем пользоваться невозможно.
Вы с таким не сталкивались?
zaitsevyan
У меня похожая ситуация, но не уверен, что идет именно «бесконечно повторяющийся билд». У меня в проекте 10 таргетов с разными настройками и макросами. При изменении файлов, IB начинал билдить все 10 таргетов… А полный билд одного таргета идет ~10 мин… И такое ощущение, что он не использовал кеш с предыдущих билдов. В итоге — я немного поигрался и убрал все это, так как работать было невозможно.
dante_photo
Сталкивался с этой проблемой в Xcode 6. Кинул это дело до выхода 7. Сейчас поведение стабильно. Ребилдится только при изменении исходного кода приложения или лайаута текущего контроллера. И только если открыт сам билдер.
egormerkushev
Отключается руками Editor > Automatically Refresh View — снять галочку.
Но бесит, да.
visput
Круто, помогло, спасибо.
Правда сейчас периодически выпадает ошибка:
Но это лучше, чем было.
egormerkushev
Бывает, что кое-что из пользовательского кода не удовлетворяет каким-то требованиям этого live rendering, но я тут не особо пока разбираюсь.