Skip to Content
Course content

229: Practice Exercise: Building a Reusable Design System Component Library

Click on the "Edit" button in the top corner of the screen to edit your slide content.

I’ve seen this exact mistake in almost every professional codebase I've joined. A developer decides to "standardize" the UI by creating a reusable component, but they accidentally build a cage instead of a tool. Let's look at a PrimaryButton that looks correct at first glance but fails the moment the product requirements change.

struct PrimaryButton: View {
    let title: String
    let action: () -> Void

    var body: some View {
        Button(action: action) {
            Text(title)
                .fontWeight(.bold)
                .foregroundColor(.white)
                .padding()
                .frame(maxWidth: .infinity)
                .background(Color.blue)
                .cornerRadius(10)
        }
    }
}

The "String-Only" Constraint

The code above works great if every single button in your app is just a piece of text. But what happens when the designer comes to you and says, "We need a loading spinner inside the button when it's submitting," or "We need a small chevron icon at the end of the 'Continue' button"?

If you use the component above, you're stuck. You have to either add a bunch of optional parameters (let icon: Image?, let isLoading: Bool), which makes the initializer a nightmare, or you have to rewrite the component entirely. You've created a "reusable" component that is actually too rigid to be useful. I call this the "String-Only Trap."

Opening the Component with ViewBuilders

To build a real design system, you need to separate the styling (the blue background, the padding, the corner radius) from the content (the text, the icons, the spinners). The fix is to use a generic type for the content and the @ViewBuilder attribute.

struct PrimaryButton<Content: View>: View {
    let action: () -> Void
    let content: Content

    // We use @ViewBuilder so the caller can pass in multiple views (like an HStack)
    init(action: @escaping () -> Void, @ViewBuilder content: () -> Content) {
        self.action = action
        self.content = content()
    }

    var body: some View {
        Button(action: action) {
            content
                .fontWeight(.bold)
                .foregroundColor(.white)
                .padding()
                .frame(maxWidth: .infinity)
                .background(Color.blue)
                .cornerRadius(10)
        }
    }
}

Now, the PrimaryButton doesn't care what's inside it; it only cares how the container looks. You can now call it with a simple string, or a complex layout, and the design system's visual rules are still enforced.

// Simple use case
PrimaryButton(action: { print("Saved!") }) {
    Text("Save Changes")
}

// Complex use case
PrimaryButton(action: { print("Loading...") }) {
    HStack {
        ProgressView().tint(.white)
        Text("Processing")
    }
}

Centralizing Tokens with a Theme Configuration

Hardcoding Color.blue and cornerRadius(10) inside your components is the next mistake most people make. If the brand changes from blue to indigo, you don't want to hunt through fifty different component files to update a color.

I recommend creating a Theme namespace. This acts as the "Single Source of Truth" for your design system. Instead of using raw values, your components should reference these tokens.

enum AppTheme {
    enum Colors {
        static let primary = Color("BrandBlue")
        static let accent = Color("BrandGold")
        static let background = Color("SystemBackground")
    }
    
    enum Spacing {
        static let standardPadding: CGFloat = 16
        static let cornerRadius: CGFloat = 12
    }
}

When you update AppTheme.Spacing.cornerRadius, every component in your library updates instantly. This is the difference between a collection of random views and a professional design system. It gives you leverage over your entire UI.




📋 Practical Task

Exercise: Building a Theme-Aware Card Component System

Your goal is to build a reusable DesignSystemCard component that follows the principles of content flexibility and token-based styling.

Requirements:

  • Create a Theme struct or enum containing at least one primary color and one standard corner radius value.
  • Build a DesignSystemCard component using a generic Content: View and @ViewBuilder so that any combination of views (Text, Images, etc.) can be placed inside the card.
  • The card must have a style property (using a custom enum: .elevated or .outlined) that changes the background and border of the card.
  • Ensure the card uses the values defined in your Theme for padding and corner radii.
  • Implement a preview that shows the card in both styles, containing different content (e.g., one with just text, one with an image and a caption).
Rating
0 0

There are no comments for now.

to be the first to leave a comment.