Forms and Pickers
So far, you’ve learned how to create apps with basic interactivity through navigation links and buttons. That’s great, but what if our needs are more complex? Just about every app collects settings, login credentials, or other information that cannot be handled using buttons and links alone. In this article, we’ll answer that question by learning about forms in SwiftUI.
Creating a form
The Form
element wraps controls used for data entry. SwiftUI includes many of those controls, including toggles, text fields, pickers, and more. We’ll delve into how they work in a little bit, but here’s a preview of what some of them look like:
That might work, but it looks pretty messy, right? That’s where forms come in. A form can wrap whatever elements we want — including views we’re already familiar with, like text and buttons. Here are those elements again but wrapped with a form:
As you can see, the Form
element adds styling and structure to our form and its controls, making it appear organized and presentable. Let’s see how a form is defined:
Form {// Content goes here!}
Next, we’ll talk about adding content to our form.
Building a contact form
Let’s build a contact form for an app in SwiftUI. First, create a new project in Xcode. Choose iOS as the platform and App as the template, then click Next.
Next, use the following options to create a simple new SwiftUI project:
- For Interface, choose SwiftUI.
- For Life Cycle, choose SwiftUI App.
- For Language, choose Swift.
- Ensure that Use Core Data and Include Tests are both unchecked.
Click Next, choose where you’d like to save your project, and click Create. Xcode will generate your new project and show you the ContentView
, which is where we’ll build out our form. At first, it’ll look like this:
struct ContentView: View {var body: some View {Text("Hello, world!").padding()}}
Before we add anything else, we’ll want to create our form. To do that, wrap the existing Text
element in a Form
view:
struct ContentView: View {var body: some View {Form {Text("Hello, world!").padding()}}}
If you run the app in the simulator, you’ll see that our text now looks like it’s part of a form. However, it’s still not interactive and doesn’t collect any information. We’ll start to change that in the next step.
Adding controls to our form
When someone contacts us, we’ll want them to enter some information, such as their email address and a message. Since the form is just a container for other views, we’ll need to add other SwiftUI views within the form to serve as controls for data entry.
TextField
A text field is a view that lets us enter and edit short bits of text, such as names and email addresses. Take a look at the following code snippet:
TextField("Name", text: $name)
This defines a text field with a title of “Name”. The text
parameter is a String
binding that contains whatever text is being edited and displayed in the text field. In this case, that binding is to a variable with the State
property wrapper called name
.
Let’s use a text field to collect email addresses in our contact form. In the updated code below, we created a variable called emailAddress
that contains an empty String
, and applied the State
property wrapper to the variable. We then replaced Text("Hello, world!")
with a text field with a title of “Email Address” and a binding to emailAddress
.
struct ContentView: View {@State private var emailAddress = ""var body: some View {Form {TextField("Email Address", text: $emailAddress)}}}
If we build and run our app now, we’ll see a text field at the top of the screen. The title, “Email Address”, serves as our placeholder text. When we tap on the text field, we’ll be able to enter our email address.
TextEditor
While text fields are great for short bits of text, we’ll want to use something different for long-form text: the TextEditor
view. Text editors let us display and edit as many lines of text as we need — perfect for composing messages, descriptions, and anything else that won’t fit on one line.
Below is an example of how to define a text editor. Unlike most other form controls, text editors don’t have a title or label attached. Instead, the only parameter is text
, which is a String
binding that contains whatever text is being displayed and edited. In this case, the binding is to a variable called fullText
.
TextEditor(text: $fullText)
We’ll use a text editor to write messages within our contact form. In ContentView
, add another State
variable called message
and give it an initial value of “Write your message here…”. Then, beneath the text field in our form, add a text editor that we can use to edit message
. Our ContentView
should now look like this:
struct ContentView: View {@State private var emailAddress = ""@State private var message = "Write your message here..."var body: some View {Form {TextField("Email Address", text: $emailAddress)TextEditor(text: $message)}}}
If we rebuild our app and run it in the simulator one more time, we’ll see that we can choose a contact method with only one tap. Then, the view will be updated to show only the TextField
for our selected contact method.
Toggle
Another form control we’ll want to use is the Toggle
, a view that provides a simple on/off switch. This is useful for representing boolean values in our forms, such as whether a setting is enabled or if the user agrees to our terms.
Each toggle has a binding and a label. The binding, labeled isOn
, is a Bool
that tells whether the toggle is on or off. The label is a view that describes what the toggle is for. Here’s an example of what that might look like:
Toggle(isOn: $hasNotifications) {Text("Notifications")}
In this toggle, the binding is to a variable called hasNotifications
, and the label is Text("Notifications")
. Since it’s common for toggles to only contain a text label, we can use this shortened syntax as well:
Toggle("Notifications", isOn: $hasNotifications)
We’ll also include an option to include logs with our message. Many apps with contact forms include this option because logs — which include information about your device and how you use the app — can be useful in finding bugs and resolving other issues. To add this option, we’ll first add another variable called includeLogs
with the State
property wrapper and an initial value of false
. Then, beneath the text editor in the view body, we’ll add a Toggle
element labeled “Include Logs”. Here’s what our ContentView
code looks like so far:
struct ContentView: View {@State private var emailAddress = ""@State private var message = "Write your message here..."@State private var includeLogs = falsevar body: some View {Form {TextField("Email Address", text: $emailAddress)TextEditor(text: $message)Toggle("Include Logs", isOn: $includeLogs)}}}
When we build the app and run it in the simulator, we should see our toggle right where we expect it to be:
Button
To round this out, we’ll add a submit button to our form. Below, you’ll see that we added a button to ContentView
titled “Submit”.
struct ContentView: View {@State private var emailAddress = ""@State private var message = "Write your message here..."@State private var includeLogs = falsevar body: some View {Form {TextField("Email Address", text: $emailAddress)TextEditor(text: $message)Toggle("Include Logs", isOn: $includeLogs)Button("Submit", action: {print("Submit button tapped")})}}}
If we build the app now, we should see the button at the bottom of the form.
When we tap the button, we should see “Submit button tapped” appear in the Xcode console.
Organizing our form with sections
So far, our form’s controls are a little bunched up, one after another. For simple forms, this might not be a problem. But for longer and more complex forms, it would help if we could give it a little more structure. Just as we’ve learned to do with lists, we can organize our forms using sections. Let’s do that with our form.
First, we’ll wrap the text field titled “Email Address” in a section with Text("How can we reach you?")
as the header. Next, we’ll place the text editor in a separate section, this time with Text("Briefly explain what's going on.")
as the header. Finally, we’ll wrap the “Include Logs” toggle in a section and add Text("This information will be sent anonymously.")
to the footer of this section. Our updated ContentView
is below.
struct ContentView: View {@State private var emailAddress = ""@State private var message = "Write your message here..."@State private var includeLogs = falsevar body: some View {Form {Section(header: Text("How can we reach you?")) {TextField("Email Address", text: $emailAddress)}Section(header: Text("Briefly explain what's going on.")) {TextEditor(text: $message)}Section(footer: Text("This information will be sent anonymously.")) {Toggle("Include Logs", isOn: $includeLogs)}Button("Submit", action: {print("Submit button tapped")})}}}
If we build the app now and take a look, we’ll see it now looks much better organized.
Pickers
A picker is a view that lets us choose from a list of options. We’ll use one to add a subject picker to our form. Here’s what the declaration could look like:
Picker("Subject", selection: $subject) {Text("Help").tag("Help")Text("Suggestion").tag("Suggestion")Text("Bug Report").tag("Bug Report")}
Here, we can see that a picker declaration has three components:
"Subject"
is the title, which describes what the picker is for.$subject
is the binding, which contains the currently selected value of the picker.- Within the brackets is the content, which consists of a
Text
view for each of the available options. Notice that each option has a tag attached. That’s how SwiftUI knows what value the picker represents.
This works, but it’s a little repetitive. We can clean it up by using a ForEach
loop:
Picker("Subject", selection: $subject) {ForEach(["Help", "Suggestion", "Bug Report"], id: \.self) { subject inText(subject)}}
Here, we pass a list of possible options to the ForEach
loop, with each option being listed only once. Note that this time, the Text
does not have a tag attached. This is because ForEach
uses the id
parameter to assign the tag automatically.
Let’s add this to our form. First, add a State
variable to ContentView
called subject
with an initial value of "Help"
. Create a new section between the "Email Address"
text field and the message text editor, using Text("What can we help you with?")
as the header for the section. Then, add in the picker from above. Here’s the code for ContentView
so far:
struct ContentView: View {@State private var emailAddress = ""@State private var message = "Write your message here..."@State private var subject = "Help"@State private var includeLogs = falsevar body: some View {Form {Section(header: Text("How can we reach you?")) {TextField("Email Address", text: $emailAddress)}Section(header: Text("What can we help you with?")) {Picker("Subject", selection: $subject) {ForEach(["Help", "Suggestion", "Bug Report"], id: \.self) { subject inText(subject)}}}Section(header: Text("Briefly explain what‘s going on.")) {TextEditor(text: $message)}Section(footer: Text("This information will be sent anonymously.")) {Toggle("Include Logs", isOn: $includeLogs)}Button("Submit", action: {print("Submit button tapped")})}}}
Here’s what it looks like when we rebuild and run the app. We can see our “Subject” picker now, but it’s grayed out, and nothing happens when we try to use it.
By default, pickers use a NavigationLink
to present a list of options on a separate page. Since we aren’t using the picker from within a NavigationView
, the link is disabled, and the picker doesn’t work. We can fix that by wrapping our form in a NavigationView
:
struct ContentView: View {@State private var emailAddress = ""@State private var message = "Write your message here..."@State private var subject = "Help"@State private var includeLogs = falsevar body: some View {NavigationView {Form {Section(header: Text("How can we reach you?")) {TextField("Email Address", text: $emailAddress)}Section(header: Text("What can we help you with?")) {Picker("Subject", selection: $subject) {ForEach(["Help", "Suggestion", "Bug Report"], id: \.self) { subject inText(subject)}}}Section(header: Text("Briefly explain what‘s going on.")) {TextEditor(text: $message)}Section(footer: Text("This information will be sent anonymously.")) {Toggle("Include Logs", isOn: $includeLogs)}Button("Submit", action: {print("Submit button tapped")})}}}}
If we build and run the app now, we’ll see that the “Subject” picker is now enabled and fully functional:
Using enumerations with pickers
Currently, our form asks for an email address that we can use to reply to messages. Since email isn’t everyone’s preferred contact method, we want users to be able to provide a phone number instead. We can represent this choice — email or phone — using an enumeration:
enum ContactMethod {case email, phone}
Then, we can use a picker to let the user choose which contact method they’d prefer:
Picker("Contact Method", selection: $preferredContactMethod) {Text("Email").tag(ContactMethod.email)Text("Phone").tag(ContactMethod.phone)}
Finally, we can use a switch
statement to display different text fields depending on whether the user is entering an email address or a phone number:
switch preferredContactMethod {case .email:TextField("Email Address", text: $emailAddress)case .phone:TextField("Phone Number", text: $phoneNumber)}
If we put this all together in our ContentView
, it should look like the code below. Note that two State
variables have been added:
phoneNumber
, with an initial value of""
preferredContactMethod
, with an initial value ofContactMethod.email
struct ContentView: View {@State private var emailAddress = ""@State private var phoneNumber = ""@State private var message = "Write your message here..."@State private var subject = "Help"@State private var includeLogs = false@State private var preferredContactMethod = ContactMethod.emailenum ContactMethod {case email, phone}var body: some View {NavigationView {Form {Section(header: Text("How can we reach you?")) {Picker("Contact Method", selection: $preferredContactMethod) {Text("Email").tag(ContactMethod.email)Text("Phone").tag(ContactMethod.phone)}switch preferredContactMethod {case .email:TextField("Email Address", text: $emailAddress)case .phone:TextField("Phone Number", text: $phoneNumber)}}Section(header: Text("What can we help you with?")) {Picker("Subject", selection: $subject) {ForEach(["Help", "Suggestion", "Feature Request"], id: \.self) { subject inText(subject)}}}Section(header: Text("Briefly explain what‘s going on.")) {TextEditor(text: $message)}Section(footer: Text("This information will be sent anonymously.")) {Toggle("Include Logs", isOn: $includeLogs)}Button("Submit", action: {print("Submit button tapped")})}}}}
If we build our app and run it in the simulator, we’ll see that we can choose our preferred contact method using the picker, and the view will update accordingly.
This works, but it could be made better. Since there are only two possible contact methods, this picker seems a little overkill. Fortunately, SwiftUI lets us choose from a few different picker styles that might better suit our needs. We can apply a picker style using the .pickerStyle()
view modifier. For example, if we’re using the SegmentedPickerStyle
, which presents the picker options as a group of segmented controls, then the view modifier would look like this:
.pickerStyle(SegmentedPickerStyle())
After applying the SegmentedPickerStyle
to our “Contact Method” picker, here’s what our ContentView
code should look like:
struct ContentView: View {@State private var emailAddress = ""@State private var phoneNumber = ""@State private var message = "Write your message here..."@State private var subject = "Help"@State private var includeLogs = false@State private var preferredContactMethod = ContactMethod.emailenum ContactMethod {case email, phone}var body: some View {NavigationView {Form {Section(header: Text("How can we reach you?")) {Picker("Contact Method", selection: $preferredContactMethod) {Text("Email").tag(ContactMethod.email)Text("Phone").tag(ContactMethod.phone)}.pickerStyle(SegmentedPickerStyle())switch preferredContactMethod {case .email:TextField("Email Address", text: $emailAddress)case .phone:TextField("Phone Number", text: $phoneNumber)}}Section(header: Text("What can we help you with?")) {Picker("Subject", selection: $subject) {ForEach(["Help", "Suggestion", "Feature Request"], id: \.self) { subject inText(subject)}}}Section(header: Text("Briefly explain what‘s going on.")) {TextEditor(text: $message)}Section(footer: Text("This information will be sent anonymously.")) {Toggle("Include Logs", isOn: $includeLogs)}Button("Submit", action: {print("Submit button tapped")})}}}}
If we rebuild our app and run it in the simulator one more time, we’ll see that we can choose our preferred contact method with only one tap, and the view will be updated accordingly.
Adding a navigation title
You might’ve noticed the large blank space at the top of our form, which popped up after we wrapped our form in a NavigationView
. That blank space is our navigation bar, which is empty because we haven’t given our view a navigation title yet. As a finishing touch, let’s fix that by using the .navigationTitle()
modifier to give our form a title of “Contact Us”. Our finished code for ContentView
should now look like this:
struct ContentView: View {@State private var emailAddress = ""@State private var phoneNumber = ""@State private var message = "Write your message here..."@State private var subject = "Help"@State private var includeLogs = false@State private var preferredContactMethod = ContactMethod.emailenum ContactMethod {case email, phone}var body: some View {NavigationView {Form {Section(header: Text("How can we reach you?")) {Picker("Contact Method", selection: $preferredContactMethod) {Text("Email").tag(ContactMethod.email)Text("Phone").tag(ContactMethod.phone)}.pickerStyle(SegmentedPickerStyle())switch preferredContactMethod {case .email:TextField("Email Address", text: $emailAddress).textContentType(.emailAddress).disableAutocorrection(true)case .phone:TextField("Phone Number", text: $phoneNumber)}}Section(header: Text("What can we help you with?")) {Picker("Subject", selection: $subject) {ForEach(["Help", "Suggestion", "Bug Report"], id: \.self) { subject inText(subject)}}}Section(header: Text("Briefly explain what‘s going on.")) {TextEditor(text: $message)}Section(footer: Text("This information will be sent anonymously.")) {Toggle("Include Logs", isOn: $includeLogs)}Button("Submit", action: {print("Submit button tapped")})}.navigationTitle("Contact Us")}}}
If we rebuild and run our app one more time, we’ll see that the blank space at the top of the screen now contains our navigation title:
Review
Congratulations! You’ve created your first form in SwiftUI. As you move forward in this path, you’ll use forms with pickers and other controls to allow users to enter and edit information.
Author
'The Codecademy Team, composed of experienced educators and tech experts, is dedicated to making tech skills accessible to all. We empower learners worldwide with expert-reviewed content that develops and enhances the technical skills needed to advance and succeed in their careers.'
Meet the full teamRelated articles
- Article
Building Lists in SwiftUI
In this article we learn how to create list views in our SwiftUI apps using the `List` structure. We will cover how to populate the contents of the list using a dynamic data source. - Article
Create Stunning Tailwind CSS Forms: A Step-by-Step Guide
Create stunning Tailwind CSS forms with this beginner-friendly step-by-step guide.