Swift Generics: cтили для UIView и не только #1

    Часть #2


    Вступление


    Идея для публикации возникла после прочтения перевода CSS для Swift: использование стилей для любых подклассов UIView. Подход достаточно интересный, но он оказался не очень гибким, т.к. не позволяет объединять стили разных типов. Подробнее можно прочитать в комментарии.


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


    Декорации


    Введем понятие декорации, которое будет олицетворять придание неких свойств объекту:


    typealias Decoration<T> = (T) -> Void

    Декорация

    Декорация — это обобщенное замыкание, которое можно применить к объекту соответствующего класса или к объекту, чей класс является подклассом используемого для создания декорации класса.


    Пример использования декорации для придания свойств объекту
    let decoration: Decoration<UIView> = { (view: UIView) -> Void in
        view.backgroundColor = UIColor.orange
        view.alpha = 0.5
        view.isOpaque = true
    }
    let view = UIView()     // класс
    decoration(view)
    let label = UILabel()   // подкласс
    decoration(label)

    Преимущества применения декораций над обычным приданием свойств объекту:


    • Можно одновременно придавать сразу несколько свойств объекту
    • Свойство описывается один раз и не требует изменений во всех местах применения декорации при рефакторинге (DRY)
    • Меньше кода и больше наглядности в местах применения декораций
    • Объединение декораций путем создания декорации, содержащей несколько других декораций
    • Стильно, модно, молодежно

    Декоратор и методы экзмепляра


    Чтобы применить декорацию следует передать экземпляр в декорирующее замыкание. Однако, более естественным процессом будет передача декораций в метод экземпляра.


    Методы экземпляра

    Методы экземпляра являются функциями, которые принадлежат экземплярам конкретного класса, структуры или перечисления. Они обеспечивают функциональность этих экземпляров, либо давая возможность доступа и изменения свойств экземпляра, либо обеспечивая функциональность экземпляра в соответствии с его целью. Метод экземпляра может быть вызван только для конкретного экземпляра типа, которому он принадлежит. Его нельзя вызвать в изоляции, без существующего экземпляра.


    Для решения данной задачи можно использовать промежуточное звено — декоратор. Декоратор является обобщенной структурой, которая имеет указатель на экземпляр класса, к которому будут применяться декорации.


    struct Decorator<T> {
        let object: T
    }

    С помощью обобщенного протокола для декорируемого экземпляра можно получить декоратор. Для целей публикации декоратор можно будет получить для экземпляра любого класса, наследуемого от UILabel.


    protocol DecoratorCompatible {
        associatedtype DecoratorCompatibleType
        var decorator: Decorator<DecoratorCompatibleType> { get }
    }
    
    extension DecoratorCompatible {
        var decorator: Decorator<Self> {
            return Decorator(object: self)
        }
    }
    
    extension UILabel: DecoratorCompatible {}

    Простые и обобщенные протоколы

    Простой протокол строго задаёт все типы — параметры своих требований. Протокол сам определяет тип, подходящий для объявления параметра функции или переменной.


    Обобщённый протокол — содержащий в своём определении подстановочное имя типа. Точный тип вычисляется только во время задания соответствия протоколу. Обобщённый протокол определяет некоторую концепцию, задавая ряд подстановочных имён для независимых типов и связывая их воедино с функциями и переменными — требованиями протокола.


    Дополним структуру декоратора методом экземпляра, который будет принимать декорации. Стоит обратить внимание, что декорации будут применяться в той последовательности, в которой будут переданы декоратору. Это касается случаев, когда несколько декораций меняют одно и то же свойство объекта.


    struct Decorator<T> {
        let object: T
        func apply(_ decorations: Decoration<T>...) -> Void {
            decorations.forEach({ $0(object) })
        }
    }

    Пример


    Для целей публикации был создан репозиторий на github, который содержит пример использования. Также доступна установка через cocoapods: pod 'Decorator'.


    Во-первых, следует создать набор нужных декораций любым удобным способом. Например, вот так:


    struct Style {
        static var fontNormal: Decoration<UILabel> {
            return { (view: UILabel) -> Void in
                view.font = UIFont.systemFont(ofSize: 14.0)
            }
        }
        static var fontTitle: Decoration<UILabel> {
            return { (view: UILabel) -> Void in
                if #available(iOS 8.2, *) {
                    view.font = UIFont.systemFont(ofSize: 17.0, weight: UIFontWeightBold)
                } else {
                    view.font = UIFont.boldSystemFont(ofSize: 17.0)
                }
            }
        }
        static func corners(rounded: Bool) -> Decoration<UIView> {
            return { [rounded] (view: UIView) -> Void in
                switch rounded {
                case true:
                    let mask = CAShapeLayer()
                    let size = CGSize(width: 10, height: 10)
                    let rect = view.bounds
                    let path = UIBezierPath(roundedRect: rect, byRoundingCorners: .allCorners, cornerRadii: size)
                    mask.path = path.cgPath
                    view.layer.mask = mask
                default:
                    view.layer.mask = nil
                }
            }
        }
    }

    Стоит обратить внимание на тот факт, что декорации представлены двумя видами:


    Decoration<UIView>
    Decoration<UILabel>

    Оба вида можно применять одновременно несмотря на то, что применяться они будут к объекту класса UILabel. Применение декораций через декоратора происходит следующим образом:


    let labelNormal = UILabel()
    labelNormal.decorator.apply(Style.fontNormal, Style.corners(rounded: false))
    let labelTitle = UILabel()
    labelNormal.decorator.apply(Style.fontTitle, Style.corners(rounded: true))

    Заключение


    Подход получился более гибким, чем в переводе статьи, т.к. удалось добиться применения разных стилей одновременно. Если есть идеи по улучшения подхода — комментарии приветствуются. Спасибо за внимание.


    Часть #2

    Share post

    Similar posts

    Comments 12

      0
      Имею похожее решение, но не такое изящное. Спасибо!
        +4

        В Кикстартере для стилей используют линзы, выглядит примерно так:


        public func cardStyle <V: UIViewProtocol> (cornerRadius radius: CGFloat = Styles.cornerRadius) -> ((V) -> V) {
        
          return roundedStyle(cornerRadius: radius)
            <> V.lens.layer.borderColor .~ UIColor.ksr_grey_500.cgColor
            <> V.lens.layer.borderWidth .~ 1.0
            <> V.lens.backgroundColor .~ .white
        }

        пример использования:


            _ = self.cardView
              |> cardStyle()
              |> dropShadowStyle()
              |> UIView.lens.layer.borderColor .~ UIColor.ksr_navy_500.cgColor

        (взято отсюда и отсюда)


        Выглядит достаточно элегантно и возможности композиции на очень хорошем уровне.
        Библиотека, позволяющая такие чудеса, называется Kickstarter-Prelude.


        Концепция линз взята из функционального программирования, широко используется в Хаскеле (см. lens package).


        На Свифте, из-за синтаксических особенностей, выглядит не так красиво, но достаточно неплохо.
        Также, Хаскель позволяет генерировать код для линз автоматически (используя Template Haskell), а тут приходится писать их вручную. Но, поскольку Kickstarter для нас уже постарался, можно использовать готовые.

          +1
          Уже можно генерировать линзы через Sourcery и пример шаблона уже есть: AutoLenses.stencil.
          0
          Пожалуй, это будет в проекте сегодня
            0

            Около полугода назад опубликовал на GitHub решение для стилизации UIView/NSView, (микро-) фреймворк StyleSheet — идея перекликается со статьёй.


            Стили ассоциируются с (пустыми) протоколами-маркерами и привязываются к подклассам UIView/NSView через protocol conformance:


            // Style-marker protocols
            protocol BodyFontStyle {}
            protocol MultilineLabelStyle {}
            
            func appStyle(palette p: PaletteProtocol) -> StyleProtocol {
                return StyleSheet(styles: [
                    // Styles implementation for different base-classes.
            
                    Style<BodyFontStyle, UILabel> { $0.font = p.font.body },
                    Style<BodyFontStyle, UITextField> { $0.font = p.font.body },
                    Style<BodyFontStyle, UITextView> { $0.font = p.font.body },
            
                    Style<MultilineLabelStyle, UILabel> {
                        $0.numberOfLines = 0
                        $0.lineBreakMode = .byWordWrapping
                    },
                ])
            }
            
            final class BodyLabel: UILabel, BodyFontStyle, MultilineLabelStyle {}
            
            // Perform on app initialization
            try! RootStyle.autoapply(style: appStyle(palette: DefaultPalette())) // Specify `mode: .appearance` to use `UIAppearance`-hitchhiking

            Таблицы стилей могут каскадироваться. Что такое "стиль" и "таблица стилей" описано в Style.swift.


            Автоматическое применение стилей реализуется одним из двух механизмов, на выбор: через swizzling или через UIAppearance-hitchhiking (см. RootStyle.swift). В обоих случаях применение стилей происходит так же, как при использовании UIAppearance (доступно для iOS и tvOS).


            Автоматическое применение использовать не обязательно, но удобно. Установка через Carthage и CocoaPods.


            Используется в production и приносит пользу :)

              0
              Вопрос вот какого плана: допустим есть какой-то подкласс с объявленными протоколами-стилями, то изменить стили для этого класса уже не получится?
                0

                Не вполне ясно, о чем вопрос. В примере выше есть BodyLabel, подкласс UIView с двумя стилями. Очевидно, вы можете изменить декларацию этого подкласса, если имеете доступ к коду. Вы также можете изменить реализацию стилей.


                Можно добавить дополнительные стили (protocol conformance) через extension.


                Если вопрос об изменении внешнего вида компонента во время выполнения, то эта задача решается несколько другими способами, независимо от используемого механизма стилизации.

                  0
                  Действительно, я плохо сформулировал вопрос. Вопрос был именно про изменение внешнего вида компонента во время выполнения. Какими механизмами?
                    +1

                    Независимо от того, стилизуете ли вы компонент через Interface Builder, из кода непосредственно, через UIAppearance или с помощью какого-то фреймворка, компонент, который изменяет свой внешний вид, должен иметь параметры отображения для каждого своего состояния.


                    К примеру, если у вас есть on/off button, то у неё должны быть свойства onColor и offColor. Вы можете задать им нужные значения любым доступным способом, а кнопка сама будет выбирать, цветом из какого свойства покраситься :)

                      0
                      Последую вашему хорошему замечанию про набор состояний. Возможно даже на вторую публикацию материала наберется!
              0
              *removed*
                +1

                При назначении свойст лейблу опечатка, повторно используется labelNormal, вместо labelTitle.


                Спасибо за статью!

                Only users with full accounts can post comments. Log in, please.