Вступление
Идея для публикации возникла после прочтения перевода CSS для Swift: использование стилей для любых подклассов UIView. Подход достаточно интересный, но он оказался не очень гибким, т.к. не позволяет объединять стили разных типов. Подробнее можно прочитать в комментарии.
В данной публикации будет сделана попытка получить более гибкий способ задания стилей, а также будут приведены примеры использования получившегося механизма.
Декорации
Введем понятие декорации, которое будет олицетворять придание неких свойств объекту:
typealias Decoration<T> = (T) -> Void
Декорация — это обобщенное замыкание, которое можно применить к объекту соответствующего класса или к объекту, чей класс является подклассом используемого для создания декорации класса.
let decoration: Decoration<UIView> = { (view: UIView) -> Void in
view.backgroundColor = UIColor.orange
view.alpha = 0.5
view.isOpaque = true
}
let view = UIView() // класс
decoration(view)
let label = UILabel() // подкласс
decoration(label)
Преимущества применения декораций над обычным приданием свойств объекту:
- Можно одновременно придавать сразу несколько свойств объекту
- Свойство описывается один раз и не требует изменений во всех местах применения декорации при рефакторинге (DRY)
- Меньше кода и больше наглядности в местах применения декораций
- Объединение декораций путем создания декорации, содержащей несколько других декораций
- Стильно, модно, молодежно
Декоратор и методы экзмепляра
Чтобы применить декорацию следует передать экземпляр в декорирующее замыкание. Однако, более естественным процессом будет передача декораций в метод экземпляра.
Методы экземпляра являются функциями, которые принадлежат экземплярам конкретного класса, структуры или перечисления. Они обеспечивают функциональность этих экземпляров, либо давая возможность доступа и изменения свойств экземпляра, либо обеспечивая функциональность экземпляра в соответствии с его целью. Метод экземпляра может быть вызван только для конкретного экземпляра типа, которому он принадлежит. Его нельзя вызвать в изоляции, без существующего экземпляра.
Для решения данной задачи можно использовать промежуточное звено — декоратор. Декоратор является обобщенной структурой, которая имеет указатель на экземпляр класса, к которому будут применяться декорации.
struct Decorator<T> {
let object: T
}
С помощью обобщенного протокола для декорируемого экземпляра можно получить декоратор. Для целей публикации декоратор можно будет получить для экземпляра любого класса, наследуемого от UILabel.
protocol DecoratorCompatible {
associatedtype DecoratorCompatibleType
var decorator: Decorator<DecoratorCompatibleType> { get }
}
extension DecoratorCompatible {
var decorator: Decorator<Self> {
return Decorator(object: self)
}
}
extension UILabel: DecoratorCompatible {}
Простой протокол строго задаёт все типы — параметры своих требований. Протокол сам определяет тип, подходящий для объявления параметра функции или переменной.
Обобщённый протокол — содержащий в своём определении подстановочное имя типа. Точный тип вычисляется только во время задания соответствия протоколу. Обобщённый протокол определяет некоторую концепцию, задавая ряд подстановочных имён для независимых типов и связывая их воедино с функциями и переменными — требованиями протокола.
Дополним структуру декоратора методом экземпляра, который будет принимать декорации. Стоит обратить внимание, что декорации будут применяться в той последовательности, в которой будут переданы декоратору. Это касается случаев, когда несколько декораций меняют одно и то же свойство объекта.
struct Decorator<T> {
let object: T
func apply(_ decorations: Decoration<T>...) -> Void {
decorations.forEach({ $0(object) })
}
}
Пример
Для целей публикации был создан репозиторий на github, который содержит пример использования. Также доступна установка через cocoapods: pod 'Decorator'.
Во-первых, следует создать набор нужных декораций любым удобным способом. Например, вот так:
struct Style {
static var fontNormal: Decoration<UILabel> {
return { (view: UILabel) -> Void in
view.font = UIFont.systemFont(ofSize: 14.0)
}
}
static var fontTitle: Decoration<UILabel> {
return { (view: UILabel) -> Void in
if #available(iOS 8.2, *) {
view.font = UIFont.systemFont(ofSize: 17.0, weight: UIFontWeightBold)
} else {
view.font = UIFont.boldSystemFont(ofSize: 17.0)
}
}
}
static func corners(rounded: Bool) -> Decoration<UIView> {
return { [rounded] (view: UIView) -> Void in
switch rounded {
case true:
let mask = CAShapeLayer()
let size = CGSize(width: 10, height: 10)
let rect = view.bounds
let path = UIBezierPath(roundedRect: rect, byRoundingCorners: .allCorners, cornerRadii: size)
mask.path = path.cgPath
view.layer.mask = mask
default:
view.layer.mask = nil
}
}
}
}
Стоит обратить внимание на тот факт, что декорации представлены двумя видами:
Decoration<UIView>
Decoration<UILabel>
Оба вида можно применять одновременно несмотря на то, что применяться они будут к объекту класса UILabel. Применение декораций через декоратора происходит следующим образом:
let labelNormal = UILabel()
labelNormal.decorator.apply(Style.fontNormal, Style.corners(rounded: false))
let labelTitle = UILabel()
labelNormal.decorator.apply(Style.fontTitle, Style.corners(rounded: true))
Заключение
Подход получился более гибким, чем в переводе статьи, т.к. удалось добиться применения разных стилей одновременно. Если есть идеи по улучшения подхода — комментарии приветствуются. Спасибо за внимание.
Комментарии (11)
dimsmol
02.05.2017 08:25+4В Кикстартере для стилей используют линзы, выглядит примерно так:
public func cardStyle <V: UIViewProtocol> (cornerRadius radius: CGFloat = Styles.cornerRadius) -> ((V) -> V) { return roundedStyle(cornerRadius: radius) <> V.lens.layer.borderColor .~ UIColor.ksr_grey_500.cgColor <> V.lens.layer.borderWidth .~ 1.0 <> V.lens.backgroundColor .~ .white }
пример использования:
_ = self.cardView |> cardStyle() |> dropShadowStyle() |> UIView.lens.layer.borderColor .~ UIColor.ksr_navy_500.cgColor
Выглядит достаточно элегантно и возможности композиции на очень хорошем уровне.
Библиотека, позволяющая такие чудеса, называется Kickstarter-Prelude.
Концепция линз взята из функционального программирования, широко используется в Хаскеле (см. lens package).
На Свифте, из-за синтаксических особенностей, выглядит не так красиво, но достаточно неплохо.
Также, Хаскель позволяет генерировать код для линз автоматически (используя Template Haskell), а тут приходится писать их вручную. Но, поскольку Kickstarter для нас уже постарался, можно использовать готовые.bubuh
04.05.2017 21:43+1Уже можно генерировать линзы через Sourcery и пример шаблона уже есть: AutoLenses.stencil.
werediver
02.05.2017 13:05Около полугода назад опубликовал на GitHub решение для стилизации
UIView
/NSView
, (микро-) фреймворк StyleSheet — идея перекликается со статьёй.
Стили ассоциируются с (пустыми) протоколами-маркерами и привязываются к подклассам
UIView
/NSView
через protocol conformance:
// Style-marker protocols protocol BodyFontStyle {} protocol MultilineLabelStyle {} func appStyle(palette p: PaletteProtocol) -> StyleProtocol { return StyleSheet(styles: [ // Styles implementation for different base-classes. Style<BodyFontStyle, UILabel> { $0.font = p.font.body }, Style<BodyFontStyle, UITextField> { $0.font = p.font.body }, Style<BodyFontStyle, UITextView> { $0.font = p.font.body }, Style<MultilineLabelStyle, UILabel> { $0.numberOfLines = 0 $0.lineBreakMode = .byWordWrapping }, ]) } final class BodyLabel: UILabel, BodyFontStyle, MultilineLabelStyle {} // Perform on app initialization try! RootStyle.autoapply(style: appStyle(palette: DefaultPalette())) // Specify `mode: .appearance` to use `UIAppearance`-hitchhiking
Таблицы стилей могут каскадироваться. Что такое "стиль" и "таблица стилей" описано в Style.swift.
Автоматическое применение стилей реализуется одним из двух механизмов, на выбор: через swizzling или через
UIAppearance
-hitchhiking (см. RootStyle.swift). В обоих случаях применение стилей происходит так же, как при использованииUIAppearance
(доступно для iOS и tvOS).
Автоматическое применение использовать не обязательно, но удобно. Установка через Carthage и CocoaPods.
Используется в production и приносит пользу :)
iWheelBuy
02.05.2017 17:09Вопрос вот какого плана: допустим есть какой-то подкласс с объявленными протоколами-стилями, то изменить стили для этого класса уже не получится?
werediver
02.05.2017 19:24Не вполне ясно, о чем вопрос. В примере выше есть BodyLabel, подкласс UIView с двумя стилями. Очевидно, вы можете изменить декларацию этого подкласса, если имеете доступ к коду. Вы также можете изменить реализацию стилей.
Можно добавить дополнительные стили (protocol conformance) через extension.
Если вопрос об изменении внешнего вида компонента во время выполнения, то эта задача решается несколько другими способами, независимо от используемого механизма стилизации.
iWheelBuy
02.05.2017 20:03Действительно, я плохо сформулировал вопрос. Вопрос был именно про изменение внешнего вида компонента во время выполнения. Какими механизмами?
werediver
02.05.2017 21:59+1Независимо от того, стилизуете ли вы компонент через Interface Builder, из кода непосредственно, через
UIAppearance
или с помощью какого-то фреймворка, компонент, который изменяет свой внешний вид, должен иметь параметры отображения для каждого своего состояния.
К примеру, если у вас есть on/off button, то у неё должны быть свойства
onColor
иoffColor
. Вы можете задать им нужные значения любым доступным способом, а кнопка сама будет выбирать, цветом из какого свойства покраситься :)iWheelBuy
05.05.2017 10:15Последую вашему хорошему замечанию про набор состояний. Возможно даже на вторую публикацию материала наберется!
Dominion1
Имею похожее решение, но не такое изящное. Спасибо!