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

    Даже для самого что ни на есть начинающего разработчика (скорее, на которого и рассчитан данный очерк), надеюсь, не секрет, что в коде не должно присутствовать никаких т.н. «hardcoded»-значений и прочих всяких там «magic numbers». Почему – тоже, надеюсь, понятно, а если нет, то в Сети имеются десятки, а то и сотни статей на эту тему, а также написан классический труд. «Android Studio» (наверное, не во всех случаях, но все же) даже любит генерировать «warnings» на эту тему и предлагать выносить строки и т.д. в ресурсные файлы. «Xcode» (пока?) такими подсказками нас не балует, и разработчику приходится самостоятельно держать себя в узде или, скажем, получать по рукам от коллег после «code review».

    Все это касается и используемых в приложении цветов.



    Последняя редакция – 16 февраля 2019 г.


    Цветовые константы



    Для начала хочется дать несколько более или менее стандартных рекомендаций.

    Во-первых, цвета всех элементов лучше сразу задавать в коде, а не в «Storyboard»/«Interface Builder». Если, конечно, это не приложение с одним экраном с тремя элементами и в одной цветовой схеме. Но даже и в этом случае никогда не знаешь наверняка, как изменится ситуация в будущем.

    Во-вторых, все цвета стоит определить константами, вынесенными в отдельный файл. Или в отдельную группу по соседству с соответствующим «view»-кодом.

    В-третьих, цвета стоит разделять на категории. Т.е. оперировать не «цветом второй кнопки на первом экране», а чем-нибудь вроде «цвета фона основного типа кнопок».

    Если от дизайнера (или от собственного чувства вкуса) поступит сигнал изменить цвет какого-либо элемента, его не придется долго искать – раз, изменять в нескольких местах (забывая какое-то из них и хватаясь за голову после отправки приложения в «iTunes Connect») – два.

    Таким образом мы будем иметь, например, файл Colors.swift с содержимым вроде:

    import UIKit
    
    enum ButtonAppearance {
        static let backgroundColor = UIColor.white
        static let borderColor = UIColor.gray
        static let textColor = UIColor.black
    }
    


    (enum без единого case – честно говоря, моя любимая структура типа для объявления констант. При таком использользовании, она автоматически лишает нас возможности создавать экземпляры типа и вообще использовать тип как-либо еще кроме задуманного способа.)

    Использование цвета будет выглядеть так:

    let someButton = UIButton()
    someButton.backgroundColor = ButtonAppearance.backgroundColor
    someButton.layer.borderColor = ButtonAppearance.borderColor.cgColor
    


    Модель цвета



    Предлагаю пойти дальше и написать модель, которая будет представлять различные используемые в приложении цвета:

    struct SchemeColor {
        
        private let color: UIColor
        
        func uiColor() -> UIColor {
            return color
        }
        
        func cgColor() -> CGColor {
            return color.cgColor
        }
        
    }
    


    Для удобства создания можно даже написать extension для UIColor:

    extension UIColor {
        
        func schemeColor() -> SchemeColor {
            return SchemeColor(color: self)
        }
        
    }
    


    В этом случае константы будут выглядеть так:

    enum ButtonAppearance {
        static let backgroundColor = UIColor.white.schemeColor()
        static let borderColor = UIColor.gray.schemeColor()
        static let textColor = UIColor.black.schemeColor()
    }
    


    В коде цвет задаваться будет таким образом:

    let someButton = UIButton()
    someButton.backgroundColor = ButtonAppearance.backgroundColor.uiColor()
    someButton.layer.borderColor = ButtonAppearance.borderColor.cgColor()
    


    И, наконец, для чего могут понадобиться такие дополнительные сложности – это…

    Цветовые схемы



    Допустим, мы хотим, чтобы наше приложение имело две цветовые схемы: темную и светлую. Для хранения списка цветовых схем определим enum:

    enum ColorSchemeOption {
        case dark
        case light
    }
    


    В данном случае, думаю, не будет зазорно создать тип для представления модели цветовой схемы в виде «синглтона»:

    struct ColorScheme {
        
        static let shared = ColorScheme()
        private (set) var schemeOption: ColorSchemeOption
        
        private init() {
            /*
            Здесь должен быть код, который определит цветовую схему и присвоит нужное значение option. Например, загрузив настройки из UserDefaults или взяв значение по умолчанию, если сохраненных настроек нет.
            */
        }
        
    }
    


    Я бы его даже определил внутри SchemeColor и сделал его private.

    Сам SchemeColor нужно модифицировать для того, чтобы он был осведомлен о том, какую цветовую схему использует приложение и возвращал нужный цвет:

    struct SchemeColor {
        
        let dark: UIColor
        let light: UIColor
        
        func uiColor() -> UIColor {
            return colorWith(scheme: ColorScheme.shared.schemeOption)
        }
        
        func cgColor() -> CGColor {
            return uiColor().cgColor
        }
        
        private func colorWith(scheme: ColorSchemeOption) -> UIColor {
            switch scheme {
            case .dark: return dark
            case .light: return light
            }
        }
    
        // ColorScheme
        
    }
    

    Цветовые константы теперь будут выглядеть уже так:

    enum ButtonAppearance {
        
        static let backgroundColor = SchemeColor(dark: Dark.backgroundColor, light: Light.backgroundColor)
        static let borderColor = SchemeColor(dark: Dark.borderColor, light: Light.borderColor)
        static let textColor = SchemeColor(dark: Dark.textColor, light: Light.textColor)
        
        private enum Light {
            static let backgroundColor = UIColor.white
            static let borderColor = UIColor.gray
            static let textColor = UIColor.black
        }
        
        private enum Dark {
            static let backgroundColor = UIColor.lightGray
            static let borderColor = UIColor.gray
            static let textColor = UIColor.black
        }
        
    }
    


    (extension для UIColor, кажется, больше не нужен.)

    А использование всего этого добра будет выглядеть все так же:

    let someButton = UIButton()
    someButton.backgroundColor = ButtonAppearance.backgroundColor.uiColor()
    someButton.layer.borderColor = ButtonAppearance.borderColor.cgColor()
    


    Чтобы поменять цвет какого-то элемента, по прежнему хватит только изменения соответствующей константы. А чтобы добавить еще одну цветовую схему, нужно добавить case в ColorSchemeOption, набор цветов для этой цветовой схемы в цветовые константы и обновить SchemeColor.

    Последнее, конечно, можно еще улучшить. Например, если количество схем разрастается, вероятно, удобней будет заменить громоздкий инициализатор на «строителя».

    Пожалуй, на этот раз все! Красивого кода!
    Поделиться публикацией

    Комментарии 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 например…

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

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