Способ управления цветовыми схемами приложения под 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 для вот для этой библиотеки.
    Поделиться публикацией
    Ой, у вас баннер убежал!

    Ну. И что?
    Реклама
    Комментарии 12
    • +1
      1 — а зачем создавать оба инстанса UIColor, если будет использован только один?
      2 — касательно проекта-примера на гитхабе. Имхо, я бы не использовал глобальные константы, а добавил екстеншен к UIColor.
      например.
      extension UIColor
      {
          @nonobjc class var backgroundSecondary: UIColor
          {
              return SchemeColor(classic: BACKGROUND_SECONDARY_COLOR_CLASSIC,
                                 dark: BACKGROUND_SECONDARY_COLOR_DARK).color()
          }
      }
      navigationController?.navigationBar.barTintColor = .backgroundSecondary 

      вместо
      let backgroundSecondaryColor = SchemeColor(classic: BACKGROUND_SECONDARY_COLOR_CLASSIC,
                                                 dark: BACKGROUND_SECONDARY_COLOR_DARK)
       navigationController?.navigationBar.barTintColor = backgroundSecondaryColor.color()
      • 0
        Насчет второго пункта – согласен, красивый путь, понравился!

        А первый не вполне понял. Это о struct ButtonAppearanceLight и struct ButtonAppearanceDark?
        • +1
          SchemeColor хранит в себе 2 UIColor.
          • 0
            А как предлагаете сделать? SchemeColor в данном случае же и предназначен, чтобы принимать в себя возможные цвета определенного элемента и возвращать нужный в зависиомсти от используемой цветовой схемы.
            • +1
              если сильно заморочится, то будет как-то так
              protocol ColorTheme {
                  var main: UIColor { get }
              }
              
              struct DarkTheme: ColorTheme {
                  var main: UIColor {
                      return .black
                  }
              }
              
              struct ClassicTheme: ColorTheme {
                  var main: UIColor {
                      return .white
                  }
              }
              
              
              
              final class ColorScheme {
                  
                  // MARK: - Properties
                  static let shared = ColorScheme()
                  var theme: ColorTheme
              }


              далее в ините можно сетапить нужную тему, тогда и Option не надо.
              
              extension UIColor
              {
                  @nonobjc class var backgroundSecondary: UIColor
                  {
                      return ColorScheme.shared.theme.main
                  }
              }
              

              а если смущает правило 3 точек, то тогда уже theme можно сделать fileprivate
              
              extension ColorScheme: ColorTheme {
                  var main: UIColor {
                      return theme.main
                  }
              }
              
              extension UIColor
              {
                  @nonobjc class var backgroundSecondary: UIColor
                  {
                      return ColorScheme.shared.main
                  }
              }
              
      • +2

        Жаль, что вас не смущает дублирование кода в структурах ButtonAppearanceLight и ButtonAppearanceDark. По хорошему вам нужна одна структура ButtonAppearance и в ней свойства должны быть не static-ами. А в энуме ColorSchemeOption к кейсам (которые, кстати, Apple рекомендует называть НЕ капсом) добавить associatied value типа ButtonAppearance (внезапно, да?) Чтоб получилось что-то типа такого:


        enum ColorSchemeOption {
            case dark(buttonAppearance: ButtonAppearance)
            case light(buttonAppearance: ButtonAppearance)
        }
        • 0
          • 0
            Как быть, если один и тот же цвет назначается на разные элементы в разных экранах? К примеру есть ГлавныйЦветКнопкиОтмена и этот же цвет используется для других контролов. Если дублировать цвета под разными названиями может образоваться очень много переменных с одинаковым цветом и при большом количестве экранов превратится в адский ад
            • 0
              Как справляетесь с этим? Лично я для себя ничего лучше, чем долгое я мучительное раздумье по поводу названия цветовых констант не придумал: делить элементы на группы, подгруппы и т.д. и называть их в духе materialized path.
              • 0
                А и не надо пытаться все стандартизировать, это путь в никуда. Полностью согласен с Tereks.
                Я для себя решил, один цвет — одна константа. А дальше уже на уровне контролов разруливаешь какой цвет использовать. Когда приходится перекрасить все приложение, всплывает куча приколов от дизайнеров, что в темной теме у нас тут так, а в светлой сяк, и ты как ни разбивай, все равно без костылей или овердублирования не обойтись.
                А так, проходишься по всем классам контролов, и если надо по некоторым вьюконтроллерам, меняешь где надо ручками с одной цветовой константы на другую. Супер гибко, кода минимум, и на приложении с полусотней экранов и парой десятков ячеек занимает максимум день.
            • –1
              про UIApperance ни слова :(
              • +1
                Во первых UIApperance, а во вторых проще использовать расширения к UIColor например…

              Только полноправные пользователи могут оставлять комментарии. Войдите, пожалуйста.

              Самое читаемое