SwiftUI Gauges

The speedometer style gauge view has long been a feature of watchOS complications. In iOS 16, macOS 13 and watchOS 9, Apple introduced a gauge view that work across all three platforms.

Note: Apple deprecated the ClockKit gauges in watchOS 9.

Gauge View

You use a Gauge view to show a value within a closed range. By default, the range is 0…1. The value must conform to BinaryFloatingPoint so a Double or Float work fine:

struct Speedometer: View {
  @State private var speed = 0.75
 
  var body: some View {
    Gauge(value: speed) {
      Text("Speed")
    }
  }
}

The label is a view that describes the gauge’s purpose. The default gauge style depends on the context but, at least on iOS, you get a linear capacity gauge:

Speed gauge with blue horizontal bar filling 75% of track

A value of 0.75 fills 75% of the track using the default range.

Using A Range

If you don’t want to map your value into the default 0…1 range you can set your own bounds:

struct Speedometer: View {
  @State private var currentSpeed = 75.0
  private let minSpeed = 0.0
  private let maxSpeed = 150.0

  var body: some View {
    Gauge(value: currentSpeed, in: minSpeed...maxSpeed) {
      Text("Speed")
    }
  }
}

A value of 75.0 fills 50% of the track using a range of 0…150:

Speed gauge with blue horizontal bar filling 50% of track

Adding Custom Labels

You can supply views for the current value, and optionally a minimum value and maximum value:

Gauge(value: currentSpeed, in: minSpeed...maxSpeed) {
  Text("Speed")
} currentValueLabel: {
  Text(currentSpeed.formatted())
} minimumValueLabel: {
  Text(minSpeed.formatted())
} maximumValueLabel: {
  Text(maxSpeed.formatted())
}

Speed gauge with blue horizontal bar filling 50% of track with labels 0 on left, 150 on right and 75 in middle below track

All four labels are views so we’re not restricted to plain text:

Gauge(value: currentSpeed, in: minSpeed...maxSpeed) {
  Label("Speed", systemImage: "car.fill")
} currentValueLabel: {
  Text(currentSpeed.formatted())
} minimumValueLabel: {
  Image(systemName: "tortoise")
} maximumValueLabel: {
  Image(systemName: "hare")
}

Speed gauge with blue horizontal bar filling 50% of track with a tortoise icon on left, hare icon on right and 75 in middle below track

Note that the bounds views (minimumValueLabel and maximumValueLabel) must be the same type.

We can apply the usual view modifiers to any of the views:

Gauge(value: currentSpeed, in: minSpeed...maxSpeed) {
  Label("Speed", systemImage: "gauge")
} currentValueLabel: {
  Text(currentSpeed.formatted())
} minimumValueLabel: {
  Image(systemName: "tortoise")
    .foregroundColor(.green)
} maximumValueLabel: {
  Image(systemName: "hare")
    .foregroundColor(.red)
}
.tint(currentSpeed > speedLimit ? .red : .green)
.labelStyle(.iconOnly)

Speed gauge with red horizontal bar filling 50% of track with a green tortoise icon on left, red hare icon on right and 75 in middle below track

Gauge Styles

Use the .gaugeStyle modifier to change the gauge style. The default on iOS is the .linearCapacity style that you can see above. There are also some accessory styles available for iOS, macOS and watchOS. These are suitable for use in the new in iOS 16 lock screen widgets.

Notes:

  • There are two extra styles, circular and linear, that are only supported on watchOS 7+. They are not available for iOS or macOS.
  • Not all styles show all four labels, but they are still used for accessibility.

accessoryLinear

A more compact version, without the description label, of the linear style with a marker that indicates the current value.

Gauge(value: currentSpeed, in: minSpeed...maxSpeed) {
  Text("MPH")
} currentValueLabel: {
    Text(currentSpeed.formatted())
} minimumValueLabel: {
    Text(minSpeed.formatted())
} maximumValueLabel: {
    Text(maxSpeed.formatted())
}
.gaugeStyle(.accessoryLinear)

Speed gauge with a black horizontal bar and a dot at the mid-point, 0 label on left, 150 on right

If you include the bounds labels the gauge shows them on the leading and trailing edges, otherwise it shows the current value label on the leading edge:

Speed gauge with a black horizontal bar and a dot at the mid-point, 75 label on left

accessoryLinearCapacity

A compact version on the linear capacity gauge with bounds labels on the leading and trailing edges. The gauge includes the label above the track and the current value label below, positioning both towards the leading edge:

Gauge(value: currentSpeed, in: minSpeed...maxSpeed) {
  Text("MPH")
} currentValueLabel: {
    Text(currentSpeed.formatted())
} minimumValueLabel: {
    Text(minSpeed.formatted())
} maximumValueLabel: {
    Text(maxSpeed.formatted())
}
.gaugeStyle(.accessoryLinearCapacity)

Speed gauge with thin blue horizontal bar filling 50% of a grey track. Labelled 0 on left and 150 on right. MPH above the track and 75 below

accessoryCircular

A compact circular gauge with an open ring and a marker. The gauge shows the current value label in the center and the gauge label in the opening at the bottom of the ring:

Gauge(value: currentSpeed, in: minSpeed...maxSpeed) {
  Text("MPH")
} currentValueLabel: {
    Text(currentSpeed.formatted())
}
.gaugeStyle(.accessoryCircular)

Black circular open ring gauge with dot at the top middle of the ring. 75 in the middle and MPH in the opening at the bottom

If you include the bounds labels the gauge shows them instead of the description label:

Black circular open ring gauge with dot at the top middle of the ring. 75 in the middle and 0 and 150 in the opening at the bottom

accessoryCircularCapacity

A closed ring version of the circular gauge with the ring filled to indicate the current value:

Gauge(value: currentSpeed, in: minSpeed...maxSpeed) {
  Text("MPH")
} currentValueLabel: {
  Text(currentSpeed.formatted())
}
.gaugeStyle(.accessoryCircularCapacity)
.tint(.purple)

Purple circular closed ring gauge with 75 in the middle. The right half of the ring is filled

This style doesn’t show the bounds or gauge label but as mentioned before you should consider including them for accessibility.