A design system amalgamates three integral components, each playing a critical role in the software development lifecycle:
Visual Language.
Framework.
Guidelines.
Design systems are made up of:
Color Palette
Typography
Spacing and Alignment
Geometric Shapes
Icons
Images and Visual Assets
User Interactions
Animations
Reusable UI Components
Auditory Components
Visual language conveys the core values of the brand to end users. To summarize, a design system is a repository filled with components. Its main goal is to accelerate agile and cohesive development for both developers and designers. It is extremely important to understand that maintaining the design system is a joint effort between these two areas, albeit at a more expensive cost. At the same time, it is recommended to start developing a design system only when the project has strengthened its stylistic vision and corporate identity; otherwise, it may result in constant changes and modifications.
An alternative approach involves gradual implementation, in which specific standardized elements are established, laying the foundation for a holistic design system. Let's dive deeper into this methodology, focusing on creating color schemes and typographic guidelines in the context of developing a design system customized for the iOS platform using Swift, and then adding simple components using buttons as an example.
The starting point is to use the expertise of designers to create an extensive repository of the colors and fonts used in the project.
Moving on, let's create a model that controls the color scheme.
struct ColorModel {
let red: Double
let green: Double
let blue: Double
let alpha: Double
init(
red: Double = 0,
green: Double = 0,
blue: Double = 0,
alpha: Double = 0
) {
self.red = red / 255.0
self.green = green / 255.0
self.blue = blue / 255.0
self.alpha = alpha
}
init?(hex: String) {
var hexSanitized = hex.trimmingCharacters(in: .whitespacesAndNewlines)
hexSanitized = hexSanitized.replacingOccurrences(of: "#", with: "")
var rgb: UInt64 = 0
var r: CGFloat = 0.0
var g: CGFloat = 0.0
var b: CGFloat = 0.0
var a: CGFloat = 1.0
let length = hexSanitized.count
guard Scanner(string: hexSanitized).scanHexInt64(&rgb) else { return nil }
if length == 6 {
r = CGFloat((rgb & 0xFF0000) >> 16)
g = CGFloat((rgb & 0x00FF00) >> 8)
b = CGFloat(rgb & 0x0000FF)
} else if length == 8 {
r = CGFloat((rgb & 0xFF000000) >> 24)
g = CGFloat((rgb & 0x00FF0000) >> 16)
b = CGFloat((rgb & 0x0000FF00) >> 8)
a = CGFloat(rgb & 0x000000FF) / 255.0
} else {
return nil
}
self.init(red: r, green: g, blue: b, alpha: a)
}
}
After creating the model, the next step is to create a color scheme for our application.
public enum Palette {
case clear
case black
case black30
case black5
case white
}
extension Palette {
var color: ColorModel {
switch self {
case .clear: return .init(red: 0, green: 0, blue: 0, alpha: 0)
case .black: return .init(red: 0, green: 0, blue: 0, alpha: 1)
case .black30: return .init(red: 0, green: 0, blue: 0, alpha: 0.3)
case .black5: return .init(red: 0, green: 0, blue: 0, alpha: 0.05)
case .white: return .init(red: 255, green: 255, blue: 255, alpha: 1)
}
}
}
Transitioning from color to typography, we initiate a model for font management.
enum FontModel {
case medium(size: CGFloat)
case regular(size: CGFloat)
private var familyName: String { "CustomFontName" }
private var name: String {
switch self {
case .medium: return familyName + "-Medium"
case .regular: return familyName + "-Regular"
}
}
private var size: CGFloat {
switch self {
case let .medium(size): return size
case let .regular(size): return size
}
}
func evaluate<Result>(_ setup: (String, CGFloat) -> Result) -> Result {
setup(name, size)
}
}
Once the font model is operational, it's time to integrate the designers' curated font repository into the project.
public enum AppFont: String {
/// 60
case forKeyboard
/// 36
case h1Medium
/// 36
case h1Regular
/// 17
case h4Medium
/// 17
case h4Regular
}
extension AppFont {
private var font: FontModel {
switch self {
case .forKeyboard: return .medium(size: 60)
case .h1Medium: return .medium(size: 36)
case .h1Regular: return .regular(size: 36)
case .h4Medium: return .medium(size: 17)
case .h4Regular: return .regular(size: 17)
}
}
var suFont: Font {
font.evaluate(Font.custom(_:size:))
}
}
Having successfully created the color and font repositories, the next phase involves their integration into the OS API. This necessitates the development of extensions that complement standard UI components, effectively extending the system's capabilities.
public extension Color {
init(_ palette: Palette) {
let color = palette.color
self.init(red: color.red, green: color.green, blue: color.blue, opacity: color.alpha)
}
}
extension Color {
init(model: ColorModel) {
self.init(red: model.red, green: model.green, blue: model.blue, opacity: model.alpha)
}
}
extension Font {
public static func get(_ fontDecor: AppFont) -> Font {
fontDecor.suFont
}
}
extension View {
public func background(_ palette: Palette) -> some View {
background(palette.suColor)
}
public func foreground(_ palette: Palette) -> some View {
foregroundColor(palette.suColor)
}
public func font(_ fontstyle: AppFont) -> some View {
font(fontstyle.suFont)
}
}
Our designers have already charted out standardized states for application buttons.
Moving ahead, we'll set up a ButtonModel to serve as the nucleus for managing button configurations.
struct ButtonModel {
let background: Palette
let border: Palette
let font: AppFont = .h4Regular
let fontColor: Palette
let cornerRadius: CGFloat = 30
}
The next step is to create a Button Modifier - a universal tool for customizing buttons in our design system.
struct ButtonModifier: ViewModifier {
let setup: ButtonModel
func body(content: Content) -> some View {
content
.frame(maxWidth: .infinity, maxHeight: .infinity)
.font(setup.font)
.foreground(setup.fontColor)
.background(setup.background)
.cornerRadius(setup.cornerRadius)
.overlay(
RoundedRectangle(cornerRadius: setup.cornerRadius)
.stroke(setup.border.suColor, lineWidth: 1)
)
}
}
We'll extend our View with added convenience methods.
extension View {
var asAnyView: AnyView {
AnyView(self)
}
}
We'll make various button types using the ButtonKind enumeration.
public enum ButtonKind {
case black(title: String, action: () -> Void)
case whiteBordered(title: String, action: () -> Void)
}
private extension ButtonKind {
var active: ButtonModel {
switch self {
case .black: return .init(background: .black, border: .clear, fontColor: .white)
case .whiteBordered: return .init(background: .white, border: .black, fontColor: .black)
}
}
private var inactive: ButtonModel {
switch self {
case .black: return .init(background: .black5, border: .clear, fontColor: .black30)
case .whiteBordered: return .init(background: .black5, border: .black30, fontColor: .black30)
}
}
}
extension ButtonKind {
func modifier(_ isActive: Bool) -> ButtonModifier {
ButtonModifier(setup: isActive ? self.active : self.inactive)
}
func action(_ isActive: Bool) -> () -> Void {
guard isActive else { return {} }
switch self {
case let .black(_, action): return action
case let .whiteBordered(_, action): return action
}
}
func label() -> AnyView {
switch self {
case let .black(title, _): return Text(title).asAnyView
case let .whiteBordered(title, _): return Text(title).asAnyView
}
}
}
Bringing it all together, we'll introduce a new initializer for our button component.
extension Button {
public init(_ kind: ButtonKind, isActive: Bool = true) where Label == AnyView {
self.init(action: kind.action(isActive)) {
kind.label()
.modifier(kind.modifier(isActive))
.asAnyView
}
}
}
And finally, the buttons we've standardized can be easily added to the application
VStack {
Button(.black(title: "Primary", action: {}))
.frame(maxWidth: .infinity, maxHeight: 60)
Button(.black(title: "Primary", action: {}), isActive: false)
.frame(maxWidth: .infinity, maxHeight: 60)
Button(.whiteBordered(title: "Secondary", action: {}))
.frame(maxWidth: .infinity, maxHeight: 60)
Button(.whiteBordered(title: "Secondary", action: {}), isActive: false)
.frame(maxWidth: .infinity, maxHeight: 60)
}
.padding(.horizontal)
In essence, we've gone from laying the groundwork for the design system to refining iOS elements like buttons. We've made it easy to maintain, customized the look and feel to iOS standards, and created a clear categorization system with Pattern Factory. Factory allows you to create easy-to-maintain and scalable systems.