Implementing the Temperature Conversion

With the basics of the interface wired up, let’s implement the conversion from Fahrenheit to Celsius. You are going to store the current Fahrenheit value and compute the Celsius value whenever the text field changes.

In ConversionViewController.swift, add a property for the Fahrenheit value. This will be an optional measurement for temperature, a Measurement<UnitTemperature>?. (Do not worry about the strange syntax of the measurement type; it is a generic type, and you will be learning more about them later.)

Listing 6.5  Adding a variable to store the temperature (ConversionViewController.swift)

@IBOutlet var celsiusLabel: UILabel!
@IBOutlet var textField: UITextField!

var fahrenheitValue: Measurement<UnitTemperature>?

This property is optional because the user might not have typed in a number. (Earlier, you used optional binding to manage the same issue for the Celsius label.)

Now, add a computed property for the Celsius value.

Listing 6.6  Adding a computed property for Celsius temperature (ConversionViewController.swift)

var fahrenheitValue: Measurement<UnitTemperature>?
var celsiusValue: Measurement<UnitTemperature>? {
    if let fahrenheitValue = fahrenheitValue {
        return fahrenheitValue.converted(to: .celsius)
    } else {
        return nil
    }
}

If there is a Fahrenheit value, it will be converted to the equivalent value in Celsius. Otherwise, nil is returned.

Any time the Fahrenheit value changes, the Celsius label needs to be updated. Take care of that next.

Add a method to ConversionViewController that updates the celsiusLabel.

Listing 6.7  Updating the Celsius label (ConversionViewController.swift)

func updateCelsiusLabel() {
    if let celsiusValue = celsiusValue {
        celsiusLabel.text = "(celsiusValue.value)"
    } else {
        celsiusLabel.text = "???"
    }
}

This method should be called whenever the Fahrenheit value changes. To do this, you will use a property observer, which is a chunk of code that is called whenever a property’s value changes.

A property observer is declared using curly braces immediately after the property declaration. Inside the braces, you declare your observer using either willSet or didSet, depending on whether you want it to run immediately before or immediately after the property value changes, respectively.

(Note that property observers are not triggered when the property value is changed from within an initializer.)

Add a property observer to fahrenheitValue that will be called after the property value changes.

Listing 6.8  Adding a property observer to fahrenheitValue (ConversionViewController.swift)

var fahrenheitValue: Measurement<UnitTemperature>? {
    didSet {
        updateCelsiusLabel()
    }
}

With that logic in place, you can now update the Fahrenheit value when the text field changes (which, in turn, will trigger an update of the Celsius label).

In fahrenheitFieldEditingChanged(_:), delete your earlier non-converting implementation and instead update the Fahrenheit value.

Listing 6.9  Updating fahrenheitFieldEditingChanged(_:) (ConversionViewController.swift)

@IBAction func fahrenheitFieldEditingChanged(_ textField: UITextField) {

    if let text = textField.text, !text.isEmpty {
        celsiusLabel.text = text
    } else {
        celsiusLabel.text = "???"
    }

    if let text = textField.text, let value = Double(text) {
        fahrenheitValue = Measurement(value: value, unit: .fahrenheit)
    } else {
        fahrenheitValue = nil
    }
}

If there is text in the text field and that text can be represented by a Double, then the Fahrenheit value is set to a Measurement initialized with that Double value. For example, if the text field contains 3.14, then fahrenheitValue is set to a value of 3.14. But if the text field contains something like three or 1.2.3, then the initial checks fail. In this case, the Fahrenheit value is set to nil.

Build and run the application. The conversion between Fahrenheit and Celsius works great – so long as you enter a valid number. (It also shows more digits than you probably want it to, which you will address in a moment.)

It would be nice if the celsiusLabel updated when the application first launches, instead of showing the value 100. Override viewDidLoad() to set the initial value, similar to what you did in Chapter 1.

Listing 6.10  Overriding viewDidLoad() (ConversionViewController.swift)

override func viewDidLoad() {
    super.viewDidLoad()

    print("ConversionViewController loaded its view.")

    updateCelsiusLabel()
}

Build and run again to see the effect of this change.

In the remainder of this chapter, you will update WorldTrotter to address two issues: You will format the Celsius value to show a precision up to one fractional digit, and you will not allow the user to type in more than one decimal separator. There are a couple of other issues with your app, but you will focus on these two for now. One of the other issues will be presented as a challenge at the end of this chapter.

Let’s start with updating the precision of the Celsius value.

Number formatters

Although the temperature conversion currently works, it is not very readable. This is because you are not truncating or rounding the fractional part of the Celsius value. For example, converting 78 degrees Fahrenheit might display the Celsius value as 25.555555555557987. To address this, you will use a number formatter to limit the number of fractional digits.

Number formatters are instances of NumberFormatter that customize the display of a number. There are formatters for dates, energy, mass, length, measurements, and so on.

Create a constant number formatter in ConversionViewController.swift.

Listing 6.11  Adding a number formatter as a property (ConversionViewController.swift)

let numberFormatter: NumberFormatter = {
    let nf = NumberFormatter()
    nf.numberStyle = .decimal
    nf.minimumFractionDigits = 0
    nf.maximumFractionDigits = 1
    return nf
}()

You are using a closure to instantiate the number formatter. A NumberFormatter is created with the .decimal style, configured to display no more than one fractional digit. You will learn more about this syntax for declaring properties in Chapter 13.

Now modify updateCelsiusLabel() to use the formatter.

Listing 6.12  Updating updateCelsiusLabel() (ConversionViewController.swift)

func updateCelsiusLabel() {
    if let celsiusValue = celsiusValue {
        celsiusLabel.text = "(celsiusValue.value)"
        celsiusLabel.text =
            numberFormatter.string(from: NSNumber(value: celsiusValue.value))
    } else {
        celsiusLabel.text = "???"
    }
}

Build and run the application. Play around with Fahrenheit values to see the formatter at work. You should never see more than one fractional digit in the Celsius label.

In the next section, you will update the application to accept a maximum of one decimal separator in the text field. To do this, you will use a common iOS design pattern called delegation.

..................Content has been hidden....................

You can't read the all page of ebook, please click here login for view all page.
Reset