Все это касается и используемых в приложении цветов.
Цветовые константы
Для начала хочется дать несколько более или менее стандартных рекомендаций.
Во-первых, цвета всех элементов лучше сразу задавать в коде, а не в Storyboard. Если, конечно, это не приложение с одним экраном с тремя элементами и в одной цветовой схеме. Но даже и в этом случае никогда не знаешь наверняка, как изменится ситуация в будущем.
Во-вторых, все цвета стоит определить константами, вынесенными в отдельный файл.
В-третьих, цвета стоит обобщить с помощью категорий. Т.е. оперировать не «цветом второй кнопки на первом экране», а чем-нибудь вроде «цвета фона основного типа кнопок».
В-четвертых, объединять наборы цветов одного элемента (например, цвет фона, цвет ободка и цвет текста одного и того же типа кнопок) в структуры (перечисляемый тип, если захочется, без дополнительных манипуляций использовать не получится – UIColor не адаптирует RawRepresentable).
Если от дизайнера (или от собственного чувства вкуса) поступит сигнал изменить цвет какого-либо элемента, его не придется долго искать – раз, изменять в нескольких местах (забывая какое-то из них и хватаясь за голову после отправки приложения в iTunes Connect) – два.
Таким образом мы будем иметь, например, файл ColorConstants.swift с содержимым вроде:
import UIKit
struct ButtonAppearance {
static let backgroundColor = #colorLiteral(red: 1.0, green: 1.0, blue: 1.0, alpha: 1.0)
static let borderColor = #colorLiteral(red: 0.1, green: 0.1, blue: 0.1, alpha: 1.0)
static let textColor = #colorLiteral(red: 0.0, green: 0.0, blue: 0.0, alpha: 1.0)
}
(Цветовые литералы, естественно, будут отображаться в Xcode цветными квадратами.)
Использование цвета будет выглядеть так:
let someButton = UIButton()
someButton.backgroundColor = ButtonAppearance.backgroundColor
Модель цвета
Предлагаю пойти дальше и написать класс-модель, который будет представлять различные используемые в приложении цвета (зачем именно – будет ясно позднее):
struct SchemeColor {
// MARK: - Properties
let сolor: UIColor
// MARK: - Initialization
init(сolor: UIColor) {
self.сolor = сolor
}
// MARK: - Methods
func uiColor() -> UIColor {
return color
}
func cgColor() -> CGColor {
return uiColor().cgColor
}
}
В этом случае цветовые константы будут выглядеть так:
struct ButtonAppearance {
static let backgroundColor = SchemeColor(color: #colorLiteral(red: 1.0, green: 1.0, blue: 1.0, alpha: 1.0))
static let borderColor = SchemeColor(color: #colorLiteral(red: 0.1, green: 0.1, blue: 0.1, alpha: 1.0))
static let textColor = SchemeColor(color: #colorLiteral(red: 0.0, green: 0.0, blue: 0.0, alpha: 1.0))
}
В коде цвет задаваться будет таким образом:
let someButton = UIButton()
someButton.backgroundColor = ButtonAppearance.backgroundColor.uiColor()
someButton.layer.borderColor = ButtonAppearance.borderColor.cgColor()
И, наконец, для чего могут понадобиться такие дополнительные сложности – это…
Цветовые схемы
Допустим, мы хотим, чтобы наше приложение имело две цветовые схемы: скажем, темную и светлую. Для хранения списка цветовых схем определим enum:
enum ColorSchemeOption {
case DARK
case LIGHT
}
Для глобального доступа, думаю, не будет зазорно в этом случае создать класс для представления модели цветовой схемы по шаблону «одиночка»:
final class ColorScheme {
// MARK: - Properties
static let shared = ColorScheme()
var option: ColorSchemeOption
// MARK: - Initialization
private init() {
/*
Здесь должен быть код, который определит цветовую схему и присвоит нужное значение option. Например, загрузив настройки из UserDefaults или взяв значение по умолчанию, если сохраненных настроек нет.
*/
}
}
Я бы его даже определил в файле, в котором определен SchemeColor и сделал его fileprivate.
Сам SchemeColor нужно модифицировать для того, чтобы он был осведомлен о том, какую цветовую схему использует приложение и возвращал нужный цвет:
struct SchemeColor {
// MARK: - Properties
private let dark: UIColor
private let light: UIColor
// MARK: - Initialization
init(light: UIColor,
dark: UIColor) {
self.dark = dark
self.light = light
}
// MARK: - Methods
func uiColor() -> UIColor {
return colorWith(scheme: ColorScheme.shared.option)
}
func cgColor() -> CGColor {
return сolorUI().cgColor
}
// MARK: Private methods
private func colorWith(scheme: ColorSchemeOption) -> UIColor {
switch scheme {
case .DARK:
return dark
case .LIGHT:
return light
}
}
}
Цветовые константы теперь будут выглядеть уже так:
struct ButtonAppearanceLight {
static let backgroundColor = #colorLiteral(red: 1.0, green: 1.0, blue: 1.0, alpha: 1.0)
static let borderColor = #colorLiteral(red: 0.1, green: 0.1, blue: 0.1, alpha: 1.0)
static let textColor = #colorLiteral(red: 0.0, green: 0.0, blue: 0.0, alpha: 1.0)
}
struct ButtonAppearanceDark {
static let backgroundColor = #colorLiteral(red: 0.0, green: 0.0, blue: 0.0, alpha: 1.0)
static let borderColor = #colorLiteral(red: 0.9, green: 0.9, blue: 0.9, alpha: 1.0)
static let textColor = #colorLiteral(red: 1.0, green: 1.0, blue: 1.0, alpha: 1.0)
}
struct ButtonAppearance {
static let backgroundColor = SchemeColor(light: ButtonAppearanceLight.backgroundColor,
dark: ButtonAppearanceDark.backgroundColor)
static let borderColor = SchemeColor(light: ButtonAppearanceLight.borderColor,
dark: ButtonAppearanceDark.borderColor)
static let textColor = SchemeColor(light: ButtonAppearanceLight.textColor,
dark: ButtonAppearanceDark.textColor)
}
А использование всего этого добра будет выглядеть все так же:
let someButton = UIButton()
someButton.backgroundColor = ButtonAppearance.backgroundColor.uiColor()
someButton.layer.borderColor = ButtonAppearance.borderColor.cgColor()
Чтобы поменять цвет какого-то элемента, по прежнему хватит только изменения соответствующей константы. А чтобы добавить еще одну цветовую схему, нужно добавить case в ColorSchemeOption, набор цветов для этой цветовой схемы в цветовые константы и добавить новую схему в инициализатор SchemeColor и его метод colorWith(scheme:).
Последнее, конечно, можно еще улучшить. Например, если количество схем разрастается, вероятно, удобней будет заменить громоздкий инициализатор на шаблон «строитель».
Заключение
ColorScheme можно использовать и для других целей, связанных с цветовой схемой. Например, можно добавить в него метод, который будет возвращать нужный внешний вид клавиатуры в зависимости от цветовой схемы:
func keyboardAppearance() -> UIKeyboardAppearance {
switch option {
case .DARK:
return .dark
case .LIGHT:
return .light
}
}
На практике такой подход мне довелось применить в Example для вот для этой библиотеки.
Комментарии (11)
s_suhanov
18.02.2018 23:26+1Жаль, что вас не смущает дублирование кода в структурах
ButtonAppearanceLight
иButtonAppearanceDark
. По хорошему вам нужна одна структураButtonAppearance
и в ней свойства должны быть не static-ами. А в энумеColorSchemeOption
к кейсам (которые, кстати, Apple рекомендует называть НЕ капсом) добавить associatied value типаButtonAppearance
(внезапно, да?) Чтоб получилось что-то типа такого:
enum ColorSchemeOption { case dark(buttonAppearance: ButtonAppearance) case light(buttonAppearance: ButtonAppearance) }
Tereks
19.02.2018 07:20Как быть, если один и тот же цвет назначается на разные элементы в разных экранах? К примеру есть ГлавныйЦветКнопкиОтмена и этот же цвет используется для других контролов. Если дублировать цвета под разными названиями может образоваться очень много переменных с одинаковым цветом и при большом количестве экранов превратится в адский ад
hummingbirddj Автор
19.02.2018 09:26Как справляетесь с этим? Лично я для себя ничего лучше, чем долгое я мучительное раздумье по поводу названия цветовых констант не придумал: делить элементы на группы, подгруппы и т.д. и называть их в духе materialized path.
maxonflic
19.02.2018 09:22про UIApperance ни слова :(
svanichkin
19.02.2018 10:52Во первых UIApperance, а во вторых проще использовать расширения к UIColor например…
pingwinator
1 — а зачем создавать оба инстанса UIColor, если будет использован только один?
2 — касательно проекта-примера на гитхабе. Имхо, я бы не использовал глобальные константы, а добавил екстеншен к UIColor.
например.
вместо
hummingbirddj Автор
Насчет второго пункта – согласен, красивый путь, понравился!
А первый не вполне понял. Это о struct ButtonAppearanceLight и struct ButtonAppearanceDark?
pingwinator
SchemeColor хранит в себе 2 UIColor.
hummingbirddj Автор
А как предлагаете сделать? SchemeColor в данном случае же и предназначен, чтобы принимать в себя возможные цвета определенного элемента и возвращать нужный в зависиомсти от используемой цветовой схемы.
pingwinator
если сильно заморочится, то будет как-то так
далее в ините можно сетапить нужную тему, тогда и Option не надо.
а если смущает правило 3 точек, то тогда уже theme можно сделать fileprivate