Development

How to Build a Better App UI Architecture - Part 2

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

Following up on the importance of consistency and reusability in Part 1, let's look at how to optimize the UI system for better consistency and reusability. We’ll begin by breaking down the Laugh Out Loud app, then look at how to build an interfaces-based system. If you haven’t downloaded LOL all ready, here is the link.

Theme vs. Styles

The first thing you’ll notice about LOL is how bright and colorful it is. During onboarding, users are prompted to choose their favorite color. You can define your background color, avatar color, and more. Yet, so far, the app isn’t fully themeable; you can only change single views (styles), not the entire application (theme).

What can be done to support multiple themes? Are there enhancements that would allow us to consistently define views across the entire app?

Protocol-Oriented Themes and Styles

About half a year ago, I shared a topic on protocol-oriented programming during a lunch and learn. For the sake of brevity, I won’t go into the details of “Advantages of POP” or “What is the theme” here. Since “theme” defines the whole appearance, it should be a protocol that reflects the most basic customizable UI properties:

`swift protocol Theme {

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

} `

By adding extensions down the road, we can modify values and customize more properties.

We can also use a protocol to define styles that are applied to components:

`swift protocol Style {

associatedtype Component
func apply(to component: Component)

} `

Color Value

Because they are the most intuitive way for users to differentiate themes, colors play a key role in the UI system. How can we use colors to style our app? Systems rely on a range of color schemes. Google, for example, uses monochromatic shades of a single color (base color with light and dark variants within each shade) to define its system.

A system utilizing monochromatic type for base color palettes is composed of: one base color, one base white color, one base black color, four light variants, and four dark variants. The basic color palette protocol is defined as follows:

`swift protocol LightVariants {

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

} `

`swift protocol DarkVariants {

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

} `

`swift protocol ColorPalette: LightVariants, DarkVariants {

static var base: UIColor { get }
static var black: UIColor { get }
static var white: UIColor { get }

} `

Have the basic color palettes ready, next we can define out theme color palette:

`swift 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 }

} `

Implementation, Literal names, and Semantic Names

Notice that in ThemeColorPalette, we use semantic names for color variables but, when implementing the base ColorPalette, we may want to go with literal names. When is it best to use literal names vs. semantic names? When they serve as an independent element not tied to the theme.

As you would expect, literal names are more universally meaningful and can be shared or reused for other projects. For example:

`swift 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 color palette object:

`swift 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

} `

`swift 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

} `

`swift 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:

`swift struct PurpleTheme: Theme {

let name: String = "purple_theme"
let colorPalette: ThemeColorPalette.Type = PurpleThemeColorPalette.self

} `

`swift struct BlueTheme: Theme {

let name: String = "blue_theme"
let colorPalette: ThemeColorPalette.Type = BlueThemeColorPalette.self

} `

`swift struct DarkTheme: Theme {

let name: String = "dark_theme"
let colorPalette: ThemeColorPalette.Type = DarkThemeColorPalette.self

} `

Components and Composite Pattern

“Components let you split the UI into independent, reusable pieces, and think about each piece in isolation.”

A design system isn’t just about colors. Often we need to take custom views into account. For example, this view in LOL. Foundation components like JokeBodyText, TagLabel, Buttons, AvatarView are repeated throughout the app. There are also more complex components, such as HeaderView, CardView, and CardStack.

Atomic Design by Brad Frost lends insight on how to break down complex UI designs. Components should be independent and reusable across various views. Sometimes we need to isolate a component from the design to ensure it is decoupled. This results in specifications for a new component to be coded.

Even with a simple component there are a number of questions to consider. Take custom buttons, for example:

  • What is the style for differing states (enabled/disabled)?
  • Is there any animation/feedback on Clicked?
  • Are there any accessibility considerations?
  • How many buttons exist in the system?
  • And what are common in styles?

So how do we go about customizing buttons? A common approach is to create a subclass inheritance from UIButton and override the properties we want to customize. But this could be a problem as subclasses may inherit unnecessary functionality and data from the superclass. And the internals of superclasses get exposed through inheritance, which leads to unnecessary complexity being shared with classes. We always want to favor composition over inheritance.

Style is used to customize components such as buttons:

`swift protocol ButtonComponentStyle: Style where Component == UIButton {}

protocol Styleable {

associatedtype T: Style
func applyStyle(componenentStyle: T)

} `

`swift extension UIButton: Styleable {

typealias T = ButtonStyle

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

} `

`swift 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 PrimaryButton with primary tint color, bordered and rounded using the following styles composition:

`swift 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 applying the same technique to all UI elements and components, we can bring high-level styling control to our app.

Although this post doesn’t cover every step of mapping a design system to code, you can get an overall sense of the basic protocols and controls strategy. One benefit is that developers can extract styles from the design system and map them in a Swift file. This serves developers building a themeable, composable framework, making future UI updates that much easier.

In the next post, we’ll take a visit to Androidland. Stay tuned.

Mei Huang
Mei Huang
Software Developer

Stay in the loop with our latest content!

Select the topics you’re interested to receive our new relevant content in your inbox. Don’t worry, we won’t spam you.

How we approach UX @ MichiganLabs
Design

How we approach UX @ MichiganLabs

July 29, 2019

At Michigan Software Labs, we focus on building superior user experiences (UX) for our clients and their customers.

Read more
Product Strategy: Turning your App Ideas Into Reality
Process

Product Strategy: Turning your App Ideas Into Reality

April 16, 2020

Anyone who has been there knows, it can be incredibly frustrating having an app idea and not being able to bring it to life. And with organizational, technical, or financial roadblocks in the way, it can be equally frustrating to simply get a project off the ground.

Read more
The Measure Of Success: Staying On Track From The Very Beginning
Process

The Measure Of Success: Staying On Track From The Very Beginning

July 6, 2020

Any successful project begins with a clearly defined problem to solve (Are You Solving the Right Problem?). Project stakeholders need to be able to articulate the overriding challenge succinctly from the very start. (Spoiler: this often involves narrowing your vision, and may include…gasp…removing functionality.)

Read more
View more articles