Introduction of View Styles at Bumble

Andrei Simvolokov
Bumble Tech

--

We are the team behind Badoo and Bumble, two of the world’s largest dating and connection applications with millions of users worldwide. To deal with the challenges that arise with such different products we rely on code reusability, and by so avoiding reinventing the wheel we keep our apps stable, our UX solutions consistent and much more.

However, for something to be reusable it has to actually be designed with that in mind. So, our dev team supports specific instruments, follows specific procedures and, of course, uses specific code architecture solutions.

Let’s say, for example, that you are creating a new feature which requires a new UI component likely to be reused in other projects. How would you go about it?

Any kind of clean architecture requires that the solution be split at least in two layers:

  • A View class — for displaying data on a screen.
  • A class which is responsible for a reusable case logic and controls a View class.

This pattern is very common to every mainstream application architecture (MVP, MVVM, VIPER, etc.)

Sounds like a nice clean solution, doesn’t it? But are these solutions really able to provide reusable components which can be styled for any specific case? Let’s test them out and see how we create such components in Bumble. Ready?

What is problematic with classic MVC/MVP/MVVM implementation?

Imagine a simple tooltip which needs to appear when a particular control appears on the screen. A model layer provides a localised text to be presented. Depending on control, each tooltip should have a different color:

Let’s use the MVP pattern as an example. The presenter needs to handle one event only (the control has appeared), set the tooltip’s text, set the tooltip’s color and present the View.

The class structure will look something like this:

The tooltip’s color will differ across different cases. So, a specific Presenter is needed for each case.

We can do the opposite and create a 100% reusable Presenter. It’s not going to control the View’s color. If a View is going to set its color on its own, then a specific View class needs to be implemented for each case.

Both patterns have problems with code reusability. Each time the tooltip needs to be reused, the developer is pushed to implement a whole new View/Presenter class. Most likely the developer will create a View/Presenter subclass instead and experience all the side effects of subclassing.

The ideal solution would be to have a single View class and a single Presenter class such that they can be reused everywhere. But how do you stylify the tooltip in each specific case? Classic architecture patterns like MVP just don’t describe that. Looks like we need a third player, a special class responsible for a View’s style.

Implementing a style component

Let’s dive into typography for a while. Imagine we publish a magazine or manage a Bumble employee benefits web-page. Here is a block of text which could be put there:

Editors change the text in two ways:

  • Editing. Editing means content changing. Let’s change, for example, 5-minute daily massages to 1-hour weekly massages.
  • Formatting. Formatting means changing text’s visible attributes, so let’s change the title’s color and make the font of the most important words bold.

The same approach can be used for our reusable UI components. Firstly, we need to split a View’s API into two parts

  • Content. This is the main data which View is supposed to output no matter in which App or which part of App is used
  • Style. This is a format data and is specific for each reusable case.

When a particular View needs to be displayed with MVP pattern on a screen:

  • a View is created
  • a View applies a specific style according to a case
  • a View is assigned to a Presenter.

So, a Presenter controls a View’s content while a View’s style is controlled by the factory which created it. This architecture is pretty flexible and facilitates the implementation of reusable components and combines them together:

  • the same Views can be reused in different contexts
  • the same Presenters can be used in the same feature cases. Views can be styled for a specific case
  • the same Styles can be used for the same UI component even if a feature case is different.

Alright, now that we have the architecture design, let’s create a self-explanatory maintainable and testable style for the next component:

I suggest defining a style as a struct:

struct NewsViewStyle {
let backgroundColor: UIColor
let borderColor: UIColor
let borderWidth: CGFloat
let cornerRadius: CGFloat
let titleAlignment: NSTextAlignment
let titleFont: UIFont
let messageAlignment: NSTextAlignment
let messageFont: UIFont
}

And then, implementing a View:

