Swift Generics: styles for UIView and not only # 2

    This publication is a continuation of the issue where the topic of decorating objects was touched. Familiarization with the first publication will help you better understand the current context, as The previously mentioned terms and solutions will be described with simplifications.


    The approach turned out to be very successful and has been repeatedly tested on real projects. In addition, additions to the approach have appeared and the usability has increased significantly.


    Let me remind you that the main element of the presented style setting method is the generalized closure:


    typealias Decoration = (T) -> Void

    You can use this closure to give properties UIViewas follows:


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

    Scenery composition


    Using the addition operator and observing the order of the scenery application, you can get the scenery composition mechanism:


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

    You can add not only closures that accept objects of the same class. However, it should be noted that the class of the object that is passed to one of the closures must be a subclass of the object that is passed to the other closure:


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

    Create Scenery


    The main inconvenience in creating the scenery was writing the code for the scenery design itself. I had to write the type of decoration, closure, class type inside the closure ... Most often it ended with CTRL + C, CTRL + V.


    To get out of the situation and generate a circuit through the autocomplete, a universal function was written that took the type of object:


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

    It was used as follows:


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

    It just selfdidn’t autocomplete and the function could not be called decoration, because most often create a closure with a name decorationand an error occurred:


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

    A more successful solution was to create a universal staticfunction:


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

    As a result, you can create a decorating closure as follows:


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

    condition


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

    Most often, a combination of such variables is used only to change the style of a particular one UIView.


    If you try to describe the style state of a UIViewsingle variable, you can use enumerations. However, it is even better suited OptionSet, which allows for combinations.


    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
        }
    }

    It can be used as follows:


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

    In the last publication , a generalized structure was introduced that has a pointer to an instance of the class to which the scenery will be applied.


    struct Decorator {
        let object: T
    }

    In the generalized structure, Decoratorwe introduce an additional variable that will be responsible for the state of the style.


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

    Preserving the state of an object through a generalized structure has become possible when using the runtime functions of object association. We introduce a class that will be associated with the decoration object and will contain the necessary variables.


    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
                }
            }
        }
    }

    Now the generalized structure Decoratorcan save state through the Holderclass associated with the object .


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

    Decoration Storage


    If you can store the state of a style, then you can store decorations for different states in the same way. This is achieved by creating a scenery dictionary in an instance of the class associated with the decoration object .[AnyHashable: Decoration]Holder


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

    To add decorations to the dictionary, we introduce the function:


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

    You can use it as follows:


    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
    }

    The use of scenery


    After filling the scenery dictionary, when the style state changes, you should apply the appropriate decoration from the dictionary. This can be achieved by changing the style state setter implementation:


    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
            }
        }
    }

    The decoration will be applied as follows:


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

    It is also worth mentioning the case when the object was set to a state of style before the corresponding decoration entered the scenery dictionary. For such a situation, it is worth finalizing the function of preparing the scenery for the state:


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

    Animations?


    If inside the applied decoration contains something that can be animated ...


    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

    ... then changing the style of the object inside the animation block will lead to the corresponding animations:


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

    Conclusion


    A convenient tool for creating, storing, using, reusing and decorating compositions was obtained. The full tool code can be found here . As usual, you can install and try it out through CocoaPods :


    pod 'Style'

    Also popular now: