How to Build a Better App UI Architecture - Part 2

November 19, 2019
How to Build a Better App UI Architecture - Part 2

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.

Mei Huang
Mei Huang
Software Developer

Looking for more like this?

Sign up for our monthly newsletter to receive helpful articles, case studies, and stories from our team.

Information Experience can make or break a product
Design Process

Information Experience can make or break a product

January 4, 2023

Kai discusses how writing impacts user experience, providing an overview of the types of writing that are involved in product development and how to approach it from a very high level.

Read more
Make an AI Art Generating Slack Bot in 10 Minutes

Make an AI Art Generating Slack Bot in 10 Minutes

February 3, 2023

David shares how easy it is to create your own AI art generating Slack bot using Typescript and Stable Diffusion.

Read more
Business Model Canvas: Helping business leaders create meaningful digital products
Business Process

Business Model Canvas: Helping business leaders create meaningful digital products

January 17, 2024

When it comes to custom software, it’s easy to feel stuck between needing to deliver value quickly, while also ensuring meaningful, long-lasting results. Learn why the Business Model Canvas can help you identify the right digital product for your business needs.

Read more
View more articles