Skip to content

How to use TextField in SwiftUI | SwiftUI Bootcamp #35

Almost every app has a place where users type something: a search bar, a username field, a tag input. This lesson builds a complete text input flow — a styled TextField, a validation function that enables or disables the save button, and a list that grows as entries are added.

What You'll Learn

  • How TextField binds to a @State string and updates live as the user types
  • How to validate input and use the result to enable/disable a button and change its appearance
  • How to clear the text field after saving, and how to grow a list from user input

Mental Model

Think of TextField like a whiteboard that two people share. The @State var textFieldText is the surface of the whiteboard — it holds the current writing. TextField is one person writing on it. Your Text(data) views in the list are another person reading what was written. When the whiteboard changes (the user types), everyone looking at it sees the update immediately.

The $textFieldText binding is the shared reference to the whiteboard — not a copy, but a live connection. The $ means "I want to write to this whiteboard", not just read from it.

Detailed Explanation

TextField("placeholder", text: $textFieldText) creates an editable text field that is two-way bound to textFieldText. Every keystroke updates the string in real time. This means you can run validation (textIsAppropriate()) on every character change just by calling it from within body.

Validation-driven UI is a core SwiftUI pattern. Instead of checking on submit, you evaluate the validation function as a computed expression inside modifiers. .background(textIsAppropriate() ? Color.blue : Color.gray) gives instant feedback — the button turns blue the moment the text becomes valid. .disabled(!textIsAppropriate()) prevents submission of invalid input at the interaction level, not just the visual level.

saveText() appends textFieldText to dataArray and then resets textFieldText = "". Setting it back to empty clears the TextField because of the two-way binding. The new entry immediately appears at the bottom of the ForEach list because dataArray is a @State array.

The commented-out .textFieldStyle(RoundedBorderTextFieldStyle()) shows SwiftUI's built-in styling. It's a quick option but offers less customization than the manual padding + background + cornerRadius approach used in the active code.

Code Structure

TextfieldBootcamp.swift demonstrates a complete input-save-display pattern. The TextField is the input. textIsAppropriate() is the validation gate used in two places (button color and .disabled). saveText() handles the submission. ForEach(dataArray) renders all saved entries below the input.

Complete Code

TextfieldBootcamp.swift

swift
import SwiftUI

struct TextfieldBootcamp: View {
    
    @State var textFieldText: String = ""     // live text binding — updates with every keystroke
    @State var dataArray: [String] = []       // stores all saved entries
    
    var body: some View {
        NavigationView {
            VStack {
                TextField("Type something here...", text: $textFieldText) // placeholder text + two-way binding
                    //.textFieldStyle(RoundedBorderTextFieldStyle()) // built-in style — simpler but less customizable
                    .padding()
                    .background(Color.gray.opacity(0.3).cornerRadius(10)) // custom styled background
                    .foregroundColor(.red)     // text color while typing
                    .font(.headline)
                                
                Button(action: {
                    if textIsAppropriate() {   // guard: only saves if input meets requirements
                        saveText()
                    }
                }, label: {
                    Text("Save".uppercased())
                        .padding()
                        .frame(maxWidth: .infinity)                              // full-width button
                        .background(textIsAppropriate() ? Color.blue : Color.gray) // visual validation feedback
                        .cornerRadius(10)
                        .foregroundColor(.white)
                        .font(.headline)
                })
                .disabled(!textIsAppropriate()) // disables tap interaction when input is invalid
                
                ForEach(dataArray, id: \.self) { data in // renders each saved entry as a text row
                    Text(data)
                }
                
                Spacer()
            }
            .padding()
            .navigationTitle("TextField Bootcamp!")
        }
    }
    
    func textIsAppropriate() -> Bool {
        // check text
        if textFieldText.count >= 3 { // minimum 3 characters required
            return true
        }
        return false
    }
    
    func saveText() {
        dataArray.append(textFieldText)  // adds current text to the list
        textFieldText = ""               // clears the TextField via the binding
    }
}

struct TextfieldBootcamp_Previews: PreviewProvider {
    static var previews: some View {
        TextfieldBootcamp()
    }
}

Code Walkthrough

  1. TextField("Type something here...", text: $textFieldText) — The first argument is placeholder text that appears when the field is empty. The text: $textFieldText binding means any keystroke updates textFieldText immediately, and any external change to textFieldText (like textFieldText = "" in saveText) updates what's shown in the field.

  2. textIsAppropriate() ? Color.blue : Color.gray — This function is called on every render. Because textFieldText changes every keystroke and textFieldText is used inside textIsAppropriate(), the button color updates reactively with every character typed. No explicit observer or listener is needed.

  3. .disabled(!textIsAppropriate()) — This is a critical UX detail. .disabled prevents the tap gesture entirely, not just the visual change. Combined with the gray color, it creates a clear "not yet ready" state.

  4. Text("Save".uppercased()).uppercased() is a Swift String method called before the text is passed to Text. This is equivalent to Text("SAVE") — it transforms the string at the data level, not the display level.

  5. dataArray.append(textFieldText) then textFieldText = "" — The order matters here. Append first (so the current text is saved), then clear. If you clear first, you'd append an empty string.

  6. ForEach(dataArray, id: \.self) — Renders all saved entries. Each new save causes dataArray to change, triggering a re-render of the ForEach. The new entry appears at the bottom of the list instantly, with SwiftUI's default insertion animation.

Common Mistakes

Mistake: Calling textIsAppropriate() inside the button action for validation, but not also using it in .disabled
If you only validate in the action closure, the button is still visually active and tappable even when invalid. Use .disabled(!textIsAppropriate()) to block the tap, and the matching ternary on background color to provide visual feedback. Both pieces work together.

Mistake: Using textFieldText.count > 3 instead of >= 3
This accepts only strings of 4+ characters. Users typing 3-character inputs ("abc") would be confused by the grayed button. Make sure your condition matches the minimum you communicate to the user.

Mistake: Forgetting textFieldText = "" in saveText()
Without clearing the field, the same text remains after saving. Users expect a save action to clear the input — this matches standard iOS form behavior.

Key Takeaways

  • TextField with a $ binding creates a live two-way sync between the field and a @State string — every keystroke is reflected immediately.
  • Validation logic as a simple Bool function can be used in multiple places (background, .disabled, if guards) to create cohesive, reactive UI.
  • Clearing the text field after saving is done by mutating the bound @State variable — the binding automatically reflects the change in the field.

Last updated: June 27, 2026

Released under the MIT License.