Hierarchical Styling: My Clean Approach to SwiftUI Design
When working with SwiftUI, I recently discovered an elegant feature of hierarchical styles that changed how I approach design implementation. Instead of directly appending styles to a ShapeStyle
, we can leverage the parent's .tint
property for more flexible and maintainable styling.
The Traditional Approach
Let's start with a basic example - a Form
containing a single Label
:
Form {
Label("Battery charging options", systemImage: "battery.100percent")
}
By default, this produces a simple, un-styled view:
While functional, I wanted to recreate something closer to Apple's Settings app, where icons are beautifully wrapped in coloured rounded rectangles.
I particularly appreciate using varying opacities of a single colour to create harmonious, complementary styles.
First Attempt: Direct Colour Assignment
My initial solution looked like this:
struct SettingsFormLabel: LabelStyle {
let foregroundColor: any ShapeStyle
let backgroundColor: any ShapeStyle
func makeBody(configuration: Configuration) -> some View {
Label {
configuration.title
} icon: {
configuration.icon
.foregroundStyle(AnyShapeStyle(foregroundColor))
.background {
RoundedRectangle(cornerRadius: 6)
.fill(AnyShapeStyle(backgroundColor))
.frame(width: 36, height: 36)
}
}
}
}
To use this style, you would write:
Form {
Label("Battery charging options", systemImage: "battery.100percent")
.labelStyle(SettingsFormLabel(
foregroundColor: Color.green,
backgroundColor: .green.quinary
))
}
This produces a styled view with our green theme:
While this works, it requires specifying both foreground and background colours every time you use the label style. This approach can become tedious and harder to maintain across larger applications.
A Better Way: Hierarchical Styling
Here's where hierarchical styling shines. By leveraging SwiftUI's built-in .tint
modifier, we can create a more elegant solution:
struct SettingsFormLabel: LabelStyle {
func makeBody(configuration: Configuration) -> some View {
Label {
configuration.title
} icon: {
configuration.icon
.foregroundStyle(.primary) // <-- here
.background {
RoundedRectangle(cornerRadius: 6)
.fill(.quinary) // <-- here
.frame(width: 36, height: 36)
}
}
}
}
Form {
Label("Battery charging options", systemImage: "battery.100percent")
.labelStyle(SettingsFormLabel())
}
.tint(.green)
This approach offers several advantages:
- Cleaner, more maintainable code
- Single point of colour control through the
.tint
modifier - Easier to make app-wide colour scheme changes
The end result looks identical, but the code is more elegant and easier to maintain. By using hierarchical styling, we've simplified our implementation while maintaining full control over the visual design.
It's important to note that the .tint
needs to be on the Form
- which affects all the .labelStyle
s. As noted by Michael Freiwald you need to use .foregroundStyle
to change them on the Label
directly.
If you found this article helpful, you can keep the ideas flowing by supporting me. Buy me a coffee or check out my apps to help me create more content like this!
Coffee Check out my apps