If you’ve worked on any large development projects, you’ll likely have encountered a design system. A design system is a library that encapsulates your branding into reusable components, colors, fonts, icons, and motifs, to help ensure a consistent style across your products.
In short, they allow us to avoid re-inventing the wheel for every screen of UI design.
Personally, I love working on things to make life easier for my team. Implementing design systems in a clean, straightforward way has huge leverage since your team will work with it every day.
In my career, I’ve implemented various design systems in my apps, and explored many diverse approaches on iOS, such as:
Every approach has its place, but in SwiftUI I’ve found the cleanest, most useable approach by far is defining your components using enums.
As you read through this post, I’ve added many code snippets, but feel free to check out my code from Github and follow along.
Let’s say your designer has just sent you a link to their designs — it’s the button component they’ve been working on. It might look something like this:
Generally speaking, any designer worthy of their Figma free tier or Sketch license will have a small set of styles, in a small palette of brand colors, and multiple sizes, icons, and alignments.
You might feel the gears turning in your head as you start to realise why Enums might be a strong approach here.
Let’s start by defining your Button. Since we’re in SwiftUI-land, we can start by defining a View
struct.
import SwiftUI
public struct MyButton: View {
private let title: String
private let action: () -> Void
public init(title: String,
action: @escaping () -> Void) {
self.title = title
self.action = action
}
public var body: some View {
Button(action: action, label: {
// button UI goes here
})
}
}
Here, for simplicity’s sake, I’ve called it MyButton
, but generally, you will usually prefix your company name — in my time I’ve built DeloitteButton
, CarbnButton
, and Gener8Button
. Personal project? Feel free to name it [YourFirstName]Button
!
To start with, we simply initialise the button the same way we initialise a default SwiftUI button — with a title and an on-tap action.
Now that we have the basics in place, we can start to make it interesting.
We can start with the most simple features of the buttons in the design system, and work down through the levels of complexity and customisation. So to start, let’s deal with the colors.
In the design system above, we’ve got 3 main colors:
We can create these options in a MyButtonColor
enum to handle this cleanly:
public struct MyButton: View {
public enum MyButtonColor {
case `default`
case accent
case error
var mainColor: Color {
switch self {
case .`default`: return .blue
case .accent: return .green.opacity(0.85)
case .error: return .red.opacity(0.6)
}
}
var detailColor: Color {
switch self {
case .`default`: return .white
case .accent: return .white
case .error: return .white
}
}
}
// ...
}
Here, we’re harnessing the astonishing power of Swift’s enumerations — when we add computed properties like mainColor
and detailColor
to the enum, your case switches over self
and finds the value defined for itself!
Quick explainer: Since default
is a Swift keyword, we need to ‘escape’ it with back-ticks if we want to call one of our enum cases default
, or the compiler will get confused.
Now we can modify our MyButton
’s body
property to include these color properties when drawing:
public var body: some View {
Button(action: action, label: {
buttonWithColor
})
}
private var buttonWithColor: some View {
Text(title)
.foregroundColor(type.detailColor)
.background(
Capsule()
.fill(type.mainColor)
)
}
Now, finally, we can modify the initialiser of MyButton
to expose this type as an argument. Since our .default
button color is the most commonly used, we should use it as the default argument for color
in this initialiser:
private let color: MyButtonColor
private let title: String
private let action: () -> Void
public init(color: MyButtonColor = .`default`,
title: String,
action: @escaping () -> Void) {
self.color = color
self.title = title
self.action = action
}
Now, your team can use MyButton
the exact same way it uses a default SwiftUI Button
and draw our .default
(blue) button. Our team can also use the more complex version of the initialiser when they want to override the default colour for the .accent
(green) or .error
(red) variants.
Interlude: Progressive Disclosure
My approach is inspired by Apple’s WWDC22 video, The craft of SwiftUI API design: Progressive disclosure.
This talk draws a comparison between progressive disclosure in UI design with progressive disclosure in API design. Progressive disclosure is the concept of making basic options easy to access, but enabling more complex use cases to be revealed as you need them.
Apple are the masters of this; from their operating system (Dock → Finder → zsh shell) to the brand-new Swift concurrency features (async/await → Task → actors & task groups).
The key to API design is trying to make the most common use cases easily accessible, with intuitive defaults. You can optimise the call site of your API so it’s easy to either roll with the defaults or pick the best approach for your use case.
According to Apple, a crucial aspect of progressively-disclosed API design is composability over enumerability. This doesn’t literally mean “don’t use enums” — it means we should allow consumers of our API the freedom to customise where possible.
In this instance, we’re going to create a new enum, MyButtonIcon
, which takes advantage of enum Associated Values to allow any Image we like to be added to the button!
public enum MyButtonIcon {
case leading(_ icon: Image)
case trailing(_ icon: Image)
}
private let color: MyButtonColor
private let icon: MyButtonIcon?
private let title: String
private let action: () -> Void
public init(color: MyButtonColor = .`default`,
icon: MyButtonIcon? = nil,
title: String,
action: @escaping () -> Void) {
self.color = color
self.icon = icon
self.title = title
self.action = action
By default, we don’t want any icon on our button — therefore, we should make this MyButtonIcon
property optional with the default value as nil.
Now we can set this up in our MyButton
’s body
:
public var body: some View {
Button(action: action, label: {
buttonForColor
})
}
private var buttonForColor: some View {
buttonContent
.foregroundColor(color.detailColor)
.background(
Capsule()
.fill(color.mainColor)
)
}
private var buttonContent: some View {
HStack(spacing: 16) {
if case .leading(let image) = icon {
iconView(for: image)
}
Text(title)
if case .trailing(let image) = icon {
iconView(for: image)
}
}
}
private func iconView(for image: Image) -> some View {
image
.renderingMode(.template)
.resizable()
.aspectRatio(contentMode: .fit)
.frame(width: 20, height: 20)
}
Here, we create a new buttonContent
subview in the buttonForColor
, which contains a SwiftUI HStack
. Based on the enum case for our icon
, we unwrap the optional on either the leading or trailing side of the original button Text
.
Using optional pattern matching, we can take the associated value for the Image out of the enum and place it in our view.
We pass this into a new iconView(for image: Image)
method to add standard SwiftUI image customisation to it, including setting renderingMode
to .template
, which means its color will be set to our MyButtonColor
’s detailColor
.
After implementing more enums to handle size and style, we’re left with an initialiser that looks like this:
public init(type: MyButtonType = .primary,
color: MyButtonColor = .`default`,
size: MyButtonSize = .large,
icon: MyButtonIcon? = nil,
title: String,
action: @escaping () -> Void) {
self.type = type
self.color = color
self.size = size
self.icon = icon
self.title = title
self.action = action
}
We’re embodying the principles of progressive disclosure here:
Button
, with a title string and an action, but developers can customise it easily when needed.When we actually use the button in our Views, we can invoke it with no customisation:
MyButton(title: "Press me!", action: { didPressButton() })
Or we can utilise all the features we’ve set up for a more customised button:
MyButton(type: .secondary,
color: .accent,
size: .small,
icon: .trailing(.Icon.profile),
title: "Press me!",
action: { goToProfile() })
Please take a look at the full sample code for MyButton on Github, along with a mini-project displaying all the possible variants in a design library.
Through building MyButton
, not only have we created a useful UI component – we’ve hit on many of the powerful features of the Swift type system:
MyButton
initialiser offers progressive disclosureI hope after reading this, you will think about utilising Enums in your code to bring design systems to life in SwiftUI.
Source: Medium