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:
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:
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:
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 color palettes ready, next we can define out theme color 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 }
}
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:
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:
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
}
Components and Composite Pattern
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:
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 PrimaryButton with primary tint color, bordered and rounded using the following 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 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.
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
May 18, 2023We discuss how you can make a plan in just 5 minutes to provide accessibility in your mobile app.
Read moreBuild what matters: Prioritize value over feature count
August 1, 2024Focusing on value delivery—rather than just feature count—combines your business goals with your users’ needs to achieve real software ROI.
Read moreHow our Associates are using AI tools: Advice for early-career developers
August 13, 2024Our 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