📜 ⬆️ ⬇️

CSS for Swift: using styles for any subclass of UIView

Original article title: Composable, type-safe UIView styling with Swift functions

Before you get acquainted with the material, I want to add something about abstracting styles from myself. This method can make your life easier when working on large projects, and especially in an actively changing product. We fully felt it on a project such as IL DE BOTE , where the requirements for the visual component of the application were significant.

As the project progressed, significant UI changes were made to the project, and thanks to the styling, we were able to get rid of a little blood. In our approach, we used extensions for standard classes (UITextField, UILabel, UITextView, UIFont, UIColor). It seems to us that the author of the article managed to raise this approach a couple of steps higher - so much so that we, rubbing our palms, rushed to use it in our new project. We hope our translation will help you optimize development time and make projects better.
')


If you do not abstract the different presentation styles used in your application (font, background color, radius of corners, etc.), making any changes to them turns into a real disaster. You can believe me - I judge from my own experience. Having suffered a lot, I began to think about the API, which will give the opportunity to create shared-classes of different UIView instances.

body {background-color: powderblue;} h1 {color: blue;} p {color: red;} 

To abstract styles on the web, there is CSS. It allows you to define classes of representations and apply the same style to the set of representations and to all their subclasses. I want to create something just as powerful.

What do I need to enjoy life:


My first idea was to abstract styles into a structure that will store all the properties necessary for styling UIView.

 struct UIViewStyle { let backgroundColor: UIColor let cornerRadius: CGFloat } 

I understood quickly enough: there are a lot of these properties. In addition, for UILabel, I would have to write a new UILabelStyle structure with different properties and a new function to apply this style to the class. And writing and using them seemed tiresome to me.

In addition, this technique is not sufficiently extensible: if I need to add classes to a new property, I will have to add it to each structure. This violates the principle of openness / closeness.

Another problem with this technique is the lack of a simple way to automatically combine two styles together. We'll have to take two structures, take their properties and assign them to a new structure, while one of them retains precedence over the other.

This can be done automatically using metaprogramming, but the solution is rather complicated. When I come to difficult decisions, I stop and ask myself: “Maybe I made a mistake at the beginning?”. Most often the answer to this question is positive.

I decided to look at the problem more fundamentally. What does style do? It takes a subclass of UIView and changes certain properties in it. In other words, it has a side effect on UIView. And it looks like a function!

 typealias UIViewStyle<T: UIView> = (T)-> Void 

Style is nothing more than a function applied to a UIView. If we want to change the fontSize in the UILabel, we simply change the corresponding property.

 let smallLabelStyle: UIViewStyle<UILabel> = { label in label.font = label.font.withSize(12) } 

As a result, we do not need to manually declare each subclass of UIView. Since the function takes the type that we need, all its properties are ready for change.

The good side of using plain-functions is that one class inherits the properties of another as simple as possible: just call one function after another. All changes will be applied, and the second class will override the first, if necessary.

 let smallLabelStyle: UIViewStyle<UILabel> = { label in label.font = label.font.withSize(12) } let lightLabelStyle: UIViewStyle<UILabel> = { label in label.textColor = .lightGray } let captionLabelStyle: UIViewStyle<UILabel> = { label in smallLabelStyle(label) lightLabelStyle(label) } 

To make the API easier to use, we will add a UIViewStyle structure that wraps the style function.

 struct UIViewStyle<T: UIView> { let styling: (T)-> Void } 

Our declared styles will now look slightly different, but they will still be as easy to use as the plain functions.

 let smallLabelStyle: UIViewStyle<UILabel> = UIViewStyle { label in label.font = label.font.withSize(12) } let lightLabelStyle: UIViewStyle<UILabel> = UIViewStyle { label in label.textColor = .lightGray } let captionLabelStyle: UIViewStyle<UILabel> = UIViewStyle { label in smallLabelStyle.styling(label) lightLabelStyle.styling(label) } 

All we have to do is make two small changes:

  1. we create a new UIViewStyle object and pass the style closure to UIViewStyle.init as a parameter;
  2. in caption style, instead of calling styles as functions, we call the styling function variable in the current UIViewStyle instance.

Now we can declare the compose function, which will take a variable parameter (an array of styles) and sequentially call them, returning a new UIViewStyle made up of several styles.

 struct UIViewStyle<T: UIView> { let styling: (T)-> Void static func compose(_ styles: UIViewStyle<T>...)-> UIViewStyle<T> { return UIViewStyle { view in for style in styles { style.styling(view) } } } } 

In essence, this is a factory method. Having two or more styles as input, he will create a new style that will in turn call up the functions of each source style.

Due to this, the declaration of composite styles becomes more pleasant to the eye and more meaningful.

let captionLabelStyle: UIViewStyle = .compose (smallLabelStyle, lightLabelStyle)

We will also add the apply function, which will take a UIView and call the Styling method with it.

 struct UIViewStyle<T: UIView> { //... func apply(to view: T) { styling(view) } } 

Now we have a clean, type-safe, composable way to declare styles in our application. And the procedure for applying these styles to our classes in a UIViewController or UIView is very simple.

 class ViewController: UIViewController { let captionLabel = UILabel() override func viewDidLoad() { super.viewDidLoad() captionLabelStyle.apply(to: captionLabel) } } 

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


All Articles