Способ управления цветовыми схемами приложения под iOS
Даже для самого что ни на есть начинающего разработчика (скорее, на которого и рассчитан данный очерк), надеюсь, не секрет, что в коде не должно присутствовать никаких т.н. «hardcoded»-значений и прочих всяких там «magic numbers». Почему — тоже, надеюсь, понятно, а если нет, то в Сети имеются десятки, а то и сотни статей на эту тему, а также написан классический труд. Android Studio (наверное, не во всех случаях, но все же) даже любит генерировать «warnings» на эту тему и предлагать выносить строки и т.д. в ресурсные файлы. Xcode (пока?) такими подсказками нас не балует, и разработчику приходится самостоятельно держать себя в узде или, скажем, получать по рукам от коллег после «code review».
Все это касается и используемых в приложении цветов.
Цветовые константы
Для начала хочется дать несколько более или менее стандартных рекомендаций.
Во-первых, цвета всех элементов лучше сразу задавать в коде, а не в 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 для вот для этой библиотеки.