This episode is freely available thanks to the support of our subscribers

Subscribers get exclusive access to new and all previous subscriber-only episodes, video downloads, and 30% discount for team members. Become a Subscriber

We build a simple currency converter to experiment with SwiftUI's state-driven view updates.

00:06 Today we'll start building our first little project that uses SwiftUI. Xcode 11 is currently still in its first beta version, so there are a few things that don't work yet, and we're also still figuring out how SwiftUI works, so sometimes this leads to confusion about whether we're doing something wrong or the framework is.

00:22 But we do understand the thing we'll build today, and we know that the SwiftUI components we'll use work correctly. What we're building is a small currency converter. We've used this example before because it lets us play and get comfortable with reactive views: views with multiple inputs and outputs that affect each other.

00:43 The currency converter has a text field for the input amount and a picker view to select a currency rate, and if either of their values change, we need to parse the input text as an amount and use the selected rate to update the output. But we won't have to write much code for performing these updates, because the state-driven views of SwiftUI will auto-update when the app's state changes.

01:15 We've created a new Xcode project for a macOS app, and we selected the SwiftUI option. When we run the app, a window opens up with a label saying "Hello World," so everything seems to work so far. We replace the default ContentView with our own Converter view containing a horizontal stack that will lay out various subviews:

import SwiftUI

struct Converter: View {
    var body: some View {
        HStack {
            // ...
        }
    }
}

02:06 The first subview in the horizontal stack is a TextField view, and this view takes a Binding in its initializer. In order to turn a string variable into a binding, we prefix it with a dollar sign when we pass it to the text field, and we turn the variable itself into a state by labeling it with @State:

struct Converter: View {
    @State var text: String = "100"

    var body: some View {
        HStack {
            TextField($text)
        }
    }
}

03:10 Once the view gets rendered, the system manages the state for us. And by prefixing the state variable with a dollar sign, we turn it into a Binding, which is a two-way communication channel: whenever the text property changes, the text field gets updated, and vice versa.

03:42 We add some more views in the HStack, and we set a width for the text field so that it doesn't stretch out to fill the window, and then we run the app:

struct Converter: View {
    @State var text: String = "100"

    var body: some View {
        HStack {
            TextField($text).frame(width: 100)
            Text("EUR")
            Text("=")
            Text("TODO")
            Text("USD")
        }
    }
}

Computing the Output

04:35 Now let's start adding in some functionality. We add a computed property for the output, in which we try to parse a Double from the input text. If parsing succeeds, we multiply the value with a hardcoded rate, and we return the result wrapped in a string. If the input can't be parsed, we return an error message:

struct Converter: View {
    @State var text: String = "100"
    var output: String {
        let parsed = Double(text)
        return parsed.map { String($0 * 1.13) } ?? "parse error"
    }
    var body: some View {
        HStack {
            TextField($text).frame(width: 100)
            Text("EUR")
            Text("=")
            Text(output)
            Text("USD")
        }
    }
}

05:31 Now, when we type in the text field, the text variable is updated. This change to the state causes a rerender of the view. And because we're using a computed property for the output, we immediately see the newly calculated output value.

06:16 To improve the formatting of the output number, we use a number formatter with its number style set to .currency. We set the formatter's currency symbol to an empty string, because we're already showing the currency in another label:

struct Converter: View {
    @State var text: String = "100"
    let formatter: NumberFormatter = {
        let f = NumberFormatter()
        f.numberStyle = .currency
        f.currencySymbol = ""
        return f
    }()
    var output: String {
        let parsed = Double(text)
        return parsed.map { formatter.string(from: NSNumber(value: $0 * 1.13)) } ?? "parse error"
    }
    var body: some View {
        HStack {
            TextField($text).frame(width: 100)
            Text("EUR")
            Text("=")
            Text(output)
            Text("USD")
        }
    }
}

Selecting a Currency

07:54 Next we can try including a list of currencies. We define a simple dictionary of currency names and rates. Later on, we can load these rates over the network, but we start out with some hardcoded values:

struct Converter: View {
    let rates: [String: Double] = ["USD": 1.13, "GBP": 0.89]
    // ...
}

09:12 We wrap the existing horizontal stack in a vertical stack, and below it, we add a List view that contains a row for each currency.

To construct a list of currencies, we take the keys from the rates dictionary and we explicitly sort them to make sure they don't jump around when the state updates. Then we have to specify how to identify the array's elements by calling identified(by:) with a key path pointing to a Hashable property. Since we're working with an array of unique strings, we can simply pass in the key path \.self.

Within each row, we set up a horizontal stack consisting of the currency symbol, a spacer, and a label showing the currency's rate. We look up the rate in the rates dictionary and we force-unwrap the result because we're using a valid key:

