Real-Time Form Validation

Forms with real-time validation provide instant feedback. Here's how to build them with computed properties.
Follow along with the code: iOS-Practice on GitHub
The Form Model
class RegistrationFormModel: ObservableObject {
@Published var fullName = ""
@Published var email = ""
@Published var password = ""
@Published var confirmPassword = ""
@Published var country = ""
@Published var age = 18
@Published var acceptedTerms = false
let countries = ["United States", "Canada", "United Kingdom", "Australia"]
}
Computed Validation Properties
var fullNameError: String? {
guard !fullName.isEmpty else { return nil }
if fullName.count < 2 {
return "Name must be at least 2 characters"
}
return nil
}
var emailError: String? {
guard !email.isEmpty else { return nil }
let emailRegex = /^[A-Za-z0-9._%+-]+@[A-Za-z0-9.-]+\.[A-Za-z]{2,}$/
if email.wholeMatch(of: emailRegex) == nil {
return "Please enter a valid email address"
}
return nil
}
var passwordError: String? {
guard !password.isEmpty else { return nil }
if password.count < 8 {
return "Password must be at least 8 characters"
}
return nil
}
var confirmPasswordError: String? {
guard !confirmPassword.isEmpty else { return nil }
if confirmPassword != password {
return "Passwords do not match"
}
return nil
}
Overall Validity
var isValid: Bool {
!fullName.isEmpty &&
fullNameError == nil &&
!email.isEmpty &&
emailError == nil &&
!password.isEmpty &&
passwordError == nil &&
!confirmPassword.isEmpty &&
confirmPasswordError == nil &&
!country.isEmpty &&
acceptedTerms
}
The Form View
struct FormValidationExerciseView: View {
@StateObject private var formModel = RegistrationFormModel()
@State private var showSuccessAlert = false
var body: some View {
Form {
Section("Personal Info") {
TextField("Full Name", text: $formModel.fullName)
.textContentType(.name)
if let error = formModel.fullNameError {
Text(error)
.font(.caption)
.foregroundColor(.red)
}
TextField("Email", text: $formModel.email)
.textContentType(.emailAddress)
.keyboardType(.emailAddress)
.autocapitalization(.none)
if let error = formModel.emailError {
Text(error)
.font(.caption)
.foregroundColor(.red)
}
}
Section("Password") {
SecureField("Password", text: $formModel.password)
.textContentType(.newPassword)
if let error = formModel.passwordError {
Text(error)
.font(.caption)
.foregroundColor(.red)
}
SecureField("Confirm Password", text: $formModel.confirmPassword)
if let error = formModel.confirmPasswordError {
Text(error)
.font(.caption)
.foregroundColor(.red)
}
}
Section {
Button("Register") {
submitForm()
}
.disabled(!formModel.isValid)
}
}
}
}
Why Computed Properties?
- Real-time - Updates as user types
- No state management - Derived from source
- Testable - Pure functions of input
- Composable - Combine for overall validity
Key Patterns
Show errors only after input:
guard !email.isEmpty else { return nil }
Disable submit until valid:
.disabled(!formModel.isValid)
Use appropriate keyboard types:
.keyboardType(.emailAddress)
.textContentType(.newPassword)
Interview Tip
This demonstrates reactive validation without external libraries. The pattern scales to complex forms with many fields.