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.
If you enjoyed this article, please feel free to follow me on my social media:
It might be interesting:
- How to access UIHostingController from a SwiftUI View
- Simplifying Data Access in SwiftUI with @dynamicMemberLookup
- Three Practical Tools for Managing References and Detecting Memory Leaks in Swift
- Paging with Peek: Three Ways to Implement Paginated Scroll in SwiftUI
- Rendering HTML Text in SwiftUI with Custom Link Styling