Forms and Pickers

Learn how to create forms with pickers and other controls in SwiftUI.

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:

VStack containing text, a button, and some input controls. The various elements appear jumbled together.

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:

Form element containing text, a button, and some input controls. The various elements look organized with consistent styling, sizing, and spacing.

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.

Xcode template selection screen with the platform set to iOS and template set to “App”

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.

Xcode options screen with product name set to “Contact Form”, interface set to SwiftUI, life cycle set to SwiftUI App, and language set to Swift. The “Use Core Data” and “Include Tests” options are 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.

Screenshot of form with ‘Hello, world’ text

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.

Screenshot of form with ‘Email Address’ text field selected. The text field contains a typing indicator, and the keyboard is visible at the bottom of the screen.

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.

Screenshot of form with a text editor added. The text editor says ‘Write your message here...’. The typing indicator and keyboard show that the text editor is selected.

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 = false
var 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:

Screenshot of form with ‘Include Logs’ toggle added.

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 = false
var 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.

Screenshot of form with ‘Submit’ button added.

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 = false
var 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.

Screenshot of form, now divided into sections.

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 in
Text(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 = false
var 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 in
Text(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.

Screenshot of form with grayed out ‘Subject’ picker.

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 = false
var 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 in
Text(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:

Screenshot of form with ‘Subject’ picker now enabled. There is some blank space added at the top of the screen.

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 of ContactMethod.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.email
enum 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 in
Text(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.

Screen recording of the form. At the top, there is a ‘Contact Method’ picker set to ‘Email’, and an email address is being entered in a text field. The picker is then selected, causing a selection screen to be shown. Tapping ‘Phone’ dismisses the selection screen. Now, the ‘Contact Method’ picker is set to ‘Phone’, and the ‘Email Address’ text field is replaced with a blank text field titled ‘Phone Number’, where a phone number is being entered. The picker is then changed back to ‘Email’, and the phone number is replaced by the email address entered earlier.

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.email
enum 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 in
Text(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.

Screen recording of the form. At the top, there are two options: ‘Email’ and ‘Phone’. ‘Email’ is selected, and an email address is being entered in a text field. Then, the picker is changed to ‘Phone’, and the ‘Email Address’ text field is replaced with a blank text field titled ‘Phone Number’, where a phone number is being entered. The picker is then changed back to ‘Email’, and the phone number is replaced by the email address entered earlier.

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.email
enum 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 in
Text(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:

Screenshot of the form with ‘Contact Us’ navigation title added.

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

Codecademy Team

'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 team