Using MeasurementFormatter in Swift

Share this article

Using MeasurementFormatter in Swift

Understanding MeasurementFormatter

The MeasurementFormatter class provides formatted and localized representations of units and measurements. When catering to a global audience, you should present this data in local units. Suppose your app shows the end user a distance between two points. Assume that the distance is in imperial units: feet, yards and miles. When catering to a global audience, you should present this data in local units. Consider the following:

The distance between New York and Paris is 3,626.81 miles

In French, you would want to not only translate the string’s text, but also the measurement units therein contained:

La distance entre New York et Paris est de 5 836,78 km

Instead of attempting to write your own utility classes to perform these conversions, you should leverage the power of Apple’s Foundation.

Up and Running

When dealing with distance measurements, MeasurementFormatter does great work with zero configuration. By the time the user installs your app, the user’s device has a default locale. Working with the distance measurement above, we can convert to the user’s localized standard:

// distance in user's default locale
// this would print with different formats in various locales:
// 3626.81 miles for en_US
// 5.836,77 km for en_IT
// 5 836,77 km for en_FR
// 5,836.77 km for en_JP
// To find out your locale: NSLocale.current
import Foundation
let distanceInMiles = Measurement(value: 3626.81, unit: UnitLength.miles)
MeasurementFormatter().string(from: distanceInMiles)

Given the simplicity, it becomes clear that using MeasurementFormatter is the way to go. As a developer, you should get into a habit of always using Foundation formatters to present data. This will lead to future-proof code and less refactors.

Understanding Locales

Locales in programming describe linguistic, cultural, and technological conventions and standards. Most units of measure fall into one of three systems: British imperial, US customary, or Metric (aka: SI system). Examples of information encapsulated by a locale may include:

  • Symbols used in numbers and currency, for example:
    • 1,200.99 and $1,200.99 in US
    • 1 200,99 and €1.234.567,89 in FR
  • Dates formatting, for example:
    • Order of date: MM/DD/YYYY vs DD/MM/YYYY
    • Presentation of date: November 23, 2016 or 11.23.2016
    • Naming of international holidays
  • Units of measure, for example:
    • Imperial vs Metric measurements
    • The spelling of such (meters, metres, etc)
    • Abbreviations like “cal” vs “C” for calories

While we may be used to our own set of measure and symbols, many regions around the world are accustomed to their own. Spelling, abbreviations and symbols vary by country and language. This makes for a very complex set of rules and conversions. Here is just a quick sample of some of the variations in units which Foundation typically handles:

  • Naming and spelling of units
    • e.g.: meters, metros, mètres, metri, etc
  • Size of units
    • e.g.: US gallon (.26 liters), Imperial Gallon (.22 liters)
  • Abbreviations of units
    • e.g.: cal, kcal, Cal or C
  • Natural sizes
    • e.g.: feet vs miles, meters vs kilometers

You can also set locale and store the user’s preference. Here is an example of how you might use this in the real world, building on the code from above:

import Foundation

// Create a local variable instance of MeasurementFormatter which we can reuse and configure
let formatter = MeasurementFormatter()
let distanceInMiles = Measurement(value: 3626.81, unit: UnitLength.miles)

// You should collect this for UIPickerView or similar input
// and replace "en_FR" with the result of the picker
// This will save the string to the "locale" key in device storage
UserDefaults.standard.set("en_FR", forKey: "locale")

// From this point on, we want to refer to the saved preference
// If the User default is nil, we failover to "en_FR"
let localeIdentifier = UserDefaults.standard.object(forKey: "locale") ?? "en_FR"

// Last we initiate our Locale class with this identifier
let locale = Locale(identifier: localeIdentifier as! String)

// Optionally set the `locale` property of our instance.
// If we do not set this, it will default to the user's device locale
formatter.locale = locale
formatter.string(from: distanceInMiles) // prints "5 836,77 km"

