Composable, type-safe UIView styling with Swift functions

If you don’t abstract away the different styles of views you use in your application (font, background color, corner radius etc.), changing them is a huge pain in the ass. Trust me, I’m speaking from experience. So, I’ve started thinking of an API that will enable shared styles of different UIView instances.

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

The web has CSS, which is an abstraction of styling. It allows you to define classes of views, and apply a single style to multiple views and all of their subclasses. I want to create something that is just as powerful.

Here’s the use-case I want to satisfy:

  • Styles have to be easy to create, change and maintain.
  • They should all be declared in a single place, and not scattered throughout the application.
  • One style should be able to inherit from another style, while being able to override something. This means there’s no repeated code.
  • A style has to be specific for a specific UIView subclass. I don’t want to be up- or down-casting whenever I have to apply a style to a view. In other words, the API needs to be type-safe.

My first thought was to abstract away styles into a struct that would hold all the properties a UIView would need to style itself.

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

This lead to a quick realisation: There’s a lot of those properties. Not only that, for UILabel I would have to write a new UILabelStyle struct with different properties, and a new function to apply the style to the label. This would be tedious to write and to use.

It’s also not very extensible. If I think of a new property I want to make style-able, I’d have to add this property to all of the structs. This is a big violation of the open/closed principle.

Another problem with this approach is that there is no easy way to automatically compose two styles together. We would have to take two structs, and assign properties from both to a new struct, while one of them maintains precedence over the other.

This could be done automatically with reflection or meta-programming, which are pretty complex solutions. When I start coming up with complex solutions, I take a step back and ask myself: “Did I do something wrong in the beginning?” Usually, the answer is yes.

So I started thinking about the problem more fundamentally. What does a style do? It takes a UIView subclass and changes some properties on it. In other words, it performs side-effects on a UIView. That sounds like a function!

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

A “Style” is nothing different than a function applied to a UIView instance. If we want to change the fontSize of a UILabel, we simply change the property like we always do.

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

This way, we don’t have to manually declare properties for each UIView subclass. Since the function takes the actual type we need, we have all of its properties ready to be changed.

The good thing about using plain functions is that one style can inherit from another really easily: just call one function after the other. All of the changes of both will be applied, and the second one will override the first one if it needs to.

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 our styling API a bit easier to use, we’ll add a UIViewStyle struct that will wrap around around the style function.

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

Our declared styles will now look a bit different, but will still be as simple to use as with 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)
}

We only have to do two minor changes:

First, we are now creating a new instance of UIViewStyle, and passing the style closure as a parameter in the UIViewStyle.init.

Secondly, in the caption style, instead of calling the styles as functions, we call the styling function variable in the actual UIViewStyle instance.

We can now declare a compose function that will take a variadic parameter (an array of styles), and call them in succession, thus returning a new UIViewStyle that is a composite of different styles.

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

This is essentially a factory method. Given two or more styles, it will produce a new style which will call the functions of each given style in succession.

This would make declaring composite styles, like our caption style, a bit prettier and more declarative.

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

We’ll also add an apply function that will take a UIView and simply call the Styling method with the UIView.

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

We now have a clean, type-safe, composable way to declare style constants in our app!

And it’s really easy to apply those styles to our labels in a UIViewController or UIView.

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

Check out an expanded implementation of this principle in a playground on my GitHub.


Remember, whenever complex solutions start popping into your head, it’s code smell. It’s probably a sign that you should take a step back and reevaluate the fundamentals of what you are doing. There is often a much simpler solution.