struct Converter: View {
    // ...
    var body: some View {
        VStack {
            HStack {
                // ...
            }
            List {
                ForEach(self.rates.keys.sorted().identified(by: \.self) { key in
                    HStack {
                        Text(key)
                        Spacer()
                        Text("\(self.rates[key]!)")
                    }
                }
            }
        }
    }
}

11:30 The list now shows up in the app, but it somehow covers up the other views. This seems to be a bug in SwiftUI that we β€”Β for the sake of brevity β€” work around by giving the list a fixed height:

struct Converter: View {
    // ...
    var body: some View {
        VStack {
            // ...
            List {
                // ...
            }.frame(height: 100)
        }
    }
}

11:50 Now we are displaying the currency rates in a list, but we're not yet doing anything with them. To prepare for showing the converted output amount for each rate, we pull the parsing of the input amount out to a computed property:

struct Converter: View {
    // ...
    @State var text: String = "100"
    // ...
    var parsedInput: Double? {
        Double(text)
    }
    var output: String {
        parsedInput.flatMap { formatter.string(from: NSNumber(value: $0 * 1.13)) } ?? "parse error"
    }
    // ...
}

On a side note: Swift 5.1 allows us to leave off the return keyword for single-line return statements inside computed properties β€” which we already knew well from single-statement closures.

12:59 Ideally, we want to write something like the following to add a converted amount to each of the currency list rows if the input parsing works:

HStack {
    Text(key)
    Spacer()
    Text("\(self.rates[key]!)")
    if let input = self.parsedInput {
        Spacer()
        Text("\(input * self.rates[key]!)")
    }
}

13:20 But if let isn't supported inside the view builder function. Instead, we first check if the variable is not nil and then force-unwrap it:

HStack {
    Text(key)
    Spacer()
    Text("\(self.rates[key]!)")
    if self.parsedInput != nil {
        Spacer()
        Text("\(self.parsedInput! * self.rates[key]!)")
    }
}

13:43 Now the app runs, and we can see the list of currencies updating with conversion results.

13:56 But now that we see it, we realize a list is not the best interface for this app. It would be nicer to select a currency using a picker view and just update the output in the original, horizontal stack.

14:16 We replace the List with a Picker. The initializer of Picker takes both a binding for the selection and a label, the latter of which we set to an empty text. We also add a state for the picker's selection:

struct Converter: View {
    // ...
    @State var selection: String = "USD"
    // ...
    var body: some View {
        VStack {
            // ...
            Picker(selection: $selection, label: Text("")) {
                ForEach(self.rates.keys.sorted().identified(by: \.self) { key in
                    // ...
                }
            }
        }
    }
}

15:07 Through binding, the picker automatically updates the selection property when we choose a value in the user interface. Now we can use this selected currency to provide the currently selected rate from a computed property. In it, we force-unwrap the looked-up rate because we know that the selection is set to a value from the picker, which only contains valid keys of the rates dictionary:

struct Converter: View {
    // ...
    @State var selection: String = "USD"
    var rate: Double {
        rates[selection]!
    }
    // ...
}

15:44 Now by reading the rate property in our calculation of the output string, we are converting into the currency that's selected in the picker:

struct Converter: View {
    // ...
    var output: String {
        parsedInput.flatMap { formatter.string(from: NSNumber(value: $0 * self.rate)) } ?? "parse error"
    }
    // ...
}

16:00 To reflect this, we show the selection in the currency symbol label:

HStack {
    TextField($text).frame(width: 100)
    Text("EUR")
    Text("=")
    Text(output)
    Text(selection)
}

16:10 And finally, we remove the extraneous views from the picker's rows so that they only contain a Text:

struct Converter: View {
    // ...
    var body: some View {
        VStack {
            // ...
            Picker(selection: $selection, label: Text("")) {
                ForEach(self.rates.keys.sorted().identified(by: \.self) { key in
                    Text(key)
                }
            }
        }
    }
}

Results and Next Steps

16:34 When we run the app, we see that everything works the way we expect. We can change the input amount and the currency selection, and the output shows the converted amount and the selected currency symbol. And if we enter an invalid input amount (e.g. one containing alphanumerics), the output label reads "parse error."

16:58 There are a few logical next steps to take. For one, we can load the conversion rates over the network. Secondly, it would be nice if we could flip the conversion so that we can enter an amount in the foreign currency and convert this back into euros. And finally, it would be good to show the invalid state in a more user-friendly way. Let's pick this up next week.

Resources

  • Sample Code

    Written in Swift 5

  • Episode Video

    Become a subscriber to download episode videos.

In Collection

See All Collections

Episode Details

Recent Episodes

See All

Unlock Full Access

Subscribe to Swift Talk

  • Watch All Episodes

    A new episode every week

  • icon-benefit-download Created with Sketch.

    Download Episodes

    Take Swift Talk with you when you're offline

  • Support Us

    With your help we can keep producing new episodes