The second step in our project will be to let the user enter their address into a form, but as part of that we’re going to add some validation – we only want to proceed to the third step if their address looks good.
We can accomplish this by adding a Form
view to the AddressView
struct we made previously, which will contain four text fields: name, street address, city, and zip. We can then add a NavigationLink
to move to the next screen, which is where the user will see their final price and can check out.
To make this easier to follow, we’re going to start by adding a new view called CheckoutView
, which is where this address view will push to once the user is ready. This just avoids us having to put a placeholder in now then remember to come back later.
So, create a new SwiftUI view called CheckoutView
and give it the same Order
property and preview that AddressView
has:
struct CheckoutView: View {
var order: Order
var body: some View {
Text("Hello, World!")
}
}
#Preview {
CheckoutView(order: Order())
}
Again, we’ll come back to that later, but first let’s implement AddressView
. Like I said, this needs to have a form with four text fields bound to four properties from our Order
object, plus a NavigationLink
passing control off to our check out view.
First, we need four new properties in Order
to store delivery details:
var name = ""
var streetAddress = ""
var city = ""
var zip = ""
Now replace the existing body
of AddressView
with this:
Form {
Section {
TextField("Name", text: $order.name)
TextField("Street Address", text: $order.streetAddress)
TextField("City", text: $order.city)
TextField("Zip", text: $order.zip)
}
Section {
NavigationLink("Check out") {
CheckoutView(order: order)
}
}
}
.navigationTitle("Delivery details")
.navigationBarTitleDisplayMode(.inline)
As you can see, that passes our order
object on one level deeper, to CheckoutView
, which means we now have three views pointing to the same data.
That code will throw up lots of errors, but it takes just one small change to fix them – change the order
property to this:
@Bindable var order: Order
Previously you've seen how Xcode lets us bind to local @State
properties just fine, even when those properties are classes using the @Observable
macros. That works because the @State
property wrapper automatically creates two-way bindings for us, which we access through the $
syntax – $name
, $age
, etc.
We haven't use @State
in AddressView
because we aren't creating the class here, we're just receiving it from elsewhere. This means SwiftUI doesn't have access to the same two-way bindings we'd normally use, which is a problem.
Now, we know this class uses the @Observable
macro, which means SwiftUI is able to watch this data for changes. So, what the @Bindable
property wrapper does is create the missing bindings for us – it produces two-way bindings that are able to work with the @Observable
macro, without having to use @State
to create local data. It's perfect here, and you'll use it a lot in your future projects.
Go ahead and run the app again, because I want you to see why all this matters. Enter some data on the first screen, enter some data on the second screen, then try navigating back to the beginning then forward to the end – that is, go back to the first screen, then click the bottom button twice to get to the checkout view again.
What you should see is that all the data you entered stays saved no matter what screen you’re on. Yes, this is the natural side effect of using a class for our data, but it’s an instant feature in our app without having to do any work – if we had used local state, then any address details we had entered would disappear if we moved back to the original view.
Now that AddressView
works, it’s time to stop the user progressing to the checkout unless some condition is satisfied. What condition? Well, that’s down to us to decide. Although we could write length checks for each of our four text fields, this often trips people up – some names are only four or five letters, so if you try to add length validation you might accidentally exclude people.
So, instead we’re just going to check that the name
, streetAddress
, city
, and zip
properties of our order aren’t empty. I prefer adding this kind of complex check inside my data, which means you need to add a new computed property to Order
like this one:
var hasValidAddress: Bool {
if name.isEmpty || streetAddress.isEmpty || city.isEmpty || zip.isEmpty {
return false
}
return true
}
We can now use that condition in conjunction with SwiftUI’s disabled()
modifier – attach that to any view along with a condition to check, and the view will stop responding to user interaction if the condition is true.
In our case, the condition we want to check is the computed property we just wrote, hasValidAddress
. If that is false, then the form section containing our NavigationLink
ought to be disabled, because we need users to fill in their delivery details first.
So, add this modifier to the end of the second section in AddressView
:
.disabled(order.hasValidAddress == false)
The code should look like this:
Section {
NavigationLink("Check out") {
CheckoutView(order: order)
}
}
.disabled(order.hasValidAddress == false)
Now if you run the app you’ll see that all four address fields must contain at least one character in order to continue. Even better, SwiftUI automatically grays out the button when the condition isn’t true, giving the user really clear feedback when it is and isn’t interactive.
SAVE 50% To celebrate Black Friday, all our books and bundles are half price, so you can take your Swift knowledge further without spending big! Get the Swift Power Pack to build your iOS career faster, get the Swift Platform Pack to builds apps for macOS, watchOS, and beyond, or get the Swift Plus Pack to learn advanced design patterns, testing skills, and more.
Sponsor Hacking with Swift and reach the world's largest Swift community!
Link copied to your pasteboard.