During the WWDC 2016, Apple announced a lot of new improvements and new libraries to make our life easier. One of them is the new native APIs in the Foundation framework to work with measurements and units. In this post we are going to cover this lib and how to use it in Swift.
Measurement
What is a measurement? Wikipedia says:
Measurement is the assignment of a number to a characteristic of an object or event, which can be compared with other objects or events.
Here, in Swift, its simply a couple of value (a number) and a unit (an object or event):
public struct Measurement<UnitType: Unit>: Comparable, Equatable {
public init(value: Double, unit: UnitType)
}
public class Unit: NSObject, NSCopying {
public init(symbol: String)
}
A measurement object is the base class you’ll use to manipulate the amount of units. This a the object representation for “1 km”, “24 degrees”, “56 kg” for example. Here an example with the UnitLength
units:
// Defines the distances
let distanceTraveled = Measurement(value: 5, unit: UnitLength.feet)
let distanceToGo = Measurement(value: 6, unit: UnitLength.feet)
The measurement library provides the common operators to manipulate them in a natural way:
// Now just work with the distances
let totalDistance = distanceTraveled + distanceToGo
let tripleDistance = 3 * distanceToGo
let halfDistance = distanceToGo / 2
Dimension
A Unit
in Swift, is an abstract object. To create a concrete unit just will need to inherit from the Dimension
object. A dimension contains a “symbol” and it manages all the logic to perform the conversion between each unit of the same class (e.g. km⇆ft, m⇆mi). Here the definition:
public class Dimension: Unit, NSCoding {
public init(symbol: String, converter: UnitConverter)
public class var baseUnit: Dimension
}
When you create a Unit
you will define a symbol (e.g. “kilometers”, “pounds”, “radians”, etc.), a converter and the base unit. A converter allows the program to make the conversion between the base unit and the other ones. For example 1000 meters = 1 kilometers. For example for the UnitLength
unit the base unit is the meter, and so each conversion is relative to it:
let meter = UnitLength.baseUnit
Here is an example of Unit
implementation, the UnitLength
:
public class UnitLength: Dimension {
/*
Base unit - meters
*/
public class var kilometers: UnitLength { get }
public class var meters: UnitLength { get }
public class var feet: UnitLength { get }
public class var miles: UnitLength { get }
// ...
}
Each Unit
declares/packages its own graduation values and units.
Here is the list of concrete units provided natively by Apple (singletons for most common units International System of Units):
UnitAcceleration
: metersPerSecondSquared, gravity.UnitAngle
: degrees, radians, gradians, etc.UnitArea
: squareMeters, squareInches, ares, etc.UnitConcentrationMass
: gramsPerLiter, milligramsPerDeciliter, etc.UnitDispersion
: partsPerMillion.UnitDuration
: seconds, minutes, hours.UnitElectricCharge
: coulombs, ampereHours, etc.UnitElectricCurrent
: amperes, etc.UnitElectricPotentialDifference
:UnitElectricResistance
: volts, etc.UnitEnergy
: joules, calories, etc.UnitFrequency
: hertz, etc.UnitFuelEfficiency
: milesPerGallon, litersPer100Kilometers, etc.UnitLength
: meters, feet, lightyears, etc.UnitIlluminance
: lux.UnitMass
: kilograms, pounds, carats, etc.UnitPower
: watts, horsepower, etc.UnitPressure
: newtonsPerMeterSquared, kilopascals, bars, etc.UnitSpeed
: metersPerSecond, knots, etc.UnitTemperature
: kelvin, celcius, fahrenheit, etc.UnitVolume
: liters, cubicMeters, gallons, etc.
The library manages implicitly for us the conversion and the comparison between the measurement:
let distanceTraveled = Measurement(value: 3500, unit: UnitLength.feet)
let distanceToGo = Measurement(value: 5, unit: UnitLength.kilometers)
// Implicit Conversion
let totalDistance = distanceTraveled + distanceToGo
print(totalDistance) // 6066,8 meters
var distanceMarker : String
// Comparison Operators
if (distanceTraveled > distanceToGo) {
distanceMarker = "Almost there!"
}
else if (distanceTraveled < distanceToGo) {
distanceMarker = "Barely started!"
}
else {
distanceMarker = "Halfway!"
}
print(distanceMarker) // display "Barely started!"
For more informations you can take a loot at the official documentation.
Unit Definition
In this section we are going to see how to create our own Unit
. To illustrate it we are going to compare animals in terms of size. So let’s create a UnitAnimalSize
unit:
// UnitAnimalSize - Custom Dimension
public class UnitAnimalSize: Dimension {
static let rats = UnitAnimalSize(symbol: "🐀", converter: UnitConverterLinear(coefficient: 1))
static let rabbit = UnitAnimalSize(symbol: "🐇", converter: UnitConverterLinear(coefficient: 2))
static let goat = UnitAnimalSize(symbol: "🐐", converter: UnitConverterLinear(coefficient: 3))
static let elephant = UnitAnimalSize(symbol: "🐘", converter: UnitConverterLinear(coefficient: 4))
}
The conversion are handled implicitly thanks to the UnitConverterLinear
. For example here, 1 🐘 = 4 🐀 or 1 🐘 = 2 🐇.
let rabbit = Measurement(value: 1, unit: UnitAnimalSize.rabbit)
let elephant = Measurement(value: 1, unit: UnitAnimalSize.elephant)
if 2 * rabbit == elephant {
print("2 rabbits is equivalent to 1 elephant")
}
As you can see this is pretty simple.
Formatting Measurements
Now one of the most interesting feature with measurements and units is the formatting! Indeed in most cases you’ll need to display the informations to you users. But they all live in different places over the world and they all have different conventions. For example to display 5 kilometers, in Canada we will display “5 km”, in Egypt “٥ كم” and in United States “3.1 mi”.
Here a simple use of the MeasurementFormatter
:
let formatter = MeasurementFormatter()
let distance = Measurement(value: 5, unit: UnitLength.kilometers) as Measurement
let result = formatter.string(from: distance)
// result: 3.1 mi - in United States
// result: 5 km - in France
Of course you can also configure the .providedUnit
, the .naturalScale
and like of another formatters the .locale
to display custom strings. For more informations, take a look at the official documentation.
Note that it seems that with Xcode 8 beta-1 and iOS 10 beta-1 the conversion between unit using the current locale and region does not seem to work yet.
Conclusion
As you can see, Apple provides us a really great framework to work with measurements and units! You can use already provided units and/or create your owns. For already provided units, the localization and the formatter style is already managed for us so we don’t have to worry about.
Have fun!
Great
Great article Yannick. Took me a while to figure out how exactly I should override baseUnit() while subclassing Unit to create UnitCurrency.
I did it, but I have a few doubts; I want to be able to instruct each .locale which UnitCurrency it should use (.dollar, .euro, etc) and would also like to instruct the formatter that the symbol has to be printed before the actual value.
Could you help me out with that?
Thx
BTW, here’s the overridden Dimension Class
public class UnitCurrency: Dimension
{
static let peso = UnitCurrency(symbol: "$", converter: UnitConverterLinear(coefficient: 1))
static let dollar = UnitCurrency(symbol: "US$", converter: UnitConverterLinear(coefficient: 15))
static let euro = UnitCurrency(symbol: "€", converter: UnitConverterLinear(coefficient: 16.5))
override public static func baseUnit() -> UnitCurrency
{
return self.peso
}
}
Nices APIs!