Leveraging Enums for Flexible Button Styling in SwiftUI


Greetings, traveler!

Enums in Swift are a powerful tool for defining a set of related values in a type-safe way. Beyond their basic use for representing fixed options, enums can serve as a foundation for more complex logic, enabling clean and maintainable code in iOS applications. In this article, we’ll explore how enums can be used to create a customizable button styling system in SwiftUI, using a practical example to demonstrate their flexibility.

The Power of Enums in SwiftUI

Enums allow developers to encapsulate related configurations, making it easier to manage variations of UI components like buttons. By associating properties and behavior with each enum case, you can define a single source of truth for styling, reducing duplication, and improving code clarity. In our example, we’ll use an enum to define different button styles, each with its own appearance and behavior based on user interaction states.

Defining the ButtonKind Enum

Consider a SwiftUI application where buttons need to support multiple styles, such as blue, red, gray, and white-bordered variants. Each style requires specific colors for the background, title, and border, with variations for regular, disabled, and pressed states. Additionally, each style may have distinct corner radii, heights, and fonts.

Here’s how we can structure the ButtonKind enum to handle these requirements:

enum ButtonKind {
    typealias ComponentColor = (
        background: Color,
        title: Color,
        border: Color
    )
    
    private typealias StateColor = (
        regular: Color,
        disabled: Color,
        pressed: Color
    )
    
    case blue, red, gray, whiteBordered
}

The ButtonKind enum defines four cases: blue, red, gray, and whiteBordered. Two type aliases are introduced to streamline the code:

  • ComponentColor: A tuple representing the background, title, and border colors for a button.
  • StateColor: A tuple defining colors for the regular, disabled, and pressed states of a single component.

Configuring Button Properties

To make the enum versatile, we extend it to provide properties like cornerRadius, height, and font, which vary by button kind:

extension ButtonKind {
    var cornerRadius: CGFloat {
        switch self {
        case .blue, .red, .gray, .whiteBordered: 12
        }
    }
    
    var height: CGFloat {
        switch self {
        case .blue, .red, .gray: 44
        case .whiteBordered: 50
        }
    }
    
    var font: Font {
        switch self {
        case .blue, .red, .gray: .body
        case .whiteBordered: .callout
        }
    }
}

Here, the whiteBordered button has a slightly taller height (50 points) and a smaller font (.callout) compared to the other styles, which use a standard height of 44 points and a .body font. The corner radius is consistent at 12 points for all styles, but the structure allows for easy customization.

Managing Component Colors

Each button style requires distinct colors for its background, title, and border, with variations for different interaction states. We define these colors using the StateColor tuple and provide a method to compute the appropriate ComponentColor based on the button’s state:

extension ButtonKind {
    private var titleColor: StateColor {
        switch self {
        case .blue, .red:
            (.white, .white, .white)
        case .gray:
            (.black, .black.opacity(0.5), .black.opacity(0.5))
        case .whiteBordered:
            (.black, .black.opacity(0.5), .black.opacity(0.5))
        }
    }
    
    private var backgroundColor: StateColor {
        switch self {
        case .blue:
            (.blue, .blue.opacity(0.5), .blue.opacity(0.5))
        case .red:
            (.red, .red.opacity(0.5), .red.opacity(0.5))
        case .gray:
            (.gray, .gray.opacity(0.5), .gray.opacity(0.5))
        case .whiteBordered:
            (.white, .white, .white)
        }
    }
    
    private var borderColor: StateColor {
        switch self {
        case .blue, .red, .gray:
            (.clear, .clear, .clear)
        case .whiteBordered:
            (.black, .black.opacity(0.5), .black.opacity(0.5))
        }
    }
    
    func componentColor(isPressed: Bool, isEnabled: Bool) -> ComponentColor {
        (
            backgroundColor(isPressed: isPressed, isEnabled: isEnabled),
            titleColor(isPressed: isPressed, isEnabled: isEnabled),
            borderColor(isPressed: isPressed, isEnabled: isEnabled)
        )
    }
}

