📜 ⬆️ ⬇️

Swift Generics: Styles for UIView and more. # 2

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


The approach was very successful and was repeatedly tested on real projects. In addition, there were additions to the approach and its 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> = (T) -> Void 

You can use this closure to impart UIView properties as follows:


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

The composition of the scenery


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


 func +<T>(lhs: @escaping Decoration<T>, rhs: @escaping Decoration<T>) -> Decoration<T> { 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 passed to one of the closures must be a subclass of the object passed to the other closure:


 Decoration<UISwitch> + Decoration<UISwitch> = Decoration<UISwitch> Decoration<UISwitch> + Decoration<UIView> = Decoration<UISwitch> Decoration<UISwitch> + Decoration<UILabel> =  

Creation of scenery


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


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


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

Used this as follows:


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

But self not autocomplete and the function could not be called decoration , since most often create a closure with the name of decoration and an error occurred:


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

A more successful solution was the creation of a universal static function:


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

To create a decorative closure in the end can be 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, the combination of such variables is used only to change the style of a particular UIView .


If you try to describe the state of the UIView style of a single variable, then you can use enums. However, OptionSet , which allows for combinations, is even better.


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

You can apply 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, having a pointer to an instance of the class to which the decorations will be applied.


 struct Decorator<T> { let object: T } 

In the generalized Decorator structure, we 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 was made possible using runtime functions of the association of objects. We introduce a class that will be associated with the decoration object and will contain the necessary variables.


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

Now the generalized Decorator structure can save state through the class associated with the Holder object.


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

Storing scenery


If you can store the state of the style, then you can also store decorations for different states. This is achieved by creating the [AnyHashable: Decoration<T>] decoration dictionary in the associated with the decoration object instance of the Holder class.


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

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


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

You can use 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 vocabulary of the scenery, when the state of the style changes, the appropriate scenery from the dictionary should be applied. This can be achieved by changing the implementation of the style state setter:


 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 got into the scenery dictionary. For such a situation, it is worth refining the function of preparing the scenery for the state:


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

Animations?


If there is something inside the applied decoration that can be animated, ...


It will be 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, applying, re-using and composition of decorations has been obtained. The full tool code can be found at the link . As usual, it is possible to install and test through CocoaPods :


pod 'Style'

')

Source: https://habr.com/ru/post/339048/


All Articles