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