Creating your own custom styles for labels allows you to adapt the layout for the available horizontal size.
Last updated: Jan 28, 2023
Label Style
Here’s my button that has a label with an icon and title:
Button(role: .destructive) {
stop()
} label: {
Label("Stop", systemImage: "xmark.circle")
.font(.title3)
.padding(.horizontal)
}
.buttonStyle(.borderedProminent)
The label style controls the appearance of the label. There are four built-in styles that control whether the label shows the icon and/or the title:
.labelStyle(.automatic)
.labelStyle(.titleAndIcon)
.labelStyle(.iconOnly)
.labelStyle(.titleOnly)
The default is .automatic
which allows the label to decide what to show depending on the context. The other styles are self-explanatory. Here’s the .iconOnly
and .titleOnly
styles:
Note: VoiceOver will still read the title even if the label is only showing the icon.
Switching Styles
When I have limited horizontal space I want to switch from showing the icon and title to showing just the icon. My first thought was to get the horizontal size class from the environment:
@Environment(\.horizontalSizeClass) var horizontalSizeClass
Then apply a condition inside the label style view modifier that switches the style when the horizontal size class is compact:
Label("Stop", systemImage: "xmark.circle")
.labelStyle(horizontalSizeClass == .compact ? .iconOnly : .titleAndIcon)
That doesn’t work, the compiler complains that the ternary expression is not returning the same types. The built-in label styles are distinct types that implement the LabelStyle
protocol:
public struct IconOnlyLabelStyle : LabelStyle { ... }
public struct TitleAndIconLabelStyle : LabelStyle { ... }
A better way is to create our own custom label style.
Custom Label Style
The LabelStyle
protocol requires a makeBody
method that accepts a LabelStyleConfiguration
:
protocol LabelStyle {
associatedtype Body: View
@ViewBuilder func makeBody(configuration: Self.Configuration) -> Self.Body
typealias Configuration = LabelStyleConfiguration
}
The configuration provides access to the label icon and title. The Label
view has an initializer that takes a LabelStyleConfiguration
that we can use in makeBody
to customize the label style without changing the layout:
struct BlueDashBorderLabelStyle: LabelStyle {
func makeBody(configuration: Configuration) -> some View {
Label(configuration)
.padding()
.overlay(
RoundedRectangle(cornerRadius: 8,
style: .continuous)
.stroke(.blue,
style: StrokeStyle(lineWidth: 4, dash: [10, 10]))
)
}
}
Applying our custom style to a label:
Label("Hello", systemImage: "sun.max")
.labelStyle(BlueDashBorderLabelStyle())
An Adaptive Label Style
In my example, I want to change the label layout depending on the horizontal size class. We can access the label icon and title via the configuration. That allows us to use our own custom layout depending on the horizontal size class:
struct AdaptiveLabelStyle: LabelStyle {
@Environment(\.horizontalSizeClass) var horizontalSizeClass
func makeBody(configuration: Configuration) -> some View {
if horizontalSizeClass == .compact {
Label(configuration)
.labelStyle(.iconOnly)
} else {
Label(configuration)
}
}
}
When the size class is compact we use the icon only label style otherwise we return the default label layout. Another choice might be to return both the icon and title but arranged in a vertical stack:
struct AdaptiveLabelStyle: LabelStyle {
@Environment(\.horizontalSizeClass) var horizontalSizeClass
func makeBody(configuration: Configuration) -> some View {
if horizontalSizeClass == .compact {
VStack {
configuration.icon
configuration.title
}
} else {
Label(configuration)
}
}
}
Label Alignment at Accessibility Sizes (Fixed in iOS 16)
One curiosity is that a bordered button does not correctly center a label when using an accessibility text size (FB9951097). For example:
Button { } label: {
Label("Stop", systemImage: "xmark.circle")
.labelStyle(.iconOnly)
}
.buttonStyle(.borderedProminent)
Here’s a preview at the default and accessibilityExtraExtraExtraLarge text sizes:
Notice how in the lower preview the icon if offset from the center. This only seems to be a problem with bordered buttons at accessibility text sizes. Apple fixed this for iOS 16.