Fully updated for Xcode 11.2
If you look in the SwiftUI preview window you’ll see the standard iOS picker interface – a spinning wheel of options. By default it will show the first option, because it reads the value of paymentType
, which we set to 0. However, when the user moves the wheel their selection changes – they might select payment type 1 or 2 instead of 0.
So, this picker doesn’t just read the value of paymentType
, it also writes the value. This is what’s called a two-way binding, because any changes to the value of paymentType
will update the picker, and any changes to the picker will update paymentType
.
This is where the dollar sign comes in: Swift property wrappers use that to provide two-way bindings to their data, so when we say $paymentType
SwiftUI will write the value using the property wrapper, which will in turn stash it away and cause the UI to refresh automatically.
At first glance all these @ and $s seem a bit un-Swifty, and it’s true that we’re not used to working in this way. However, they allow us to get features that would otherwise require a lot of hassle:
@State
we wouldn’t be able to change properties in our structs, because structs are fixed values.@EnvironmentObject
we wouldn’t be able to receive shared data from elsewhere in our app.ObservableObject
we wouldn’t be notified when an external value changes.$property
two-way bindings we would need to update values by hand.Anyway, that’s our basic picker complete, so if we return to OrderView.swift we can update our code so that it shows our new CheckoutView
struct rather than some text saying “Checkout”.
Find this line of code:
NavigationLink(destination: Text("Check out")) {
And replace it with this:
NavigationLink(destination: CheckoutView()) {
Try running the app now, then go to the Order tab and press Place Order. The result is… well, less than perfect, let’s put it that way. And that’s despite putting in quite a lot of work just to get this far.
Well, we’re going to change just one word in CheckoutView.swift, and it should make all that work feel justified.
Inside CheckoutView
, I’d like you to change VStack
to Form
, then press Cmd+R to try the app again. Can you spot the difference?
Previously we had a spinning wheel picker, but now that we’re in a form we get a single table row that shows our picker’s title and currently selected value. Even better, when that row is tapped a new screen slides in showing other options, and you can select one and see that choice reflected back in the original screen.
This is the power of SwiftUI’s declarative approach to user interfaces: we say what behavior we want rather than the precise styling of it, and SwiftUI will automatically adapt it according to the context where it’s used.
Let’s continue on with our form by adding two more components: one that lets users select whether they have an iDine loyalty card, and another that lets them enter their card number. Both of these need two-way bindings just like our picker, so let’s start with two new @State
properties:
@State private var addLoyaltyDetails = false
@State private var loyaltyNumber = ""
Now we can add controls to our form to represent those – Toggle
is the equivalent of a UISwitch
, and TextField
is the equivalent of UITextField
. Add these two inside our existing form section:
Toggle(isOn: $addLoyaltyDetails) {
Text("Add iDine loyalty card")
}
TextField("Enter your iDine ID", text: $loyaltyNumber)
There’s not a lot of code there, but it’s worth mentioning some details:
@State
properties we just made.Toggle
switch has some text inside that will automatically appear to the left as a description.TextField
has some placeholder text so folks know what to type in there.Before you run the app, there’s another change I’d like to talk about first. That text field we just added – should it always be there, or only when the toggle switch is enabled?
We bound Toggle
to the value of addLoyaltyDetails
, which means when the user flicks it on or off that Boolean gets set to true or false. Wouldn’t it be great if the text field was visible only when the toggle was on?
Well, it turns out that’s pretty easy to do. Try wrapping the text field in a condition:
Toggle(isOn: $addLoyaltyDetails) {
Text("Add iDine loyalty card")
}
if addLoyaltyDetails {
TextField("Enter your iDine ID", text: $loyaltyNumber)
}
When you run the program now you’ll see that changing the state of the toggle shows or hides the text field. If you think it through this should all make sense:
addLoyaltyDetails
property.@State
.@State
or @EnvironmentObject
changes its value, SwiftUI will re-invoke the body
property.body
property directly reads the value of addLoyaltyDetails
to decide whether the text field is created or not.For an improved effect, modify the binding on the Toggle
so that it animates any changes it causes:
Toggle(isOn: $addLoyaltyDetails.animation()) {
Text("Add iDine loyalty card")
}
That will cause the loyalty card row to slide in and out smoothly.
Let’s try another common control: segmented controls. In SwiftUI this is actually just a Picker
with a modifier, so it works in exactly the same way – we give it a two-way binding that stores its selection index, then use a ForEach
to loop over an array to create some options to choose from.
For this screen, we can use a segmented control to represent various tip percentages that the user can select from. So, first add this property to store the options we want to show:
static let tipAmounts = [10, 15, 20, 25, 0]
Now add this property to store the selected segment:
@State private var tipAmount = 1
We can now put all that into a segmented control in our form. I’m going to put this into a new section in our form, because it lets us add a title that makes the UI clearer:
Section(header: Text("Add a tip?")) {
Picker("Percentage:", selection: $tipAmount) {
ForEach(0 ..< Self.tipAmounts.count) {
Text("\(Self.tipAmounts[$0])%")
}
}.pickerStyle(SegmentedPickerStyle())
}
We’re going to add one more component to our form, which is a button to actually confirm the order. We’ll come back to its exact functionality in just a moment, because there are other things we need to look at first.
Here’s the final section for the table:
Section(header:
Text("TOTAL: $100")
) {
Button("Confirm order") {
// place the order
}
}
Yes, I know the total order value is wrong, but just run the app for now.
We added a button inside ItemDetail
and it was blue text on a clear background, centered on the screen. Now we have a button in our form, and it’s different: it’s blue text, left aligned, and if you tap it the whole row flashes gray. This is another example of the way SwiftUI’s forms system changes the design and behavior of components inside it.
SPONSORED Instabug helps you identify and resolve severe crashes quickly. You can retrace in-app events and know exactly which line of code caused the crash along with environment details, network logs, repro steps, and the session profiler. Ask more questions or keep users up-to-date with in-app replies straight from your dashboard. Instabug takes data privacy seriously, so no one sees your data but you! See more detailed features comparison and try Instabug's crash reporting SDK for free.