Development

How to Build a Better App UI Architecture - Part 2

November 19, 2019

Work xlarge

Fol­low­ing up on the impor­tance of con­sis­ten­cy and reusabil­i­ty in Part 1, let’s look at how to opti­mize the UI sys­tem for bet­ter con­sis­ten­cy and reusabil­i­ty. We’ll begin by break­ing down the Laugh Out Loud app, then look at how to build an inter­faces-based sys­tem. If you haven’t down­loaded LOL all ready, here is the link.

Theme vs. Styles

The first thing you’ll notice about LOL is how bright and col­or­ful it is. Dur­ing onboard­ing, users are prompt­ed to choose their favorite col­or. You can define your back­ground col­or, avatar col­or, and more. Yet, so far, the app isn’t ful­ly the­me­able; you can only change sin­gle views (styles), not the entire appli­ca­tion (theme).

What can be done to sup­port mul­ti­ple themes? Are there enhance­ments that would allow us to con­sis­tent­ly define views across the entire app?

Pro­to­col-Ori­ent­ed Themes and Styles

About half a year ago, I shared a top­ic on pro­to­col-ori­ent­ed pro­gram­ming dur­ing a lunch and learn. For the sake of brevi­ty, I won’t go into the details of Advan­tages of POP” or What is the theme” here. Since theme” defines the whole appear­ance, it should be a pro­to­col that reflects the most basic cus­tomiz­able UI properties:

protocol Theme {
var name: String { get }
var themeColorPalette: ThemeColorPalette.Type { get }

}

By adding exten­sions down the road, we can mod­i­fy val­ues and cus­tomize more properties.

We can also use a pro­to­col to define styles that are applied to components:

protocol Style {
associatedtype Component
func apply(to component: Component)

}

Col­or Value

Because they are the most intu­itive way for users to dif­fer­en­ti­ate themes, col­ors play a key role in the UI sys­tem. How can we use col­ors to style our app? Sys­tems rely on a range of col­or schemes. Google, for exam­ple, uses mono­chro­mat­ic shades of a sin­gle col­or (base col­or with light and dark vari­ants with­in each shade) to define its system.

A sys­tem uti­liz­ing mono­chro­mat­ic type for base col­or palettes is com­posed of: one base col­or, one base white col­or, one base black col­or, four light vari­ants, and four dark vari­ants. The basic col­or palette pro­to­col is defined as follows:

protocol LightVariants {
static var lightVariant02: UIColor { get }
static var lightVariant04: UIColor { get }
static var lightVariant06: UIColor { get }
static var lightVariant08: UIColor { get }

}

protocol DarkVariants {
static var darkVariant02: UIColor { get }
static var darkVariant04: UIColor { get }
static var darkVariant06: UIColor { get }
static var darkVariant08: UIColor { get }

}

protocol ColorPalette: LightVariants, DarkVariants {
static var base: UIColor { get }
static var black: UIColor { get }
static var white: UIColor { get }

}

Have the basic col­or palettes ready, next we can define out theme col­or palette:

protocol ThemeColorPalette {
static var primary: ColorPalette.Type { get }
static var secondary: ColorPalette.Type { get }
static var background: ColorPalette.Type { get }
static var surface: ColorPalette.Type { get }
static var error: ColorPalette.Type { get }

}

Imple­men­ta­tion, Lit­er­al names, and Seman­tic Names

Notice that in The­me­Col­or­Palette, we use seman­tic names for col­or vari­ables but, when imple­ment­ing the base Col­or­Palette, we may want to go with lit­er­al names. When is it best to use lit­er­al names vs. seman­tic names? When they serve as an inde­pen­dent ele­ment not tied to the theme.

As you would expect, lit­er­al names are more uni­ver­sal­ly mean­ing­ful and can be shared or reused for oth­er projects. For example:

struct PurpleColor: ColorPalette {
static let base = hexStringToUIColor("9C27B0")

}

struct BlueColor: ColorPalette {

static let base = hexStringToUIColor("2196F3")

}

struct RedColor: ColorPalette {

static let base = hexStringToUIColor("F44336")

}

struct GreyColor: ColorPalette {

static let base = hexStringToUIColor("9E9E9E")

}

Now we can build our themes col­or palette object:

struct PurpleThemeColorPalette: ThemeColorPalette {
static var primary: ColorPalette.Type = PurpleColor.self
static var secondary: ColorPalette.Type = PurpleColor.self
static var background: ColorPalette.Type = GreyColor.self
static var surface: ColorPalette.Type = GreyColor.self
static var error: ColorPalette.Type = RedColor.self

}

struct BlueThemeColorPalette: ThemeColorPalette {
static var primary: ColorPalette.Type = BlueColor.self
static var secondary: ColorPalette.Type = BlueColor.self
static var background: ColorPalette.Type = GreyColor.self
static var surface: ColorPalette.Type = GreyColor.self
static var error: ColorPalette.Type = RedColor.self

}

