SwiftUI Essentials

Working with Units and Measurements in SwiftUI

1 Working with Units and Measurements in SwiftUI 07:03

A few years ago, Apple added a number of APIs to the Foundation framework to make it easier to work with units and measurements. The APIs are flexible and straightforward to use. As of this year, SwiftUI integrates with these APIs to make it trivial to display units and measurements. The SwiftUI APIs we discuss in this episode are available as of iOS 15, tvOS 15, macOS 12, and watchOS 8.

Working with Units and Measurements in Swift

Most developers are not aware that Apple introduced the units and measurements APIs we discuss in this episode. They are part of the Foundation framework and available on every Apple platform. Let me show you how they work.

Fire up Xcode and create a playground. Remove the contents of the playground and add an import statement for the Foundation framework.

Working with Units and Measurements in SwiftUI

A measurement is encapsulated by the Measurement struct. The initializer accepts a value, a double, and the unit of measure. In this example, we create a Measurement object that defines a length in kilometers.

import Foundation

let length = Measurement(value: 120.0, unit: UnitLength.kilometers)

The unit parameter defines the unit of measure the Measurement object encapsulates. The UnitLength class inherits from Dimension. As the name suggests, Dimension represents a dimensional unit of measure.

The Foundation framework defines Dimension subclasses for most of the common units of measure, such as UnitMass and UnitTemperature, and for a few less common units of measure, such as UnitIlluminance and UnitFuelEfficiency. You can also subclass Dimension to add support for a unit of measure Foundation doesn't support.

Let's continue with the example. We can convert the value the Measurement object stores to another unit by invoking the converted(to:) method on the Measurement object. The converted(to:) method returns a Measurement object. It doesn't modify Measurement object it is invoked on. The convert(to:) method is a mutating method that modifies the Measurement object it is invoked on.

To convert the value of the Measurement object to miles, we invoke the converted(to:) method, passing in UnitLength.miles. We can access the value in miles through the measurement's value property.

import Foundation

let length = Measurement(value: 120.0, unit: UnitLength.kilometers)

length.converted(to: UnitLength.miles).value

It should be no surprise that the unit of measure you pass to convert(to:) or converted(to:) needs to match the unit of measure of the Measurement object. A runtime error is thrown if the units of measure don't match.

A runtime error is thrown if the units of measure don't match.

The units and measurements APIs can do more than converting measurements. Let's define a length in kilometers and a length in miles.

import Foundation

let kilometers = Measurement(value: 10.0, unit: UnitLength.kilometers)
let miles = Measurement(value: 10.0, unit: UnitLength.miles)

The Measurement struct supports the four basic operations of arithmetic, addition, subtraction, division, and multiplication. Adding the length in kilometers to the length in miles is similar to adding two integers. Because the UnitLength class defines its base unit as meters, performing calculations on measurements is trivial. The result of the addition is a little over 26,000 meters.

import Foundation

let kilometers = Measurement(value: 10.0, unit: UnitLength.kilometers)
let miles = Measurement(value: 10.0, unit: UnitLength.miles)

let result = kilometers + miles

The units and measurements API comes with a number of formatting options. The value of a Measurement object can be displayed into something you and I understand. Let's invoke the formatted(_:) method on result to see the result of the addition.

import Foundation

let kilometers = Measurement(value: 10.0, unit: UnitLength.kilometers)
let miles = Measurement(value: 10.0, unit: UnitLength.miles)

let result = kilometers + miles

result.formatted() // 16 mi

You can optionally pass a format style to the formatted(_:) method to define how the value of the Measurement object is formatted. The object we pass to the formatted(_:) method needs to conform to the FormatStyle protocol.

Foundation defines the Measurement.FormatStyle struct to make this straightforward. Because Measurement is generic over UnitType, we specify the unit of measure between angle brackets.

let formatStyle = Measurement<UnitLength>.FormatStyle

The initializer accepts four arguments, a width, a locale, a usage, and a number format style. The width defines the display of the measurement, wide, narrow, or abbreviated. The locale and numberFormatStyle parameters define how the value of the measurement is formatted. The usage parameter defines in which context the measurement is displayed. If we pass nil for the locale parameter, the user's current locale is used.

let formatStyle = Measurement<UnitLength>.FormatStyle(
    width: .wide,
    usage: .general,
    numberFormatStyle: .number
)

result.formatted(formatStyle)

Formatting Measurements in SwiftUI

As of this year, SwiftUI integrates with the units and measurements APIs. These improvements are available as of iOS 15, tvOS 15, macOS 12, and watchOS 8. Let's take a look at an example. I created a project that uses SwiftUI for the application's user interface. The ContentView struct defines a windSpeed property and a formatStyle property. The windSpeed property stores a Measurement object that encapsulates a value of 20 kilometers per hour. The formatStyle property defines how the measurement should be displayed to the user.

import SwiftUI

struct ContentView: View {

    // MARK: - Properties

    private let windSpeed = Measurement(
        value: 20.0,
        unit: UnitSpeed.kilometersPerHour
    )

    private let formatStyle = Measurement<UnitSpeed>.FormatStyle(
        width: .abbreviated,
        numberFormatStyle: .number
    )

    // MARK: - View

    var body: some View {
        Text("Hello, world!")
            .padding()
    }
}

struct ContentView_Previews: PreviewProvider {
    static var previews: some View {
	    ContentView()
    }
}

To display the measurement, we pass it to the initializer of the Text struct. To format the measurement, we pass the value of the formatStyle property as the second argument. The Text object uses the format style to decide how to display the measurement to the user.

// MARK: - View

var body: some View {
    Text(windSpeed, format: formatStyle)
        .padding()
}

Formatting Measurements in SwiftUI

Using measurements has more benefits, though. Remember that the user's current locale defines how the measurement is formatted if we don't pass a locale to the initializer of the Measurement.FormatStyle struct. Let's add a few more previews and change the locale. We create a group in the body of the static computed previews property and create three previews. We apply the environment modifier to set the locale of each preview.

struct ContentView_Previews: PreviewProvider {
    static var previews: some View {
        Group {
            ContentView()
                .environment(\.locale, Locale(identifier: "fr"))
            ContentView()
                .environment(\.locale, Locale(identifier: "ar_MA"))
            ContentView()
                .environment(\.locale, Locale(identifier: "zh_CN"))
        }
    }
}

Notice that the measurement is formatted differently for each preview, taking the user's current locale into account. Not only does this save you time, it ensures the user experience is tailored to the user. Details like this set good applications apart from great applications.

Formatting Measurements in SwiftUI

What's Next?

The Foundation framework supports a range of units of measure and you can define custom ones if needed. Working with measurements is intuitive and straightforward. The added support for SwiftUI is a welcome addition, making it easier than ever to provide a tailored user experience.