Оригинальное название статьи: Composable, type-safe UIView styling with Swift functions
Прежде, чем вы познакомитесь с материалом, мне хочется добавить кое-что про абстрагирование стилей от себя. Этот метод может облегчить вашу жизнь при работе на крупных проектах и особенно — в активно меняющемся продукте. Мы в полной силе ощутили это на таком проекте, как ИЛЬ ДЕ БОТЭ, где требования к визуальной составляющей приложения были значительными.
По мере развития в проект вносились существенные UI-изменения, и благодаря выделению стилей нам удалось отделаться малой кровью. В своем подходе мы использовали расширения для стандартных классов (UITextField, UILabel, UITextView, UIFont, UIColor). Нам кажется, что автору статьи удалось поднять данный подход на пару ступеней выше — настолько, что мы, потирая ладошки, бросились использовать его в нашем новом проекте. Надеемся, наш перевод поможет вам оптимизировать время на разработку и сделать проекты лучше.
Если вы не абстрагируете разные стили представления, используемые в вашем приложении (шрифт, цвет фона, радиус углов и т.д.), внесение в них каких-либо изменений превращается в настоящую катастрофу. Можете поверить мне — я сужу, исходя из собственного опыта. Изрядно намучившись, я стал думать над API, которое даст возможность создавать shared-классы различных экземпляров UIView.
body {background-color: powderblue;}
h1 {color: blue;}
p {color: red;}
Чтобы абстрагировать стили в вебе, существует CSS. Он позволяет определять классы представлений и применять один и тот же стиль ко множеству представлений и ко всем их подклассам. Я хочу создать что-то настолько же мощное.
Что мне нужно, чтобы радоваться жизни:
- стили должны быть простыми в создании, внесении изменений и поддержке;
- все стили должны декларироваться в одном месте, а не быть разбросанными по приложению;
- один стиль должен быть способным наследовать свойства другого стиля и при этом быть способным переопределять что-либо. Это поможет избавиться от повторов в коде;
- для каждого специфического подкласса UIView должен существовать специфический стиль. Я не хочу заниматься преобразованиями каждый раз, когда мне понадобится применить класс к представлению. Другими словами, API должен быть типобезопасным.
Моей первой идеей было абстрагирование стилей в структуру, которая будет хранить все свойства, необходимые для стилизации UIView.
struct UIViewStyle {
let backgroundColor: UIColor
let cornerRadius: CGFloat
}
Достаточно быстро я понял: этих свойств очень много. Кроме того, для UILabel мне пришлось бы написать новую структуру UILabelStyle с другими свойствами и новую функцию для применения этого стиля к классу. И писать, и использовать их казалось мне утомительным.
Помимо того, данная методика недостаточно растяжима: если мне потребуется добавить классам новое свойство, мне придётся добавлять его в каждую структуру. Это нарушает принцип открытости/закрытости.
Ещё одной проблемой данной методики является отсутствие простого способа автоматически скомпоновать вместе два стиля. Придётся взять две структуры, взять их свойства и присвоить их новой структуре, пока одна из них сохраняет старшинство над другой.
Это можно сделать автоматически при помощи метапрограммирования, однако решение это довольно сложное. Когда я прихожу к сложным решениям, я останавливаюсь и спрашиваю себя: «Возможно, я допустил ошибку в начале?». Чаще всего ответ на этот вопрос положительный.
Я решил посмотреть на проблему более фундаментально. Чем занимается стиль? Он берёт подкласс UIView и меняет в нём определенные свойства. Другими словами, он оказывает на UIView побочное действие. А это похоже на функцию!
typealias UIViewStyle<T: UIView> = (T)-> Void
Style — это не что иное, как функция, применяемая к UIView. Если мы хотим изменить fontSize в UILabel, мы просто меняем соответствующее свойство.
let smallLabelStyle: UIViewStyle<UILabel> = { label in
label.font = label.font.withSize(12)
}
В результате нам не нужно вручную декларировать каждый подкласс UIView. Так как функция принимает тот тип, который нужен нам, все её свойства готовы к изменению.
Хорошая сторона использования plain-функций заключается в том, что один класс наследует свойства другого максимально просто: достаточно вызвать одну функцию после другой. Все изменения будут применены, и второй класс переопределит первый, если это будет необходимо.
let smallLabelStyle: UIViewStyle<UILabel> = { label in
label.font = label.font.withSize(12)
}
let lightLabelStyle: UIViewStyle<UILabel> = { label in
label.textColor = .lightGray
}
let captionLabelStyle: UIViewStyle<UILabel> = { label in
smallLabelStyle(label)
lightLabelStyle(label)
}
Чтобы сделать API более простым в использовании, мы добавим структуру UIViewStyle, которая обернёт функцию стиля.
struct UIViewStyle<T: UIView> {
let styling: (T)-> Void
}
Наши декларированные стили теперь будут выглядеть несколько иначе, но по-прежнему будут так же просты в использовании, как и plain-функции.
let smallLabelStyle: UIViewStyle<UILabel> = UIViewStyle { label in
label.font = label.font.withSize(12)
}
let lightLabelStyle: UIViewStyle<UILabel> = UIViewStyle { label in
label.textColor = .lightGray
}
let captionLabelStyle: UIViewStyle<UILabel> = UIViewStyle { label in
smallLabelStyle.styling(label)
lightLabelStyle.styling(label)
}
Всё, что нам нужно сделать, это внести два небольших изменения:
- мы создаём новый объект UIViewStyle и передаём замыкание стиля в UIViewStyle.init в качестве параметра;
- в caption style, вместо вызова стилей в виде функций, мы вызываем переменную функции styling в текущем экземпляре UIViewStyle.
Теперь мы можем декларировать функцию compose, которая возьмёт вариативный параметр (массив стилей) и последовательно вызовет их, вернув новый UIViewStyle, составленный из нескольких стилей.
struct UIViewStyle<T: UIView> {
let styling: (T)-> Void
static func compose(_ styles: UIViewStyle<T>...)-> UIViewStyle<T> {
return UIViewStyle { view in
for style in styles {
style.styling(view)
}
}
}
}
По сути это фабричный метод. Имея на входе два или более стиля, он создаст новый стиль, который будет по очереди вызывать функции каждого исходного стиля.
Благодаря этому декларирование составных стилей становится приятнее глазу и более содержательным.
let captionLabelStyle: UIViewStyle = .compose(smallLabelStyle, lightLabelStyle)
Также мы добавим функцию apply, которая возьмёт UIView и вызовет с ним метод Styling.
struct UIViewStyle<T: UIView> {
//...
func apply(to view: T) {
styling(view)
}
}
Теперь у нас есть чистый, типобезопасный, компонуемый способ декларирования стилей в нашем приложении. А процедура применения этих стилей к нашим классам в UIViewController или UIView очень проста.
class ViewController: UIViewController {
let captionLabel = UILabel()
override func viewDidLoad() {
super.viewDidLoad()
captionLabelStyle.apply(to: captionLabel)
}
}
Автор: Лайв Тайпинг