Handling text input with delegates in Swift
Text fields are among the first UIKit controls most junior iOS developers use. A sign-in screen, search bar, checkout form, or profile editor all depend on receiving text, checking it, and responding at the right moment. UIKit gives you the UITextField control, while its delegate protocol lets you decide what users may enter and what should happen when they tap Return.
The useful part is that text input involves several small decisions rather than one large feature. You need to connect the delegate, manage focus, validate values without frustrating the person typing, and make the keyboard work well. These patterns are especially valuable when building apps for Australian users, where forms may include four-digit postcodes, state abbreviations, mobile numbers, and suburb names.
Choosing the right text field behaviour
A UITextField is designed for a single line of text. You can configure its keyboard, placeholder, text alignment, secure-entry mode, and return-key behaviour. For example, an email field should use .emailAddress, while a password field usually needs isSecureTextEntry = true.
let emailField = UITextField()
emailField.placeholder = "Email address"
emailField.keyboardType = .emailAddress
emailField.autocapitalizationType = .none
emailField.autocorrectionType = .no
emailField.returnKeyType = .next
The configuration should match the expected input, but it should not become an excuse to trust the keyboard completely. A user can paste text, use dictation, connect a hardware keyboard, or enter content from an accessibility feature. Treat keyboard settings as guidance and use validation logic for the real rules.
An Australian address form is a good example. A postcode field might accept exactly four digits, but a suburb field should allow spaces and apostrophes. Someone in Sydney may enter “St Kilda” even though that suburb is in Melbourne, so checking the format is the text field’s job; checking whether the suburb belongs to a particular address is a later business rule.
Connecting a delegate safely
The UITextFieldDelegate protocol receives events from a text field. Assign the view controller as the delegate, then implement only the methods needed by the screen. The most commonly used callbacks are textFieldShouldReturn(_:), textFieldDidBeginEditing(_:), textFieldDidEndEditing(_:), and textField(_:shouldChangeCharactersIn:replacementString:).
final class LoginViewController: UIViewController, UITextFieldDelegate {
@IBOutlet private weak var emailField: UITextField!
@IBOutlet private weak var passwordField: UITextField!
override func viewDidLoad() {
super.viewDidLoad()
emailField.delegate = self
passwordField.delegate = self
}
func textFieldShouldReturn(_ textField: UITextField) -> Bool {
if textField === emailField {
passwordField.becomeFirstResponder()
} else {
textField.resignFirstResponder()
}
return true
}
}
Using identity checks such as textField === emailField is clearer than relying on tag values. Tags can work for a tiny prototype, but named outlets make the relationship between the control and its behaviour easier to understand. If you create fields programmatically, set their delegates after initialising them and keep the delegate alive for as long as the fields need it.
There is an architectural choice here. A view controller is fine for a small screen, but a large form can become difficult to maintain when it owns every validation rule. Keeping navigation decisions in the controller and moving reusable formatting into separate types gives you cleaner code. The same separation is useful when comparing navigation approaches, such as this routing comparison, even though the example is from another technology stack.
Validating and filtering user input
The shouldChangeCharactersIn delegate method is called before UIKit applies a proposed edit. It provides the current NSRange and replacement string, which means you can calculate the complete future value before accepting it.
func textField(
_ textField: UITextField,
shouldChangeCharactersIn range: NSRange,
replacementString string: String
) -> Bool {
guard let currentText = textField.text,
let textRange = Range(range, in: currentText) else {
return false
}
let updatedText = currentText.replacingCharacters(
in: textRange,
with: string
)
if textField === postcodeField {
return updatedText.count <= 4 &&
updatedText.allSatisfy(\.isNumber)
}
return true
}
Converting NSRange into a Swift Range<String.Index> matters because Swift strings support extended grapheme clusters. Direct integer indexing can break with accented characters or emoji. A safe conversion also handles invalid ranges rather than crashing.
Avoid showing an error on every keystroke unless the rule is genuinely immediate. For a required email field, an empty value is perfectly normal while the user is still typing. A better approach is to allow editing, then validate on textFieldDidEndEditing or when the user submits the form.
func textFieldDidEndEditing(_ textField: UITextField) {
if textField === emailField {
let value = textField.text?.trimmingCharacters(in: .whitespacesAndNewlines) ?? ""
emailField.layer.borderColor =
isValidEmail(value) ? UIColor.systemGreen.cgColor : UIColor.systemRed.cgColor
}
}
Validation should produce useful feedback. “Invalid input” is vague; “Enter a four-digit postcode” tells the user what to fix. Also avoid silently deleting meaningful characters. A person typing “O’Connor” or “McArthur” should not lose punctuation simply because a narrow filter was written for a basic demo.
Managing focus, keyboards, and submission
A polished form moves naturally from one field to the next. becomeFirstResponder() opens the keyboard and focuses a field, while resignFirstResponder() dismisses it. A screen can also dismiss the keyboard when the user taps outside the form.
override func touchesBegan(
_ touches: Set<UITouch>,
with event: UIEvent?
) {
view.endEditing(true)
super.touchesBegan(touches, with: event)
}
For a longer form, put the fields inside a UIScrollView or use a keyboard-management approach so the active control stays visible. This matters on smaller iPhones and when Dynamic Type increases text sizes. It also makes the experience better for people using an external keyboard, where the on-screen keyboard may not appear at all.
Return-key handling can act as a lightweight form workflow. .next should advance to the next field, while .done can dismiss editing or trigger a final validation. Do not submit automatically just because a user pressed Return unless the screen’s purpose makes that behaviour obvious, such as a search field.
Australian apps often collect mobile numbers in several formats, including 04xx xxx xxx and numbers beginning with +61. Keep the raw value and the display format conceptually separate. A formatter can show spaces for readability, while a normalisation step converts the number into a consistent form before sending it to an API. The same principle applies to suburbs, states, and postcodes.
Building input that is easy to test
Delegate methods are event-driven, so testing them directly is useful. You can check that a postcode accepts “4000”, rejects letters, and stops accepting characters after four digits. You can also test that pressing Return moves focus from the email field to the password field.
Keep validation functions small and independent from UIKit where possible:
func isValidPostcode(_ value: String) -> Bool {
value.count == 4 && value.allSatisfy(\.isNumber)
}
func isValidEmail(_ value: String) -> Bool {
value.contains("@") && value.contains(".")
}
These examples are deliberately simple. Production validation may need stronger rules, server-side confirmation, and clear handling of optional fields. Still, pure functions are easy to test and easier to reuse than logic buried inside a view controller callback.
Reusable text-field components can hold common behaviour such as border styling, error labels, placeholder configuration, and accessibility hints. Give every field an informative accessibilityLabel, and make sure an error message is available to VoiceOver users rather than communicated only through colour. Resources from an iOS developer community can also be useful when comparing UIKit patterns and learning how other developers structure practical projects.
Use Xcode’s View Debugger when a field appears on screen but does not receive input. Check that the view is enabled, not covered by another view, and connected to the expected outlet. If the delegate callback never runs, verify the delegate assignment before changing the validation code.
Text fields become much easier to manage when each responsibility has a clear location: UIKit controls collect input, delegates coordinate editing events, validation methods check values, and model or networking code handles the submitted data. Start with a small form, test pasted and empty values, and then add formatting only where it improves clarity. A practical rule is to accept normal typing first, validate the complete value at the right boundary, and give specific feedback when something needs fixing.