SwiftQA Random

How do you design reusable SwiftUI components?

“Reusable” in SwiftUI specifically means designing around its styling/configuration conventions rather than just extracting a view into its own file.

1. Configure via parameters, not hardcoded content — the baseline

struct PrimaryButton: View {
    let title: String
    let icon: String?
    let action: () -> Void
    init(_ title: String, icon: String? = nil, action: @escaping () -> Void) {
        self.title = title
        self.icon = icon
        self.action = action
    }
    var body: some View {
        Button(action: action) {
            HStack {
                if let icon { Image(systemName: icon) }
                Text(title)
            }
        }
        .buttonStyle(.borderedProminent)
    }
}

Sensible defaults for optional parameters keep the common case simple while allowing customization.

2. Use ViewBuilder for structural composition, not just data parameters

struct Card<Content: View>: View {
    @ViewBuilder let content: Content
    var body: some View {
        VStack(alignment: .leading, spacing: 8) { content }
            .padding()
            .background(.regularMaterial)
            .clipShape(RoundedRectangle(cornerRadius: 12))
    }
}

Card doesn’t care what’s inside it, only that it provides consistent chrome around arbitrary content.

3. Prefer view modifiers over parameter explosion for optional behavior

struct BadgeModifier: ViewModifier {
    let count: Int
    func body(content: Content) -> some View {
        content.overlay(alignment: .topTrailing) {
            if count > 0 {
                Text("\(count)")
                    .font(.caption2)
                    .padding(4)
                    .background(.red, in: Circle())
            }
        }
    }
}
extension View {
    func badge(count: Int) -> some View { modifier(BadgeModifier(count: count)) }
}

This scales better than a component anticipating every combination of optional decorations up front.

4. Custom ButtonStyle/LabelStyle/ToggleStyle for consistent interactive behavior

struct PillButtonStyle: ButtonStyle {
    func makeBody(configuration: Configuration) -> some View {
        configuration.label
            .padding(.horizontal, 16)
            .padding(.vertical, 8)
            .background(.tint, in: Capsule())
            .opacity(configuration.isPressed ? 0.7 : 1.0)
    }
}

This applies to any button anywhere, decoupling “how it looks” from “what it does” — more reusable than a bespoke wrapper view.

5. Design system tokens instead of hardcoded values

enum DesignTokens {
    enum Spacing { static let small: CGFloat = 8; static let medium: CGFloat = 16 }
    enum Colors { static let primaryAction = Color("PrimaryAction") }
}

Changing the design system updates every component consistently — ties back to the shared-module discussion, a DesignSystem module multiple feature teams depend on.

6. Respect the environment rather than fighting it

struct StatCard: View {
    @Environment(\.dynamicTypeSize) private var typeSize
    let value: String
    let label: String
    var body: some View {
        if typeSize.isAccessibilitySize {
            VStack { Text(value); Text(label) }
        } else {
            HStack { Text(value); Text(label) }
        }
    }
}

A component that hardcodes layout assumptions breaks the moment it’s reused somewhere with different constraints.

7. Keep state ownership external when the component is meant to be controlled, internal when it’s self-contained

// Caller controls state
struct SearchField: View {
    @Binding var text: String
    var body: some View { TextField("Search", text: $text) }
}

// Component owns its own state
struct ExpandableSection: View {
    @State private var isExpanded = false
}

Getting this wrong in either direction hurts reusability.

8. Accessibility as a built-in property of the component, not an afterthought per call site

struct IconButton: View {
    let systemName: String
    let accessibilityLabel: String  // required, not optional/forgettable
    let action: () -> Void
    var body: some View {
        Button(action: action) { Image(systemName: systemName) }
            .accessibilityLabel(accessibilityLabel)
    }
}

Making it a required parameter prevents an entire class of accessibility regressions.

9. Preview coverage for every meaningful variant

#Preview("Default") { PrimaryButton("Continue") {} }
#Preview("Dark Mode") { PrimaryButton("Continue") {}.preferredColorScheme(.dark) }
#Preview("Accessibility Size") { PrimaryButton("Continue") {}.dynamicTypeSize(.accessibility3) }

10. Where reusability stops being worth it — the judgment call

Don’t extract a component prematurely. A view used exactly once doesn’t need ViewBuilder flexibility or design-token abstraction. Extract once you have (or clearly anticipate) multiple real consumers with varying needs.

Interview-ready summary

TechniqueWhat it buys
Parameters with sensible defaultsSimple common case, customizable when needed
@ViewBuilder content closuresStructural flexibility beyond fixed data fields
Custom ViewModifiersComposable optional behavior without parameter explosion
Custom ButtonStyle/LabelStyleReuse across any button/label, not just a bespoke wrapper
Design tokensConsistent, centrally-updatable styling
Respecting @EnvironmentCorrectness across dark mode, dynamic type, layout direction
Deliberate state ownershipRight integration ergonomics per use case
Required accessibility parametersPrevents accessibility regressions by construction
Preview coverage of variantsCatches breakage in the component, not at each call site

One-liner if pressed: “I’d design around SwiftUI’s own composition primitives rather than just extracting a view — @ViewBuilder for structural flexibility, custom ButtonStyle/ViewModifiers for composable optional behavior instead of parameter explosion, design tokens instead of hardcoded values, and explicit attention to environment values like dynamic type and dark mode so the component is correct in contexts I didn’t originally write it for — but I’d only extract into this level of reusable component once there’s a real second consumer, not speculatively on first use.”