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

habr.png

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


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


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


typealias Decoration = (T) -> Void


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


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


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


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


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


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


Decoration + Decoration = Decoration
Decoration + Decoration = Decoration
Decoration + Decoration = нельзя


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


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


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


func decor(_ type: T.Type, closure: @escaping Decoration) -> Decoration {
    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) -> Decoration {
        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 Style {

    let object: T
}


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


extension Style where T: Decorable {

    var state: AnyHashable? {
        get {
            //
        }
        set {
            //
        }
    }
}


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


class Holder {

    var state = Optional.none
}

var KEY: UInt8 = 0

extension Decorable {

    var holder: Holder {
        get {
            if let holder = objc_getAssociatedObject(self, &KEY) as? Holder {
                return holder
            } else {
                let holder = Holder()
                let policy = objc_AssociationPolicy.OBJC_ASSOCIATION_RETAIN_NONATOMIC
                objc_setAssociatedObject(self, &KEY, holder, policy)
                return holder
            }
        }
    }
}


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


extension Style where T: Decorable {

    var state: AnyHashable? {
        get {
            return object.holder.state
        }
        set(value) {
            object.holder.state = value
        }
    }
}


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


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


class Holder {

    var state = Optional.none
    var states = [AnyHashable: Decoration]()
}


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


extension Style where T: Decorable {

    func prepare(state: AnyHashable, decoration: @escaping Decoration) {
        object.holder.states[state] = decoration
    }
}


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


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


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


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


extension Style 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.style.apply(decoration)
            }
            holder.state = value
        }
    }
}


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


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


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


extension Style where T: Decorable {

    func prepare(state: AnyHashable, decoration: @escaping Decoration) {
        let holder = object.holder
        holder.states[state] = decoration
        if state == holder.state {
            object.style.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.style.state = .selected
}


Заключение


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


pod 'Style'

© Habrahabr.ru