Adapting SwiftUI Label Style

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)

Red button with stop icon and title

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:

Red buttons with stop icon, red button with Stop title

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())

Sun icon and Hello title with blue dashed line border

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)
    }
  }
}

Red button with stop icon

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)
    }
  }
}

Red button with stop icon above Stop title

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:

Blue rounded buttons with white cross in circle. Top image at default text size has circle centered, bottom image at accessibility text size has button offset to the right.

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.