Your application should allow the user to globally set their localization preferences. This especially important when working with data. Developers frequently rely on the user’s device setting but this can cause problems. For example: imagine an US expat living in France. Their device may be set to a French locale as they may know the language. This can still be problematic, as they may not have a grasp on metric measurements over imperial. It is a welcome feature to have localization preferences on a per-application basis.

Unit Style

The unitStyle property of MeasurementFormatter is the most basic of them all. This property expects a case from the UnitStyle enum which provides the following:

  • .short
    • Formats the unit of measure as the shortest abbreviated version or symbol.
    • e.g.: 10 feet would be formatted as 10’
  • .medium
    • This is the default value if none is specified.
    • Formats the unit of measure as an abbreviation
    • e.g.: 10 feet would be formatted as 10 ft
  • .long
    • Formats the unit of measure in a fully written form.
    • e.g.: 10 feet would be formatted as 10 feet
import Foundation

// You can optionally set the `unitStyle` property
// Expects enum case from `MeasurementFormatter.UnitStyle`
// Defaults to `.medium`

// You can simply pass the case value: `.long`
// or you can write it out as shown below

let formatter = MeasurementFormatter()
let distanceInMiles = Measurement(value: 3626.81, unit: UnitLength.miles)

formatter.unitStyle = MeasurementFormatter.UnitStyle.long
formatter.string(from: distanceInMiles) // 3,626.81 miles

Unit Options

The unitOptions property is like the unitStyle property. It also expects an enum case and the values are:

  • .providedUnit – displays the unit of measure provided and does not perform conversions.
  • .naturalScale – does some magic for us. Apple mapped out the typical units of measure to more human readable values. For example, a person would not describe 10 feet as 0.00189394 miles. Thus, the naturalScale property is usually appropriate to convert this measurement.
  • .temperatureWithoutUnit – is pretty self-explanatory. At times you may want to use or display a temperature without a unit of measure. This is set apart from the others as conversions of temperature tend to remain the same or be custom.
let formatter = MeasurementFormatter()

// Working with meters and evaluating the results
let distanceInMeters = Measurement(value: 2, unit: UnitLength.meters)
formatter.string(from: distanceInMeters) // prints "0,002 kilometres"

// However, we can see the class is aware of the original unit of measure
formatter.string(from: UnitLength.meters) // prints "metres"

// Optionally set the `unitOptions` property
// This can be: .providedUnit, .naturalScale, or .temperatureWithoutUnit
// Options provided by MeasurementFormatter.UnitOptions
// This defaults to a privately defined format if unset
formatter.unitOptions = .naturalScale
formatter.string(from: distanceInMeters) // prints "2 metres"

Formatting Numbers

When working with measure, it is not uncommon to change the formatting of the numbers. The numberFormatter property exists for this exact reason. A similar class to MeasurementFormatter is the NumberFormatter class. They work side-by-side to give you more granular control of string output. We can instantiate and configure the NumberFormatter and then set our MeasuremFormatter.numberFormatter to our NumberFormatter instance:

import Foundation
let formatter = MeasurementFormatter()
formatter.unitOptions = .naturalScale
formatter.unitStyle = .long
formatter.locale = Locale(identifier: "en_FR")

let distanceInMeters = Measurement(value: 2, unit: UnitLength.meters) // 2.0 m

// Optionally set the `numberFormatter` property
// Expects an instance of `NumberFormatter`
// You could set the `NumberFormatter.numberStyle` property to:
//   .none
//   .decimal
//   .currency
//   .percent
//   .scientific
//   .spellOut
// Defaults to .decimal if unset

let numberFormatter = NumberFormatter()
numberFormatter.numberStyle = .spellOut
formatter.numberFormatter = numberFormatter

formatter.string(from: distanceInMeters) // two meters

This gives us a granular control for a wide variety of applications.

Nuances and Gotchya’s

Measurement conversion is simple arithmetic, but presentation is not. The core libraries will not always follow conventions that you may expect. The underlying documentation and Swift source is written by developers in many different countries. As such, many of the comments and outputs seem to have small inconsistencies.

Inconsistency in unit names

