A form collects editable input, but editable input is not yet a valid business request. A quantity field can temporarily be empty while somebody edits it. A product code can contain surrounding whitespace. Keep those draft strings separate from the typed value that the application is prepared to submit.
Review the SwiftUI state lesson. This reference develops the form behavior around that state: validation, feedback, focus and accessibility. The complete UI example targets an iOS 16+ SwiftUI app. Its keyboard modifiers are iOS-specific, so copying it into a macOS-only target requires adapting those controls.
Draft state and valid state serve different jobs
TextField bindings hold editable text. Converting every keystroke directly into a required Int can make normal editing awkward because an empty or partially typed string has no integer value. Preserve draft text and derive a validated request when needed.
Place reusable validation in a pure model function or initializer. The view can use that result to enable submission and present help, while the action validates again at its boundary. A disabled button is a helpful interaction rule, not a substitute for the domain contract; other callers or a future UI may invoke the same operation.
State the accepted rule clearly. This example accepts a nonblank trimmed code and an integer quantity from one through fifty. A number-pad keyboard makes entry convenient but does not prove pasted text is numeric or in range. Parsing and validation still matter.
Feedback should explain how to recover
Permanent help communicates the format before an error occurs. Larger forms can also show field-specific messages after a field has been edited or submission attempted. Avoid marking every empty field as an alarming error immediately on first display, and avoid waiting until a failed server request to explain a local numeric rule.
Use words as well as color. A red border alone is not enough for a person who cannot perceive that color or who uses a screen reader. Keep the message associated with the relevant field and make the needed correction concrete, such as enter a whole number from one through fifty.
Separate local validation from server rejection. A code can fit the local grammar but not exist in inventory, and the server remains responsible for authoritative permissions and stock consistency. Present that response with a useful retry or correction path instead of pretending local validation guarantees success.
Clear or version a prepared status when the draft changes. Otherwise a message saying Ready PEN:4 can remain beside fields now containing BOOK and 9, leaving the user uncertain which values will be submitted. This example clears the status through onChange using the callback form available on its iOS 16 baseline. A newer-only target can adopt the newer overload while preserving the same behavior.
Accessibility and focus are part of behavior
Provide meaningful control labels and avoid using a placeholder as the only explanation. Native controls offer useful accessibility behavior, but verify the actual reading order and labels with VoiceOver. Hints should add information rather than repeat the label word for word.
FocusState can move between fields and dismiss focus after a successful local action. A number pad may not offer a standard return key, so an accessible explicit action remains useful. Keep controls reachable at larger Dynamic Type sizes and avoid fixed heights that clip text or hide feedback.
Test keyboard entry, pasted text, assistive technology and invalid transitions. A form that works with one simulator screenshot may still fail when the user increases text size or navigates without tapping. Treat those interactions as normal supported paths rather than optional polish added after the data model is finished.
Official references
Read Form, FocusState and accessibility hints). Review the form as an editable state machine: draft, local validity, submission, success or rejection. Test meaningful labels, large text, keyboard paths and correction messages alongside the pure validation boundary.
SwiftUI Forms, Validation and Accessibility Examples
Each example shows the code, the output it produces and what each part does.
A complete accessible iOS form
Create a new iOS SwiftUI app and replace its app source with this complete example. Foundation supplies whitespace trimming. The form stores drafts and constructs an immutable request only after validation.
After entering PEN and 4, then activating Prepare reorder, the visible status is:
The initial button is disabled because the drafts are invalid. This output is a UI state, not console output from a command-line program. The demonstration prepares a request locally and does not send a stock update.
import SwiftUI
import Foundation
struct ReorderRequest {
let code: String
let quantity: Int
init?(code: String, quantityText: String) {
let cleaned = code.trimmingCharacters(in: .whitespacesAndNewlines)
guard !cleaned.isEmpty, let quantity = Int(quantityText),
(1...50).contains(quantity) else { return nil }
self.code = cleaned
self.quantity = quantity
}
}
struct ReorderForm: View {
enum Field: Hashable { case code, quantity }
@State private var code = ""
@State private var quantity = ""
@State private var message = ""
@FocusState private var focused: Field?
private var request: ReorderRequest? {
ReorderRequest(code: code, quantityText: quantity)
}
var body: some View {
NavigationStack {
Form {
Section("Product") {
TextField("Product code", text: $code)
.textInputAutocapitalization(.never)
.autocorrectionDisabled()
.focused($focused, equals: .code)
.submitLabel(.next)
.onSubmit { focused = .quantity }
Text("A product code is required.")
}
Section("Quantity") {
TextField("Quantity", text: $quantity)
.keyboardType(.numberPad)
.focused($focused, equals: .quantity)
.accessibilityHint("Enter a whole number from one through fifty")
Text("Enter 1–50 whole units.")
}
Button("Prepare reorder") {
guard let request else { return }
focused = nil
message = "Ready \(request.code):\(request.quantity)"
}
.disabled(request == nil)
if !message.isEmpty { Text(message) }
}
.navigationTitle("Reorder")
.onChange(of: code) { _ in message = "" }
.onChange(of: quantity) { _ in message = "" }
}
}
}
@main struct ReorderApp: App {
var body: some Scene { WindowGroup { ReorderForm() } }
}Ready PEN:4SwiftUI Forms, Validation and Accessibility in Real Projects
Worked exercise: Test the request boundary independently
Reuse the same validation contract in a standalone Foundation program. Verify trimming, a valid range and rejection of empty or noninteger drafts. The view should call this kind of boundary instead of owning a separate incompatible set of rules.
import Foundation
struct Request {
let code: String
let quantity: Int
init?(code: String, quantityText: String) {
let cleaned = code.trimmingCharacters(in: .whitespacesAndNewlines)
guard !cleaned.isEmpty, let quantity = Int(quantityText),
(1...50).contains(quantity) else { return nil }
self.code = cleaned
self.quantity = quantity
}
}
if let request = Request(code: " PEN ", quantityText: "4") {
print("\(request.code):\(request.quantity)")
}
print(Request(code: " ", quantityText: "4") == nil)
print(Request(code: "PEN", quantityText: "4.5") == nil)
print(Request(code: "PEN", quantityText: "51") == nil)PEN:4
true
true
trueThese checks exercise the model without needing a UI harness. Add boundary tests for one and fifty and make a deliberate policy for whitespace in numeric input. UI tests should then focus on interaction and feedback, not duplicate every parser case through slow screen automation.
SwiftUI Forms, Validation and Accessibility FAQ
Does a number pad guarantee valid numeric input?
No. It assists entry, but pasted text, hardware keyboards and programmatic updates can still produce unexpected drafts. Even a valid integer can be outside the accepted business range. Parse the string and validate the result at the request boundary. Keeping draft text separate also supports temporary empty input while editing. The keyboard choice improves usability; it does not replace a typed validation contract or server-side checks.
Is disabling the submit button enough validation?
It is useful feedback, but the action and domain boundary should still construct or validate a proper request. Other callers, future interfaces or asynchronous state changes can bypass a particular button's enabled state. Reuse one validation policy so the UI and operation agree. Also explain why submission is unavailable; an unexplained disabled control can leave users stuck without knowing which field or format needs attention.
Should errors be communicated only with red styling?
No. Color can supplement a clear textual message, but it cannot be the only information channel. Users may have color-vision differences or rely on assistive technology. Associate useful corrective text with the relevant control, preserve readable contrast and verify VoiceOver behavior. Native accessibility support is a foundation rather than proof that the entire form's custom messages and reading order are understandable under every interaction path.
Can local validation guarantee a reorder succeeds?
No. It checks the client-side shape and selected business constraints, while the server remains authoritative for permissions, current inventory and transaction rules. A locally valid code can refer to a missing item, and stock can change before submission completes. Present server rejection as a distinct recoverable outcome. Keep cancellation and stale-result handling in the submission workflow so an old response does not overwrite a newer draft or status.
SwiftUI Forms, Validation and Accessibility Exercises
Answer each one and check it instantly. If you get stuck, open the hint before the answer.
Where should reusable code-and-quantity rules live?
SwiftUI Forms, Validation and Accessibility Quiz
2 questions to check what you learned.
Why keep a quantity draft as String while editing?
- Numeric ranges never need checking.
- Strings automatically authorize stock updates.
- Temporary empty or partial input is legitimate before a valid Int request exists.
What should accompany color-based error styling?
- A useful textual explanation associated with the relevant field.
- Only a smaller placeholder with no label.
- A hidden error that appears only in debug logs.