class NewsView: UIView {
private let titleLabel = UILabel()
private let messageLabel = UILabel()
override init(frame: CGRect) {
super.init(frame: frame)
self.setupConstraints()
}
required init?(coder: NSCoder) {
super.init(coder: coder)
}
var title: String? {
get { self.titleLabel.text }
set { self.titleLabel.text = newValue }
}
var message: String? {
get { self.messageLabel.text }
set { self.messageLabel.text = newValue }
}
func apply(style: NewsViewStyle) {
self.backgroundColor = style.backgroundColor
self.layer.borderColor = style.borderColor.cgColor
self.layer.borderWidth = style.borderWidth
self.layer.cornerRadius = style.cornerRadius
self.titleLabel.textAlignment = style.messageAlignment
self.titleLabel.textColor = style.messageColor
self.titleLabel.font = style.messageFont
self.messageLabel.textAlignment = style.messageAlignment
self.messageLabel.textColor = style.messageColor
self.messageLabel.font = style.messageFont
}
func setupConstraints() {
// ...
}
}

As you can see, the title-text and the message-text are the displayed content of this View. At the same time, a huge set of properties became a style of the View. A specific Style entity can be easily defined. For example, a general component style for the Badoo App would look like this:

extension NewsViewStyle {
public static var badoo: NewsViewStyle {
NewsViewStyle(
backgroundColor: .white,
borderColor: UIColor(hex: 0x783Bf9),
borderWidth: 1,
cornerRadius: 0,
titleAlignment: .left,
titleColor: UIColor(hex: 0x783Bf9),
titleFont: UIFont.systemFont(ofSize: 22),
messageAlignment: .left,
messageColor: UIColor(hex: 0x767676),
messageFont: UIFont.systemFont(ofSize: 14)
)
}
}

So now we’re able to create a View, apply a Style and assign the View to Presenter.

let view = NewsView()
view.apply(style: .badoo)
let presenter = NewPresenter(view: view)

Substyles

The previous example features a very simple View. However, its Style is a large structure and difficult to maintain. If we continue increasing a View’s complexity we will definitely struggle with the style’s size, so let’s try and break it down into smaller pieces.

First of all, we’ll create a very abstract ViewStyle structure which can be used for all the Views.

struct ViewStyle { 
let backgroundColor: UIColor?
let borderColor: UIColor?
let borderWidth: CGFloat
let cornerRadius: CGFloat
}
extension UIView {
func apply(style: ViewStyle) {
self.backgroundColor = style.backgroundColor
self.layer.borderColor = style.borderColor?.cgColor
self.layer.borderWidth = style.borderWidth
self.layer.cornerRadius = style.cornerRadius
}
}

Now, let’s create a LabelStyle to be applied on UILabel. UILabel is a subclass of UIView, so it must contain a superclass style’s data. This can be achieved through aggregation.

struct LabelStyle { 
// Superclass style
let viewStyle: ViewStyle
// Own values
let textAlignment: NSTextAlignment
let textColor: UIColor?
let font: UIFont?
}
extension UILabel {
func apply(style: LabelStyle) {
// Applying superclass style
self.apply(style: style.viewStyle)
// Applying own values
self.textAlignment = style.textAlignment
self.textColor = style.textColor
self.font = style.font
}
}

Finally, we have to refactor the NewsViewStyle structure. NewsView is a subclass of UIView, so it must aggregate a ViewStyle structure. Also, it contains two UILabels and must have their styles.

struct NewsViewStyle { 
// Superclass style
let viewStyle: ViewStyle
// Aggregated components’ styles
let titleStyle: LabelStyle
let messageStyle: LabelStyle
// Own values
// -
}
class NewsView {
// ...
func apply(style: NewsViewStyle) {
// Applying superclass style
self.apply(style: style.viewStyle)
// Applying aggregated components’ styles
self.titleLabel.apply(style: style.titleStyle)
self.messageLabel.apply(style: style.messageStyle)
// Applying own values
// -
}
// ...
}

This code is much better! It’s shorter, more understandable and it can be easily extended. It means we are now able to reuse substyles on as many levels as we need.

Default styles’ values

What does our NewsViewStyle.badoo definition look like now?

extension NewsViewStyle { 
public static var badoo: NewsViewStyle {
NewsViewStyle(
viewStyle: ViewStyle(
backgroundColor: .white,
borderColor: UIColor(hex: 0x783BF9),
borderWidth: 1,
cornerRadius: 0
),
titleStyle: LabelStyle(
viewStyle: ViewStyle(
backgroundColor: .clear,
borderColor: .clear,
borderWidth: 0,
cornerRadius: 0
),
textAlignment: .left,
textColor: UIColor(hex: 0x783BF9),
font: UIFont.systemFont(ofSize: 22)
),
messageStyle: LabelStyle(
viewStyle: ViewStyle(
backgroundColor: .clear,
borderColor: .clear,
borderWidth: 0,
cornerRadius: 0
),
textAlignment: .left,
textColor: UIColor(hex: 0x767676),
font: UIFont.systemFont(ofSize: 14)
)
)
}
}