You may find “metres” written as “meters” despite the locale remaining the same. I believe this is simply oversight, and some community contributors have opened radars to fix these issues in future versions of Swift.

Natural Scale is not perfect

You may find the naturalScale property converting outside of ranges you would expect. In my own development, I have found inches converting to feet vice versa at times I did not expect. However, despite these nuances, I would trust Apple 99% of the time rather than dealing with edge cases. These libraries continue to evolve and improve with each release.

Wrapping Up

The MeasurementFormatter class is one of the more straightforward utility classes in the Foundation framework. Despite being a more basic class, it is a powerful one and very simple one to learn.

Further reading

Frequently Asked Questions (FAQs) about Using MeasurementFormatter

What is the primary function of MeasurementFormatter in iOS development?

MeasurementFormatter is a powerful tool in iOS development that allows developers to convert measurements into localized strings for display in the user interface. It supports a wide range of units and can convert between units in the same dimension. This makes it easier to present data in a format that is familiar and understandable to the user, regardless of their location or language settings.

How can I customize the output of MeasurementFormatter?

MeasurementFormatter provides several options for customizing the output. You can set the locale to determine the language and formatting conventions used. You can also set the unit style to either short, medium, or long, depending on how much detail you want to include. Additionally, you can specify the unit options to control the conversion of units.

Can MeasurementFormatter handle temperature conversions?

Yes, MeasurementFormatter can handle temperature conversions. However, it’s important to note that temperature conversions are not as straightforward as other types of conversions. This is because the zero points for different temperature scales are not the same. For example, 0 degrees Celsius is not the same as 0 degrees Fahrenheit. MeasurementFormatter takes this into account when performing temperature conversions.

How does MeasurementFormatter handle unit options?

MeasurementFormatter provides a property called unitOptions that allows you to control the conversion of units. For example, you can set the unitOptions to .providedUnit to force the formatter to use the unit that was provided when creating the Measurement object. Alternatively, you can set it to .naturalScale to allow the formatter to choose the most appropriate unit based on the value of the measurement.

What are some common use cases for MeasurementFormatter?

MeasurementFormatter is commonly used in apps that deal with measurements in some way. For example, a weather app might use it to display temperatures in the user’s preferred unit of measurement. A fitness app might use it to display distances covered during a workout. It’s also useful in any app that needs to display measurements in a localized format.

Can MeasurementFormatter handle conversions between different dimensions?

No, MeasurementFormatter cannot handle conversions between different dimensions. For example, it cannot convert a measurement in meters to a measurement in kilograms, as these are measurements of different dimensions (length and mass, respectively). It can only convert between units within the same dimension.

How does MeasurementFormatter handle localization?

MeasurementFormatter uses the locale property to handle localization. The locale determines the language and formatting conventions used by the formatter. By default, the formatter uses the current locale, but you can set it to a specific locale if needed.

Can I use MeasurementFormatter to format measurements in a specific unit?

Yes, you can use MeasurementFormatter to format measurements in a specific unit. You can do this by setting the unitOptions property to .providedUnit. This forces the formatter to use the unit that was provided when creating the Measurement object.

How does MeasurementFormatter handle unit styles?

MeasurementFormatter provides a property called unitStyle that allows you to control the style of the unit. You can set it to .short, .medium, or .long. The .short style uses the shortest possible abbreviation for the unit, the .medium style uses a longer abbreviation, and the .long style uses the full name of the unit.

Can I use MeasurementFormatter to format measurements in a non-localized format?

Yes, you can use MeasurementFormatter to format measurements in a non-localized format. To do this, you would need to set the locale property to a locale that uses the formatting conventions you want. For example, you could set it to an English locale to format measurements using English conventions, regardless of the user’s actual locale.

Clay UnicornClay Unicorn
View Author

Clay is a tech consultant by day and hacker by night. He's launched dozens of mobile/web/IoT applications for companies like Pepsi, Samsung, and Red Bull. He loves teaching more than anything, and is always happy to help!

Share this article
Read Next
Get the freshest news and resources for developers, designers and digital creators in your inbox each week
Loading form