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 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, which tries 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 for now, 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 if it's not, we
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.