In the previous episode, we used the MeasurementFormatter class to format the raw values the Clear Sky API returns. This is convenient and we use the MeasurementFormatter class several more times in the next few episodes. In this episode, we create an elegant API that wraps around the MeasurementFormatter API to avoid code duplication.
Defining the Clear Sky Formatter
Add a Swift file to the Utilities group and name it ClearSkyFormatter.swift. Declare a struct with name ClearSkyFormatter.
import Foundation
struct ClearSkyFormatter {
}
Declare a private, constant property with name measurementFormatter. We create a MeasurementFormatter instance and store a reference to the instance in the measurementFormatter property.
import Foundation
struct ClearSkyFormatter {
// MARK: - Properties
private let measurementFormatter = MeasurementFormatter()
}
We need the ability to explicitly set the locale of the measurement formatter and its number formatter. For that reason, we declare an initializer that defines a parameter with name locale of type Locale. We only need to set the locale of the the measurement formatter and its number formatter when running unit tests so we define a default value for the locale parameter, the current locale of the device.
import Foundation
struct ClearSkyFormatter {
// MARK: - Properties
private let measurementFormatter = MeasurementFormatter()
// MARK: - Initialization
init(locale: Locale = .current) {
}
}
In the body of the initializer, we create a NumberFormatter instance and set its locale to the value of the locale parameter.
import Foundation
struct ClearSkyFormatter {
// MARK: - Properties
private let measurementFormatter = MeasurementFormatter()
// MARK: - Initialization
init(locale: Locale = .current) {
let numberFormatter = NumberFormatter()
numberFormatter.locale = locale
}
}
Next we set the measurement formatter's locale to the value of the locale parameter and update the measurement formatter's numberFormatter property with the NumberFormatter instance we created earlier.
import Foundation
struct ClearSkyFormatter {
// MARK: - Properties
private let measurementFormatter = MeasurementFormatter()
// MARK: - Initialization
init(locale: Locale = .current) {
let numberFormatter = NumberFormatter()
numberFormatter.locale = locale
measurementFormatter.locale = locale
measurementFormatter.numberFormatter = numberFormatter
}
}
I want the API of the ClearSkyFormatter struct to be easy to use. We create two methods, a method to format a wind speed and a method to format a temperature. Declare a method with name formatWindSpeed(_:). The method defines a parameter with name windSpeed of type Float and it returns a string.
func formatWindSpeed(_ windSpeed: Float) -> String {
}
In the body of the method, we create a Measurement object. This should look familiar if you watched the previous episode. We pass the initializer the wind speed as a Double and the unit of the value, miles per hour.
func formatWindSpeed(_ windSpeed: Float) -> String {
let measurement = Measurement(
value: Double(windSpeed),
unit: UnitSpeed.milesPerHour
)
}
We transform the wind speed to a formatted string by passing the Measurement object to the string(from:) method of the MeasurementFormatter instance.
func formatWindSpeed(_ windSpeed: Float) -> String {
let measurement = Measurement(
value: Double(windSpeed),
unit: UnitSpeed.milesPerHour
)
return measurementFormatter.string(from: measurement)
}
The API to format a temperature is similar. Declare a method with name formatTemperature(_:). The method defines a parameter with name temperature of type Float and it returns a string.
func formatTemperature(_ temperature: Float) -> String {
}
In the body of the method, we create a Measurement object. We pass the initializer the temperature as a Double and the unit of the value, degrees Fahrenheit.
func formatTemperature(_ temperature: Float) -> String {
let measurement = Measurement(
value: Double(temperature),
unit: UnitTemperature.fahrenheit
)
}
We transform the temperature to a formatted string by passing the Measurement object to the string(from:) method of the MeasurementFormatter instance.
func formatTemperature(_ temperature: Float) -> String {
let measurement = Measurement(
value: Double(temperature),
unit: UnitTemperature.fahrenheit
)
return measurementFormatter.string(from: measurement)
}
Updating the Location Cell View Model
Open LocationCellViewModel.swift. The measurementFormatter property no longer holds a reference to a MeasurementFormatter instance. We create a ClearSkyFormatter object and assign the object to the measurementFormatter property.
private let measurementFormatter = ClearSkyFormatter()
We also need to update the computed windSpeed and temperature properties. In the body of the computed windSpeed property, we no longer create a Measurement object. We invoke the formatWindSpeed(_:) method of the ClearSkyFormatter object, passing in the wind speed.
var windSpeed: String? {
guard let windSpeed = weatherData?.currently.windSpeed else {
return nil
}
return measurementFormatter.formatWindSpeed(windSpeed)
}
We repeat these steps for the computed temperature property. We remove the Measurement object and invoke the formatTemperature(_:) method of the ClearSkyFormatter object, passing in the temperature.
var temperature: String? {
guard let temperature = weatherData?.currently.temperature else {
return nil
}
return measurementFormatter.formatTemperature(temperature)
}
Writing a Few Unit Tests
Let's write a few unit tests to verify that the ClearSkyFormatter struct behaves as expected. Select the Thunderstorm project in the Project Navigator and click the + button at the bottom of the Targets section.
Add a target for the test suite by choosing the Unit Testing Bundle template from the iOS > Test section.

