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 UIView
as 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 self
didn’t autocomplete and the function could not be called decoration
, because most often create a closure with a name 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 to create a universal static
function:
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 UIView
single 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, Decorator
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 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 Decorator
can save state through the Holder
class 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'