-
Section 1: Getting Started
-
Section 2: Core Syntax
-
Section 3: Optionals
-
Section 4: Object-Oriented and Value Types
-
Section 5: Memory Management
-
Section 6: Generics and Error Handling
-
Section 7: Concurrency
-
Section 8: Working with Collections
-
Section 9: Codable and Data Handling
-
Section 10: Protocol-Oriented Programming
-
Section 11: Testing and Tooling
-
Section 12: Practical Projects
-
Section 13: Interview Practice
-
Section 14: More Practice Exercises
-
Section 15: More Standard Library
-
Section 16: Advanced Concurrency
-
Section 17: Foundation Framework Deep Dive
-
Section 18: URLSession and Networking Deep Dive
-
Section 19: Combine Framework
-
Section 20: SwiftUI Fundamentals for Swift Developers
-
Section 21: Server-Side Swift with Vapor
-
Section 22: Swift Package Manager Deep Dive
-
Section 23: Swift Concurrency Deep Dive
-
Section 24: More Language Features
-
Section 25: Error Handling Deep Dive
-
Section 26: Testing Deep Dive
-
Section 27: Data Structures and Algorithms in Swift
-
Section 28: More Practice Exercises
-
Section 29: More Interview Practice
-
Section 30: Swift Macros (Swift 5.9+)
-
Section 31: Property Wrappers Ecosystem
-
Section 32: Swift Interop Deep Dive
-
Section 33: iOS App Architecture Patterns
-
Section 34: Performance and Debugging
-
Section 35: App Distribution and CI/CD
-
Section 36: More Practical Projects
-
Section 37: SwiftData and Persistence
-
Section 38: More Design Patterns
-
Section 39: More Review and Practice
-
Section 40: More Foundation Deep Dive
-
Section 41: Advanced Collections in Swift
-
Section 42: Advanced Generics Practice
-
Section 43: UIKit for Legacy and Hybrid Apps
-
Section 44: watchOS and visionOS Development Basics
-
Section 45: More Networking Patterns
-
Section 46: More Testing Practice
-
Section 47: Accessibility in Swift Apps
-
Section 48: Localization
-
Section 49: More Practical Projects Round 2
-
Section 50: Swift Charts Framework
-
Section 51: More Interview and Algorithm Practice
-
Section 52: Final Practice and Mastery
-
Section 53: Swift Compiler and Build System
-
Section 54: More Concurrency Practice
-
Section 55: App Store Guidelines and Review
-
Section 56: More Design and Architecture
229: Practice Exercise: Building a Reusable Design System Component Library
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
Themestruct or enum containing at least one primary color and one standard corner radius value. - Build a
DesignSystemCardcomponent using a genericContent: Viewand@ViewBuilderso that any combination of views (Text, Images, etc.) can be placed inside the card. - The card must have a
styleproperty (using a custom enum:.elevatedor.outlined) that changes the background and border of the card. - Ensure the card uses the values defined in your
Themefor 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).
There are no comments for now.