Hmm… it’s really bulky, right? Also, half of the code is describing default values. Do we need to do it for each style? Swift allows us to provide default values for parameters, so it is good for providing them to Style’s constructor:

struct ViewStyle {
// ...
init(backgroundColor: UIColor? = nil,
borderColor: UIColor? = nil,
borderWidth: CGFloat = 0,
cornerRadius: CGFloat = 0) {
self.backgroundColor = backgroundColor
self.borderColor = borderColor
self.borderWidth = borderWidth
self.cornerRadius = cornerRadius
}
// ...
}

However, subclasses suffer a tricky negative effect. What happens when a default cornerRadius of some subclass is not equal to zero? In such case, a subclass entity cannot distinguish between a default value and explicit zero value.
UIKit provides a reset-on-nil algorithm for many components’ properties. Examples can be found in UIView.backgroundColor, UILabel.font, UILable.textColor, etc. Let’s do the same.

All the Style’s properties need to be optional and nullified by default. Applicators must apply default values for all nil properties. Besides that, a substyle of any style can be nil, so applicators must be able to apply an optional style.

struct ViewStyle {
let backgroundColor: UIColor?
let borderColor: UIColor?
let borderWidth: CGFloat?
let cornerRadius: CGFloat?
init(backgroundColor: UIColor? = nil,
borderColor: UIColor? = nil,
borderWidth: CGFloat? = nil,
cornerRadius: CGFloat? = nil) {
self.backgroundColor = backgroundColor
self.borderColor = borderColor
self.borderWidth = borderWidth
self.cornerRadius = cornerRadius
}
}
extension UIView {
func apply(style: ViewStyle?) {
self.backgroundColor = style?.backgroundColor
self.layer.borderColor = style?.borderColor?.cgColor
self.layer.borderWidth = style?.borderWidth ?? 0
self.layer.cornerRadius = style?.cornerRadius ?? 0
}
}

This is much more compact! It is easy to read, maintain and extend.

At last! One style solution to rule them all…Or is it?

Of course, this solution is not a silver bullet. Here are at least a couple of problems we have encountered.

Occasional inability to reuse universal substyles.

In the previous example we defined a universal ViewStyle helper struct which has a backgroundColor property, but can it be used for any subclass of UIView? Take a look at this next simple View:

It’s just a circle with a border. For example, be used as an indicator in which case the style can be constructed with a substyle helper.

Also, the same View can be used as a color picker:

In this case, the View’s background color represents the View’s content. However, border color and width are still part of the style. The substyle ViewStyle helper cannot be used because it would override the background color provided by the content.

Depending on the context some properties can be a part of style/content

Let’s move on now to look at the next UI element. This is a button which is used as a dating profile badge. It shows that a user is interested in sports and does sometimes take exercise.

It has an image. Should it be a part of ButtonStyle or not? Well, it depends on the context. This part of a View can be specified by the View-level or by the model-level.

When specified by the View-level it means that this icon is always shown for this kind of button. Image is a part of the style.

If it is specified by the model, then another icon can be shown for this button at some point in time. Image is a part of the content. For example, an image can be a part of the content provided by a server. Presenter receives an image and shows it in the View.

So, in a common case image cannot be a part of ButtonStyle, but in a particular case, it can be a part of an ImagedButtonStyle.

Can we make it even better?

Yeap. The idea of Styles can be improved and extended. Here is the list of ideas which we’ve already use or going to use in the future:

  • Basic styles and styles’ components (colors, fonts, dimensions…) can be controlled by a design system for all the company’s platforms. And we have one.
  • View-components with all possible styles can be easily presented in a Gallery App for fast development.
  • Styles can be covered with Visual Regression Tests.
  • Basic styles and styles’ components (colors, fonts, dimensions…) can be stored as resources on a device and can be replaced at any time from a server-side.

Do you have any more ideas on how Styles could be used? How else would you tackle the issue of UI component reusability? Would ViewStyles work for you? Feel free to share your ideas in the comments section!

--

--