Each property (titleColor, backgroundColor, borderColor) returns a StateColor tuple with colors for the regular, disabled, and pressed states. The componentColor(isPressed:isEnabled:) method combines these into a ComponentColor tuple, selecting the appropriate color for each component based on the button’s state. For example:

  • The blue and red buttons use a white title color across all states.
  • The whiteBordered button has a black border that fades to 50% opacity when disabled or pressed.
  • The gray button uses a gray background with a black title, both fading to 50% opacity in non-regular states.

The state selection logic is handled by helper methods:

extension ButtonKind {
    private func backgroundColor(isPressed: Bool, isEnabled: Bool) -> Color {
        if isPressed {
            self.backgroundColor.pressed
        } else if !isEnabled {
            self.backgroundColor.disabled
        } else {
            self.backgroundColor.regular
        }
    }
    
    private func titleColor(isPressed: Bool, isEnabled: Bool) -> Color {
        if isPressed {
            self.titleColor.pressed
        } else if !isEnabled {
            self.titleColor.disabled
        } else {
            self.titleColor.regular
        }
    }
    
    private func borderColor(isPressed: Bool, isEnabled: Bool) -> Color {
        if isPressed {
            self.borderColor.pressed
        } else if !isEnabled {
            self.borderColor.disabled
        } else {
            self.borderColor.regular
        }
    }
}

These methods ensure the correct color is chosen based on whether the button is pressed or disabled, maintaining a consistent logic across all components.

Applying the Style in SwiftUI

To integrate this with SwiftUI, we create a custom ButtonStyle and extend the Button type for easy application:

extension Button {
    func style(_ kind: ButtonKind) -> some View {
        buttonStyle(CustomButtonStyle(kind))
    }
}

private struct CustomButtonStyle: ButtonStyle {
    @Environment(\.isEnabled) var isEnabled
    
    private let kind: ButtonKind
    
    init(_ kind: ButtonKind) {
        self.kind = kind
    }
    
    func makeBody(configuration: Configuration) -> some View {
        let color = kind.componentColor(
            isPressed: configuration.isPressed,
            isEnabled: isEnabled
        )
        
        return configuration.label
            .padding()
            .foregroundStyle(color.title)
            .font(kind.font)
            .frame(height: kind.height)
            .frame(maxWidth: .infinity)
            .background(
                color.background,
                in: RoundedRectangle(cornerRadius: kind.cornerRadius)
            )
            .background(
                RoundedRectangle(cornerRadius: kind.cornerRadius)
                .stroke(
                    color.border,
                    lineWidth: 2
                )
            )
    }
}

The CustomButtonStyle struct uses the ButtonKind enum to configure the button’s appearance. It retrieves the isEnabled state from the environment and the isPressed state from the button’s configuration. The button’s label is styled with the appropriate font, title color, background color, and border, all derived from the ButtonKind enum. The button is set to fill the available width and uses a fixed height, with a rounded rectangle shape for both the background and border.

Using the Custom Button

To apply the style, you can use the .style(_:) modifier on a SwiftUI Button:

Button("Tap Me") {
    // Action
}
.style(.blue)

This creates a blue button with a white title, a 44-point height, and a 12-point corner radius. The button automatically adjusts its appearance when pressed or disabled, thanks to the logic in ButtonKind.

Why Use Enums for This?

Using an enum like ButtonKind centralizes the configuration for each button style, making it easy to add new styles or modify existing ones without changing the button’s implementation. The enum encapsulates all style-related logic, ensuring consistency and reducing the risk of errors. For example, adding a new style, such as a green button, requires only a new case in ButtonKind and corresponding color, font, and size definitions.

Enums also make the code more readable and maintainable. Instead of scattering style properties across multiple views or relying on hardcoded values, developers can reference a single ButtonKind case to apply a consistent look and behavior.

Conclusion

Enums in Swift provide a robust way to manage complex UI configurations, as demonstrated by this button styling system in SwiftUI. By leveraging enums, developers can create flexible, reusable, and type-safe solutions that simplify UI customization. The ButtonKind example shows how a single enum can handle multiple properties and states, streamlining the process of building consistent and interactive UI components. This approach can be extended to other UI elements, such as text fields or cards, making enums a valuable tool in any iOS developer’s toolkit.