iOS / Swift / Swift values and SwiftUI ownership / Lesson 5
Forms, drafts, and validation
Keep editing forgiving while creating explicit validated commands and preserving work through recoverable failures.
On this page
Lesson goals
- A form is an editing workflow
- Validate at the command boundary
- Make the draft owner explicit
A form is an editing workflow
Form provides a container for data-entry controls, but it does not decide when a draft is valid or when it becomes saved data. Users need intermediate states such as an empty required field while correcting it. Keep text editing separate from accepted domain values and define Save, Cancel, pending, and failure behavior before connecting a backend. A direct binding into a shared saved model is inappropriate when Cancel should discard changes. Conversely, an immediately applied preference can use a direct binding if that behavior is clear and reversible under the product contract.
Validate at the command boundary
The first fragment parses a synthetic seat count using a documented positive range. It trims only permitted surrounding whitespace before parsing and rejects values outside one through twenty. This is a local teaching rule, not a universal capacity limit. A more informative result can distinguish missing, malformed, and out-of-range input when the interface needs targeted feedback. Numeric keyboard configuration does not enforce this rule; paste, hardware keyboards, and accessibility input can supply other values. The authoritative service must still check current capacity and authorization when a real command is evaluated.
import Foundation
func parseSeatDraft(_ text: String) -> Int? {
let normalized = text.trimmingCharacters(in: .whitespacesAndNewlines)
guard let value = Int(normalized), (1...20).contains(value) else { return nil }
return value
}
print(parseSeatDraft(" 4 ") ?? -1)
print(parseSeatDraft("21") ?? -1)
4
-1
The fallback only demonstrates parse failure. The accepted command should carry Int rather than the original text or a sentinel.
Make the draft owner explicit
The second fragment owns a text draft and a preview in local State. Save validates before updating the preview; invalid input leaves the last accepted preview unchanged. The form does not issue a remote write. A real submit needs pending state, a duplicate-command policy, and safe error mapping. Keep a visible label associated with the field rather than relying only on placeholder text that disappears during editing. Do not clear the draft before confirmation, since an ordinary network failure should not destroy work the user can still correct or retry.
struct SeatForm: View {
@State private var draft = ""
@State private var accepted: Int?
@State private var message = ""
var body: some View {
Form {
TextField("Seats", text: $draft).keyboardType(.numberPad)
Button("Preview") {
guard let value = parseSeatDraft(draft) else {
message = "Enter 1 through 20 seats"; return
}
accepted = value; message = "Preview ready"
}
Text(message)
if let accepted { Text("Seats: \(accepted)") }
}
}
}
A valid draft 4 yields Preview ready and Seats: 4; invalid input displays the range message.
Requires SwiftUI and the preceding parser. No keyboard, simulator, or form execution occurred during authoring.
Plan focus as part of correction
FocusState can coordinate which field is active, but focus should move for a clear task reason. A form with several errors can guide users to the first relevant field while preserving all drafts. Do not shift focus on every asynchronous update or continually steal it from assistive technology. Choose submit labels and next-field behavior coherently, noting that keyboard layouts differ and a number pad may not expose the same return action as a text keyboard. Keep a visible submission control so the workflow does not depend on one keyboard affordance.
Keep field errors and remote outcomes distinct
A valid-looking draft can be rejected remotely because availability changed or permission expired. That failure is different from an unparseable number. Map stable server categories or field identifiers into localized messages without exposing internal exception details. For an uncertain write outcome, do not assume the command failed and blindly repeat it; the server may already have committed. Preserve an operation identity and use the service’s reconciliation or idempotency contract. The client’s busy flag improves interaction but does not replace authoritative duplicate protection or access control.
Verify correction, cancellation, and repeated activation
Test an empty draft, malformed text, both accepted endpoints, a value just above the range, and permitted whitespace. Then verify that error correction removes obsolete messages, Cancel preserves saved data, and a failed submit keeps useful work. Exercise repeated activation before a fake response completes to establish the chosen pending policy. Add large text, Arabic labels, and keyboard-visible layout checks in an authorized target host. Pure parser tests establish only the input rule and cannot prove focus behavior or spoken error context on iOS.
Common errors and fixes
A number pad is treated as validation
- Why it happens
- An input hint was mistaken for a domain guarantee.
- How to fix it
- Parse and range-check the command explicitly.
- Prevent it next time
- Include pasted malformed input in tests.
A failed save clears the draft
- Why it happens
- Editing state was reset before confirmed success.
- How to fix it
- Keep the draft until success or deliberate cancellation.
- Prevent it next time
- Test rejected and uncertain remote outcomes.
Cancel cannot restore original data
- Why it happens
- The form edited the authoritative shared model directly.
- How to fix it
- Use a separate draft and an explicit commit command.
- Prevent it next time
- Specify save and cancel semantics before binding.
Check your understanding
1. Should an error message display the complete backend exception?
Show worked solution
No. Translate a stable application failure category into a useful localized explanation, preserving safe diagnostics through reviewed logging. Raw exceptions can expose internal details and often do not tell the user how to recover. Keep field identity separate from translated label text.
2. What should happen if Preview is activated with 21 after a successful 4?
Show worked solution
The range error appears and the accepted preview remains 4 under the shown contract. If the product instead wants to hide prior previews while editing, model that policy explicitly. A failed parse must never become accepted 21 or silently coerce to 20.
Key takeaways
- A form is an editing workflow
- Verify correction, cancellation, and repeated activation