Вступление
Идея для публикации возникла после прочтения перевода 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))
Заключение
Подход получился более гибким, чем в переводе статьи, т.к. удалось добиться применения разных стилей одновременно. Если есть идеи по улучшения подхода — комментарии приветствуются. Спасибо за внимание.