struct DarkThemeColorPalette: ThemeColorPalette {
static var primary: ColorPalette.Type = GreyColor.self
static var secondary: ColorPalette.Type = GreyColor.self
static var background: ColorPalette.Type = GreyColor.self
static var surface: ColorPalette.Type = GreyColor.self
static var error: ColorPalette.Type = RedColor.self

}

And now the theme object:

struct PurpleTheme: Theme {
let name: String = "purple_theme"
let colorPalette: ThemeColorPalette.Type = PurpleThemeColorPalette.self

}

struct BlueTheme: Theme {
let name: String = "blue_theme"
let colorPalette: ThemeColorPalette.Type = BlueThemeColorPalette.self

}

struct DarkTheme: Theme {
let name: String = "dark_theme"
let colorPalette: ThemeColorPalette.Type = DarkThemeColorPalette.self

}

Com­po­nents and Com­pos­ite Pattern

Com­po­nents let you split the UI into inde­pen­dent, reusable pieces, and think about each piece in isolation.”

A design sys­tem isn’t just about col­ors. Often we need to take cus­tom views into account. For exam­ple, this view in LOL. Foun­da­tion com­po­nents like Joke­Body­Text, TagLabel, But­tons, AvatarView are repeat­ed through­out the app. There are also more com­plex com­po­nents, such as Head­erView, Card­View, and CardStack.

Atom­ic Design by Brad Frost lends insight on how to break down com­plex UI designs. Com­po­nents should be inde­pen­dent and reusable across var­i­ous views. Some­times we need to iso­late a com­po­nent from the design to ensure it is decou­pled. This results in spec­i­fi­ca­tions for a new com­po­nent to be coded.

Even with a sim­ple com­po­nent there are a num­ber of ques­tions to con­sid­er. Take cus­tom but­tons, for example:

  • What is the style for dif­fer­ing states (enabled/​disabled)?
  • Is there any animation/​feedback on Clicked?
  • Are there any acces­si­bil­i­ty considerations?
  • How many but­tons exist in the system?
  • And what are com­mon in styles?

So how do we go about cus­tomiz­ing but­tons? A com­mon approach is to cre­ate a sub­class inher­i­tance from UIBut­ton and over­ride the prop­er­ties we want to cus­tomize. But this could be a prob­lem as sub­class­es may inher­it unnec­es­sary func­tion­al­i­ty and data from the super­class. And the inter­nals of super­class­es get exposed through inher­i­tance, which leads to unnec­es­sary com­plex­i­ty being shared with class­es. We always want to favor com­po­si­tion over inheritance.

Style is used to cus­tomize com­po­nents such as buttons:

protocol ButtonComponentStyle: Style  where  Component == UIButton {}

protocol Styleable {

associatedtype T: Style
func applyStyle(componenentStyle: T)

}

extension UIButton: Styleable {
typealias T = ButtonStyle

func applyStyle(componenentStyle: ButtonStyle) {
    componenentStyle.apply(to: self)
}

}

enum ButtonBaseStyle: ButtonComponentStyle {
case primaryTint
case rounded
case bordered
case disabled
func apply(to component: UIButton) {
    switch self {
    case .primaryTint:
        component.backgroundColor = currentTheme.colorPalette.primary.base
    case .bordered:
        component.layer.borderColor = currentTheme.colorPalette.primary.darkVariant08.cgColor
        component.layer.borderWidth = design.borderWidth.small
    case .rounded:
        component.layer.cornerRadius = design.cornerRadius.small
    case .disabled:
        component.backgroundColor = currentTheme.colorPalette.primary.lightVariant02
    }
}

}

Next, we can build a Pri­ma­ry­But­ton with pri­ma­ry tint col­or, bor­dered and round­ed using the fol­low­ing styles composition:

class PrimaryButton: UIButton, Styleable {
typealias T = ButtonBaseStyle

func applyStyle(componentStyle: ButtonBaseStyle) {
    componenentStyle.apply(to: self)
}

override init(frame: CGRect) {
    super.init(frame: frame)
    self.applyStyle(componenentStyle: .bordered)
    self.applyStyle(componenentStyle: .primaryTint)
    self.applyStyle(componenentStyle: .rounded)
}

required init?(coder aDecoder: NSCoder) {
    fatalError("init(coder:) has not been implemented")
}

}

By apply­ing the same tech­nique to all UI ele­ments and com­po­nents, we can bring high-lev­el styling con­trol to our app.

Although this post doesn’t cov­er every step of map­ping a design sys­tem to code, you can get an over­all sense of the basic pro­to­cols and con­trols strat­e­gy. One ben­e­fit is that devel­op­ers can extract styles from the design sys­tem and map them in a Swift file. This serves devel­op­ers build­ing a the­me­able, com­pos­able frame­work, mak­ing future UI updates that much easier.

In the next post, we’ll take a vis­it to Android­land. Stay tuned.

Ready to get started?

Call us at 616-594-0269 or send us a note below.
Visit our office @ 452 Ada Drive SE Suite 300 Ada, Michigan 49301
Send us an e-mail @ info@michiganlabs.com