NEW: Join my free 100 Days of SwiftUI challenge today! >>

Two-way bindings in SwiftUI

Paul Hudson    @twostraws   

Fully updated for Xcode 11 GM

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:

  • Without @State we wouldn’t be able to change properties in our structs, because structs are fixed values.
  • Without @EnvironmentObject we wouldn’t be able to receive shared data from elsewhere in our app.
  • Without ObservableObject we wouldn’t be notified when an external value changes.
  • Without $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:

  1. Both controls are bound to those @State properties we just made.
  2. The Toggle switch has some text inside that will automatically appear to the left as a description.
  3. The 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:

  • The toggle has a two-way binding to the addLoyaltyDetails property.
  • That means when the toggle is changed, the property updates.
  • That property is marked with @State.
  • When any @State or @EnvironmentObject changes its value, SwiftUI will re-invoke the body property.
  • That 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.

Further reading

LEARN SWIFTUI FOR FREE I have a massive, free SwiftUI video collection on YouTube teaching you how to build complete apps with SwiftUI – check it out!

Similar solutions…

MASTER SWIFT NOW
Buy Testing Swift Buy Practical iOS 12 Buy Pro Swift Buy Swift Design Patterns Buy Swift Coding Challenges Buy Server-Side Swift (Vapor Edition) Buy Server-Side Swift (Kitura Edition) Buy Hacking with macOS Buy Advanced iOS Volume One Buy Advanced iOS Volume Two Buy Hacking with watchOS Buy Hacking with tvOS Buy Hacking with Swift Buy Dive Into SpriteKit Buy Swift in Sixty Seconds Buy Objective-C for Swift Developers Buy Beyond Code

Was this page useful? Let us know!