Development

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.

The 5 Minute Accessibility Strategy
Android Development iOS

The 5 Minute Accessibility Strategy

May 18, 2023

We discuss how you can make a plan in just 5 minutes to provide accessibility in your mobile app.

Read more
Build what matters: Prioritize value over feature count
Development Process

Build what matters: Prioritize value over feature count

August 1, 2024

Focusing on value delivery—rather than just feature count—combines your business goals with your users’ needs to achieve real software ROI.

Read more
How our Associates are using AI tools: Advice for early-career developers
Development

How our Associates are using AI tools: Advice for early-career developers

August 13, 2024

Our 2024 Associates at Michigan Labs share their experiences using AI tools like GitHub Copilot and ChatGPT in software development. They discuss how these tools have enhanced their productivity, the challenges they've faced, and provide advice for using AI effectively.

Read more
View more articles