Mobile
Article
By Clay Unicorn

Using MeasurementFormatter in Swift

By Clay Unicorn

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"
--ADVERTISEMENT--

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

Recommended
Sponsors
The most important and interesting stories in tech. Straight to your inbox, daily. Get Versioning.
Login or Create Account to Comment
Login Create Account