Способ управления цветовыми схемами приложения под iOS

habr.png

Даже для самого что ни на есть начинающего разработчика (скорее, на которого и рассчитан данный очерк), надеюсь, не секрет, что в коде не должно присутствовать никаких т.н. «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 для вот для этой библиотеки.

© Habrahabr.ru