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>?).

@IBOutlet var celsiusLabel: UILabel!
var fahrenheitValue: Measurement<UnitTemperature>?

The reason this property is optional is because the user might not have typed in a number, similar to the empty string issue you fixed earlier.

Now add a computed property for the Celsius value. This value will be computed based on the Fahrenheit value.

var fahrenheitValue: Measurement<UnitTemperature>?

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

First you check to see whether there is a Fahrenheit value. If there is, you convert this value to the equivalent value in Celsius. If there is no Fahrenheit value, then you cannot compute a Celsius value and so you return nil.

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.

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

You want this method to be called whenever the Fahrenheit value changes. To do this, you will use a property observer, which is a chunk of code that gets 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 to be notified immediately before or immediately after the property value changes, respectively.

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

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

(One small note: Property observers are not triggered when the property value is changed from within an initializer.)

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 nonconverting implementation and instead update the Fahrenheit value.

@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
    }
}

First you check whether the text field has some text. If so, you check to see whether that text can be represented by a Double. For example, 3.14 can be represented by a Double, but both three and 1.2.3 cannot. If both of those checks pass, then the Fahrenheit value is set to a Measurement initialized with that Double value. If either of those checks fails, then 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 was updated when the application first launched instead of still showing the value 100.

Override viewDidLoad() to set the initial value, similar to what you did in Chapter 1.

override func viewDidLoad() {
    super.viewDidLoad()

    updateCelsiusLabel()
}

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

You use a number formatter to customize the display of a number. There are other formatters for formatting dates, energy, mass, length, measurements, and more.

Create a constant number formatter in ConversionViewController.swift.

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

Here you are using a closure to instantiate the number formatter. You are creating a NumberFormatter with the .decimal style and configuring it to display no more than one fractional digit. You will learn more about this new syntax for declaring properties in Chapter 16.

Now modify updateCelsiusLabel() to use this formatter.

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 on 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