Name the target ThunderstormTests and click the Finish button to add the target.

Add a group with name Cases to the ThunderstormTests group and remove the file with name ThunderstormTests.swift. Add a Swift file to the Cases group by choosing the Unit Test Case Class template from the iOS > Source section.

Name the XCTestCase subclass ClearSkyFormatterTests.

Xcode may ask you to create an Objective-C bridging header. Click Don't Create as we don't need an Objective-C bridging header.

Add an import statement for the Thunderstorm target and prefix the import statement with the testable attribute to gain access to the internal entities of the Thunderstorm target.
import XCTest
@testable import Thunderstorm
final class ClearSkyFormatterTests: XCTestCase {
...
}
Remove the contents of the ClearSkyFormatterTests class. We start with a clean slate. Declare a method with name testFormatWindSpeed(). A unit test is required to start with the word test. I usually append the name of the API under test and make the method throwing. That isn't strictly necessary in this example, though.
// MARK: - Tests for Format Wind Speed
func testFormatWindSpeed() throws {
}
In the body of the unit test, we create a Locale object by invoking an initializer that accepts a language identifier. I live in Belgium so I pass the language identifier of the region I live in. I do this deliberately because it allows me to verify that the ClearSkyFormatter struct correctly formats the raw value I pass it. This becomes clear in a moment.
// MARK: - Tests for Format Wind Speed
func testFormatWindSpeed() throws {
let locale = Locale(identifier: "nl-BE")
}
We create a ClearSkyFormatter object and pass the Locale object to the initializer. This example illustrates why we defined an initializer that accepts a Locale object.
// MARK: - Tests for Format Wind Speed
func testFormatWindSpeed() throws {
let locale = Locale(identifier: "nl-BE")
let formatter = ClearSkyFormatter(locale: locale)
}
In the last step, we assert that the ClearSkyFormatter object correctly formats the wind speed we pass to its formatWindSpeed(_:) method. The raw value we pass it is a wind speed in miles per hour. The formatted value is a wind speed in kilometers per hour and translated to Dutch. This setup should give us the confidence that the ClearSkyFormatter struct behaves as expected.
// MARK: - Tests for Format Wind Speed
func testFormatWindSpeed() throws {
let locale = Locale(identifier: "nl-BE")
let formatter = ClearSkyFormatter(locale: locale)
XCTAssertEqual(
formatter.formatWindSpeed(12.35),
"20 km/u"
)
}
We also write a unit test for the formatTemperature(_:) method. Declare a method with name testFormatTemperature() and copy the implementation of the testFormatWindSpeed() method. We only need to change the assertion. We assert that the ClearSkyFormatter object correctly formats the temperature we pass to its formatTemperature(_:) method. The raw value we pass it is a temperature in degrees Fahrentheit. The formatted value is a temperature in degrees Celcius.
// MARK: - Tests for Format Temperature
func testFormatTemperature() throws {
let locale = Locale(identifier: "nl-BE")
let formatter = ClearSkyFormatter(locale: locale)
XCTAssertEqual(
formatter.formatTemperature(32.0),
"0 °C"
)
}
Run the test suite to make sure the unit tests pass and the ClearSkyFormatter struct behaves as expected.
What's Next?
We created the ClearSkyFormatter struct to avoid code duplication, but it also provides us with a nice and convenient API. Spending a little bit of time creating the ClearSkyFormatter struct helps us refactoring the other view models in the next few episodes.