Skip to content

Validated Text Field

A text field that checks its value when it loses focus and shows its error only after that.

Validated Text Field, playing
Download .swift
Origin
Original work
Found by
text field · validation · error message · focus · form · input · campo de texto · validación · mensaje de error · foco · formulario

A text field that checks its value when it loses focus and shows its error only after that.

struct ValidatedTextFieldDemo: View {
@State private var email = ""
@State private var name = ""
var body: some View {
ScrollView {
VStack(alignment: .leading, spacing: 20) {
Text("Your details")
.font(.largeTitle.bold())
ValidatedTextField(title: "Email", text: $email) { value in
value.contains("@") && value.contains(".") ? nil : "Enter a valid email address."
}
.keyboardType(.emailAddress)
.textInputAutocapitalization(.never)
.autocorrectionDisabled()
ValidatedTextField(title: "Name", text: $name) { value in
value.isEmpty ? "Tell us your name." : nil
}
}
.padding(24)
}
.tint(.indigo)
.background(LinearGradient(colors: [.indigo.opacity(0.12), .orange.opacity(0.08)], startPoint: .top, endPoint: .bottom))
}
}
ParameterTypeDefaultDescription
titleLocalizedStringResourceRequired
textBinding<String>Required
validate(String) -> String?RequiredReturns the message to show, or nil when the value is fine.

Copy this code into your project, or download the file.

ValidatedTextField.swift
// SPDX-License-Identifier: MIT
// Copyright (c) 2026 Oscar Canton and SwiftUI Blocks contributors
import SwiftUI
/// Decides when the error is shown, so the field never complains while someone is typing.
struct ValidatedTextFieldFeedback: Equatable {
private(set) var message: String?
/// The only moment an error can appear.
mutating func fieldLostFocus(text: String, validate: (String) -> String?) {
message = validate(text)
}
/// While typing, an error can only go away, as soon as the text is fixed.
mutating func textChanged(text: String, validate: (String) -> String?) {
if message != nil, validate(text) == nil { message = nil }
}
}
/// A text field that checks its value when it loses focus and shows its error only after that.
struct ValidatedTextField: View {
let title: LocalizedStringResource
@Binding var text: String
/// Returns the message to show, or `nil` when the value is fine.
let validate: (String) -> String?
@State private var feedback = ValidatedTextFieldFeedback()
@FocusState private var isFocused: Bool
@Environment(\.accessibilityReduceMotion) private var reduceMotion
private var borderStyle: AnyShapeStyle {
if feedback.message != nil { return AnyShapeStyle(.red) }
return isFocused ? AnyShapeStyle(.tint) : AnyShapeStyle(.quaternary)
}
var body: some View {
VStack(alignment: .leading, spacing: 6) {
Text(title)
.font(.subheadline.weight(.medium))
.accessibilityHidden(true)
TextField("", text: $text)
.focused($isFocused)
.padding(14)
.background(.background, in: .rect(cornerRadius: 14, style: .continuous))
.overlay {
RoundedRectangle(cornerRadius: 14, style: .continuous)
.strokeBorder(borderStyle, lineWidth: 1.5)
}
.accessibilityLabel(title)
.accessibilityHint(feedback.message ?? "")
if let message = feedback.message {
Label(message, systemImage: "exclamationmark.circle.fill")
.font(.footnote)
.foregroundStyle(.red)
.transition(reduceMotion ? .opacity : .opacity.combined(with: .move(edge: .top)))
.accessibilityHidden(true)
}
}
.animation(reduceMotion ? nil : .snappy(duration: 0.25), value: feedback)
.onChange(of: isFocused) { _, focused in
guard !focused else { return }
feedback.fieldLostFocus(text: text, validate: validate)
if let message = feedback.message {
AccessibilityNotification.Announcement(message).post()
}
}
.onChange(of: text) { _, newValue in
feedback.textChanged(text: newValue, validate: validate)
}
}
}
// MARK: - Demo
struct ValidatedTextFieldDemo: View {
@State private var email = ""
@State private var name = ""
var body: some View {
ScrollView {
VStack(alignment: .leading, spacing: 20) {
Text("Your details")
.font(.largeTitle.bold())
ValidatedTextField(title: "Email", text: $email) { value in
value.contains("@") && value.contains(".") ? nil : "Enter a valid email address."
}
.keyboardType(.emailAddress)
.textInputAutocapitalization(.never)
.autocorrectionDisabled()
ValidatedTextField(title: "Name", text: $name) { value in
value.isEmpty ? "Tell us your name." : nil
}
}
.padding(24)
}
.tint(.indigo)
.background(LinearGradient(colors: [.indigo.opacity(0.12), .orange.opacity(0.08)], startPoint: .top, endPoint: .bottom))
}
}
#Preview {
ValidatedTextFieldDemo()
}