[Из песочницы] Магия IBDesignable или расширяем функциональность Interface Builder в Xcode
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 класс (который по сути пустой). Ничто не мешает добавить еще несколько стилей, а также расширить тип стиля и сделать динамический выбор применяемых значений в зависимости от класса вьюшки.
Резюме
В статье я не пытался преподнести новый способ стилизации элементов интерфейса. Однако, возможно, данный подход подтолкнет кого-нибудь на другое оригинальное применение данной фичи.
Исходники можно найти на гите.