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

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


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


    Напомню, что основным элементом представленного способа задания стилей является обобщенное замыкание:


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

    Использовать данное замыкание для придания свойств UIView можно следующим образом:


    let decoration: Decoration<UIView> = { (view: UIView) -> Void in
        view.backgroundColor = .white
    }
    let view = UIView()
    decoration(view)

    Композиция декораций


    Используя оператор сложения и соблюдая порядок применения декораций можно получить механизм композиции декораций:


    func +<T>(lhs: @escaping Decoration<T>, rhs: @escaping Decoration<T>) -> Decoration<T> {
        return { (value: T) -> Void in
            lhs(value)
            rhs(value)
        }
    }

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


    Decoration<UISwitch> + Decoration<UISwitch> = Decoration<UISwitch>
    Decoration<UISwitch> + Decoration<UIView> = Decoration<UISwitch>
    Decoration<UISwitch> + Decoration<UILabel> = нельзя

    Создание декораций


    Главным неудобством при создании декорации было написание кода самой конструкции декорации. Приходилось писать тип декорации, замыкание, тип класса внутри замыкания… Чаще всего это заканчивалось CTRL+C, CTRL+V.


    Чтобы выйти из ситуации и генерировать замыкание через автокомплит была написана универсальная функция, которая принимала тип объекта:


    func decor<T>(_ type: T.Type, closure: @escaping Decoration<T>) -> Decoration<T> {
        return closure
    }

    Использовалось это следующим образом:


    let decoration = decor(UIView.self) { (view) in
        view.backgroundColor = .white
    }

    Вот только self не автокомплитится и функцию нельзя было назвать decoration, т.к. чаще всего замыкание создавать с именем decoration и возникала ошибка:


    error: variable used within its own initial value
    let decoration = decoration(UIView.self) { (view) in

    Более удачным решением стало создание универсальной static функции:


    protocol Decorable: class {}
    
    extension NSObject: Decorable {}
    
    extension Decorable {
    
        static func decoration(closure: @escaping Decoration<Self>) -> Decoration<Self> {
            return closure
        }
    }

    Создавать декорирующее замыкание в итоге можно следующим образом:


    let decoration = UIView.decoration { (view) in
        view.backgroundColor = .white
    }

    Состояние


    class MyView: UIView {
    
        var isDisabled: Bool = false
        var isFavorite: Bool = false
        var isSelected: Bool = false
    }

    Чаще всего сочетание подобных переменных применяется лишь для того, чтобы изменить стиль конкретного UIView.


    Если попытаться описать состояние стиля UIView одной переменной, то можно использовать перечисления. Однако, еще лучше подойдет OptionSet, который позволяет предусмотреть сочетания.


    struct MyViewState: OptionSet, Hashable {
    
        let rawValue: Int
    
        init(rawValue: Int) {
            self.rawValue = rawValue
        }
    
        static let normal = MyViewState(rawValue: 1 << 0)
    
        static let disabled = MyViewState(rawValue: 1 << 1)
        static let favorite = MyViewState(rawValue: 1 << 2)
        static let selected = MyViewState(rawValue: 1 << 3)
    
        var hashValue: Int {
            return rawValue
        }
    }

    Применять можно следующим образом:


    class MyView: UIView {
    
        var state: MyViewState = .normal
    }
    
    let view = MyView()
    view.state = [.disabled, .favorite]
    view.state = .selected

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


    struct Decorator<T> {
    
        let object: T
    }

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


    extension Decorator where T: Decorable {
    
        var state: AnyHashable? {
            get {
                //
            }
            set {
                //
            }
        }
    }

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


    class Holder<T:Decorable> {
    
        var state = Optional<AnyHashable>.none
    }
    
    var KEY: UInt8 = 0
    
    extension Decorable {
    
        var holder: Holder<Self> {
            get {
                if let holder = objc_getAssociatedObject(self, &KEY) as? Holder<Self> {
                    return holder
                } else {
                    let holder = Holder<Self>()
                    let policy = objc_AssociationPolicy.OBJC_ASSOCIATION_RETAIN_NONATOMIC
                    objc_setAssociatedObject(self, &KEY, holder, policy)
                    return holder
                }
            }
        }
    }

    Теперь обобщенная структура Decorator может сохранять состояние через ассоциированный с объектом Holder класс.


    extension Decorator where T: Decorable {
    
        var state: AnyHashable? {
            get {
                return object.holder.state
            }
            set(value) {
                object.holder.state = value
            }
        }
    }

    Хранение декораций


    Если можно хранить состояние стиля, то точно так же можно хранить декорации для разных состояний. Это достигается путем создания словаря декораций [AnyHashable: Decoration<T>] в ассоциированном с объектом декорации экземпляре класса Holder.


    class Holder<T:Decorable> {
    
        var state = Optional<AnyHashable>.none
        var states = [AnyHashable: Decoration<T>]()
    }

    Чтобы добавлять декорации в словарь введем функцию:


    extension Decorator where T: Decorable {
    
        func prepare(state: AnyHashable, decoration: @escaping Decoration<T>) {
            object.holder.states[state] = decoration
        }
    }

    Использовать можно следующим образом:


    let view = MyView()
    view.decorator.prepare(state: MyViewState.disabled) { (view) in
        view.backgroundColor = .gray
    }
    view.decorator.prepare(state: MyViewState.favorite) { (view) in
        view.backgroundColor = .yellow
    }

    Применение декораций


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


    extension Decorator where T: Decorable {
    
        var state: AnyHashable? {
            get {
                return object.holder.state
            }
            set(value) {
                let holder = object.holder
                if let key = value, let decoration = holder.states[key] {
                    object.decorator.apply(decoration)
                }
                holder.state = value
            }
        }
    }

    Применяться декорация будет следующим образом:


    let view = MyView()
    // подготовка декораций
    view.decorator.state = .selected

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


    extension Decorator where T: Decorable {
    
        func prepare(state: AnyHashable, decoration: @escaping Decoration<T>) {
            let holder = object.holder
            holder.states[state] = decoration
            if state == holder.state {
                object.decorator.apply(decoration)
            }
        }
    }

    Анимации?


    Если внутри применяемой декорации содержится что-то, что можно анимировать,...


    When positive, the background of the layer will be drawn with
    rounded corners. Also effects the mask generated by the
    'masksToBounds' property. Defaults to zero. Animatable.

    open var cornerRadius: CGFloat

    … то изменения стиля объекта внутри анимационного блока приведет к соответствующим анимациям:


    UIView.animate(withDuration: 0.5) {
        view.decorator.state = .selected
    }

    Заключение


    Получен удобный инструмент создания, хранения, применения, переиспользования и композиции декораций. Полный код инструмента можно найти по ссылке. Как обычно есть возможность установить и опробовать через CocoaPods:


    pod 'Style'
    Поделиться публикацией
    AdBlock похитил этот баннер, но баннеры не зубы — отрастут

    Подробнее
    Реклама
    Комментарии 0

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