Label
One of the new SwiftUI components from this year is Label
. The SwiftUI team did is a great job with Label
’s documentation: if you’re not familiar with this view yet, please have a read at it first.
Label
combines a text and and an image in a single view, it also adapts based on the context (e.g. if it’s put on a toolbar) and dynamic type.
In this article, let’s explore this view beyond the basics.
Initializers
Label
comes with six initializers:
- the first four offer all possible combinations of text as
StringProtocol
orLocalizedStringKey
, and an image from an assets catalog or SF Symbols - the most flexible initializer takes two generics views, no strings attached
- the last initializer takes in a
LabelStyleConfiguration
All initializers have their place and use: we will cover all of them in this article.
Label styles
Unless we’re in a special context (e.g. a navigation bar), by default both the Label
title and image are displayed.
If we want to show only one of the two components (either only the image, or only the title), or change our Label
appearance in another way, we can do so via the labelStyle(_:)
view modifier:
this modifier accepts a LabelStyle
instance.
LabelStyle
tells SwiftUI how we would like the Label
to be drawn on screen, by default we have three options:
IconOnlyLabelStyle()
TitleOnlyLabelStyle()
DefaultLabelStyle()
The names are self-explanatory. These backed-in styles are mutually exclusive: if multiple are applied to the same Label
, only the closest one to the Label
will take effect.
// Only `IconOnlyLabelStyle()` will be applied to the label:
Label("Title", systemImage: "moon.circle.fill")
.labelStyle(IconOnlyLabelStyle())
.labelStyle(TitleOnlyLabelStyle())
.labelStyle(DefaultLabelStyle())
As LabelStyle
is a protocol, we can define our own styles:
public protocol LabelStyle {
associatedtype Body: View
func makeBody(configuration: Self.Configuration) -> Self.Body
typealias Configuration = LabelStyleConfiguration
}
Similarly to ViewModifier
, LabelStyle
requires a makeBody(configuration:)
method, which gives us the opportunity to define our own label style.
makeBody(configuration:)
takes in a LabelStyleConfiguration
instance, which is the same parameter accepted by the last Label
initializer we’ve listed above.
This configuration carries the complete set of instructions that defines the Label
up to this point.
We can’t define a brand new configuration ourselves, this is reserved to SwiftUI, however we do have access to the current components of the Label
, the image (named icon
) and the title:
public struct LabelStyleConfiguration {
/// A type-erased title view of a label.
public var title: LabelStyleConfiguration.Title { get }
/// A type-erased icon view of a label.
public var icon: LabelStyleConfiguration.Icon { get }
}
Thanks to this configuration, our LabelStyle
is applied on top of the current style.
For example, here we build a LabelStyle
that adds a shadow to the entire Label
:
struct ShadowLabelStyle: LabelStyle {
func makeBody(configuration: Configuration) -> some View {
Label(configuration)
.shadow(color: Color.gray.opacity(0.9), radius: 4, x: 0, y: 5)
}
}
This is the only place where we can use this
Label
initializer
As ShadowLabelStyle
is a styling on top of the current LabelStyle
, it will be applied to whatever the Label
currently is.
Therefore, if we use it along with IconOnlyLabelStyle
for example, the final result will be a Label
with just the icon and our shadow:
Label("Title", systemImage: "moon.circle.fill")
.labelStyle(ShadowLabelStyle())
.labelStyle(IconOnlyLabelStyle())
Label style erasers
The .labelStyles
declaration order is important: previously we’ve seen how the three backed-in styles are mutually exclusive, what this really means is that in their own definition, they don’t use the configuration passed in makeBody(configuration:)
, but will create a new one instead.
In other words, IconOnlyLabelStyle
, TitleOnlyLabelStyle
, and DefaultLabelStyle
act as style erasers: once applied, any previous style is not carried over.
Going back to our ShadowLabelStyle
example:
Label("Title", systemImage: "moon.circle.fill")
.labelStyle(ShadowLabelStyle())
.labelStyle(IconOnlyLabelStyle())
will output the Label
icon with a shadow.
Label("Title", systemImage: "moon.circle.fill")
.labelStyle(IconOnlyLabelStyle()) // <- the label style order has been swapped
.labelStyle(ShadowLabelStyle())
will output the Label
icon with no shadow.
Since we’re using a style eraser, SwiftUI won’t even bother to apply our style first, this can be verified by adding a breaking point in ShadowLabelStyle
’s makeBody(configuration:)
implementation: SwiftUI won’t call our method at all.
This is in line with what we’ve seen with SwiftUI’s preference keys: SwiftUI always strive to do the least amount of work possible.
Can we define our own style eraser?
As mentioned above, only SwiftUI can create new configurations, however there’s a simple trick that will make any custom style also a style eraser: apply one of the native style erasers in our makeBody(configuration:)
implementation.
struct ShadowEraseLabelStyle: LabelStyle {
func makeBody(configuration: Configuration) -> some View {
Label(configuration)
.shadow(color: Color.gray.opacity(0.9), radius: 4, x: 0, y: 5)
.labelStyle(DefaultLabelStyle()) // <- ✨
}
}
In this example we force our Label
to display both the text and the icon, along with our shadow, any other style applied previously is ignored:
Label("Title", systemImage: "moon.circle.fill")
.labelStyle(ShadowEraseLabelStyle())
.labelStyle(TitleOnlyLabelStyle())
.labelStyle(IconOnlyLabelStyle())
Again, since our style now acts as a style eraser, it won’t be applied on top of to the current style, but will start with a clean Label
instead.
LabelStyleConfiguration’s icon and title style
We might have tried to erase the style also by passing the two configuration views to a new Label
:
struct ShadowLabelTryStyle: LabelStyle {
func makeBody(configuration: Configuration) -> some View {
Label(
title: { configuration.icon },
icon: { configuration.title }
)
.shadow(color: Color.gray.opacity(0.9), radius: 4, x: 0, y: 5)
}
}
with our view body:
Label("Title", systemImage: "moon.circle.fill")
.labelStyle(ShadowLabelTryStyle())
.labelStyle(IconOnlyLabelStyle())
Interestingly, this would not have worked:
it turns out that configuration.icon
and configuration.title
carry over the whole configuration style.
In the example above the title
view would have been hidden, despite us creating a new view Label
without passing directly the configuration itself.
To further prove this, let’s define a new style that all it does is swapping the Label
’s’ title
with its icon
:
struct SwapLabelStyle: LabelStyle {
func makeBody(configuration: Configuration) -> some View {
Label(
title: { configuration.icon },
icon: { configuration.title }
)
}
}
The new label has the original title as its icon, and the original icon as its title.
Now imagine this view body:
Label("Title", systemImage: "moon.circle.fill")
.labelStyle(SwapLabelStyle())
.labelStyle(IconOnlyLabelStyle())
What we expect the final outcome to be?
We first apply IconOnlyLabelStyle
, therefore the title "Title"
is hidden, while the image "moon.circle.fill"
is shown.
It doesn’t matter that we swap them in SwapLabelStyle
: the user will see the original icon, despite being actually the title in the SwapLabelStyle
Label
.
Truly custom styles
For completeness, I must point out that LabelStyle
’s makeBody(configuration:)
only requires some View
to be returned, it doesn’t require a Label
(or a Label
with a few modifiers).
This means that really we can do whatever we want with it: what about turning our label into a HStack
?
struct HStackLabelStyle: LabelStyle {
func makeBody(configuration: Configuration) -> some View {
HStack {
configuration.icon
Spacer()
configuration.title
}
}
}
And here we use it as any other Label
:
Label("Title", systemImage: "moon.circle.fill")
.labelStyle(HStackLabelStyle())
While this works, this is the perfect opportunity to point out that .labelStyle
modifiers work only if they’re applied to a Label
:
since HStackLabelStyle
doesn’t return a Label
, any further label style applied, including erasing ones, will be ignored.
Label("Title", systemImage: "moon.circle.fill")
.labelStyle(HStackLabelStyle())
.labelStyle(ShadowLabelStyle())
.labelStyle(IconOnlyLabelStyle())
Applying them before HStackLabelStyle
would work:
Label("Title", systemImage: "moon.circle.fill")
.labelStyle(ShadowLabelStyle())
.labelStyle(IconOnlyLabelStyle())
.labelStyle(HStackLabelStyle())
However, if we do this, we probably shouldn’t use Label
in the first place.
Accessible Labels
While LabelStyle
is mainly thought for adding new styles, we can also use it to make our Label
s more accessible.
For example, when the system content size is among the accessibility ones, we might want to strip any Label
effect and hide the icon, leaving the bare minimum necessary for the user to go on with their task.
This is a great example where LabelStyle
excels, along with our conditional modifier extension:
struct AccessibleLabelStyle: LabelStyle {
@Environment(\.sizeCategory) var sizeCategory: ContentSizeCategory
func makeBody(configuration: Configuration) -> some View {
Label(configuration)
.if(sizeCategory.isAccessibilityCategory) { $0.labelStyle(TitleOnlyLabelStyle()) }
}
}
Here’s an example with all possible sizes:
Label Extensions
Despite label styles being the main way to customize and standardize our labels, sometimes we can get away by creating a Label
extension instead.
For example, among SF Symbols 2 updates, this year we’ve gained color variants for some of them:
unfortunately, out of the box, Label
defaults to display the mono color variant with no way to change it.
This can be addressed via Label
extension:
extension Label where Title == Text, Icon == Image {
init(_ title: LocalizedStringKey, colorfulSystemImage systemImage: String) {
self.init {
Text(title)
} icon: {
Image(systemName: systemImage)
.renderingMode(.original)
}
}
}
Which we can use by replacing the systemImage
argument name with colorfulSystemImage
, for example:
Label("Title", colorfulSystemImage: "moon.circle.fill")
.labelStyle(ShadowLabelStyle())
Conclusions
If I told you at the beginning of this article that you were about to read ~1500 words on Label
, I bet you wouldn’t believe me: it’s just an icon and a text, right?
Label
is another example of SwiftUI looking incredibly simple on the surface, but actually hiding lots of complexity and flexibility behind the scenes.
This shouldn’t surprise us anymore: and yet here we are, wondering what else we can discover about SwiftUI, I hope you will join me in this journey.
Thanks for reading and please let me know if I’ve missed anything!
⭑⭑⭑⭑⭑