Centralize Form Validation in Swift with a Reusable @Validated Property Wrapper
Repetitive validation logic in Swift form models creates drift. A reusable @Validated property wrapper centralizes rules, exposes published error state, and keeps SwiftUI views declarative while making trade-offs explicit.
18 Sept 2026, 20:42 UTC

Form models accumulate the same boilerplate: a raw value, a flag for validity, an error string, and a manual check that runs on every change. When each field needs its own rules and messages, the duplication spreads across view models and makes consistent error handling hard to maintain.
A reusable @Validated property wrapper moves the rule and its state into one place. The wrapper owns the value, runs a validation closure on set, and exposes a published error state that SwiftUI or Combine can observe. That gives compile-time type safety for the field type and a single source of truth for validity without repeating didSet logic.
Designing a wrapper that owns validation state
The wrapper needs three things: storage for the wrapped value, a validation closure that returns a Bool and a message, and an observable error. With Swift 5.1 or later property wrappers are available, and with iOS 13+ / macOS 10.15+ Combine's @Published can drive UI updates.
struct Validated {
private var validator: (Value) -> (Bool, String)
private var _value: Value
@Published private(set) var error: String?
var wrappedValue: Value {
get { _value }
set {
_value = newValue
let (ok, msg) = validator(newValue)
error = ok ? nil : msg
}
}
init(wrappedValue: Value, validate: @escaping (Value) -> (Bool, String)) {
self._value = wrappedValue
self.validator = validate
let (ok, msg) = validate(wrappedValue)
self.error = ok ? nil : msg
}
var projectedValue: Validated { self }
}The wrapper stores the closure, updates error on every write, and projects itself so $property gives access to error. The design keeps validation logic out of the model and out of the view.
Integration with SwiftUI and Combine
A form model becomes declarative. Each field is annotated with @Validated and a rule. The projected value provides error for binding.
struct LoginForm {
@Validated(validate: { $0.count >= 3 ? (true, "") : (false, "Username too short") })
var username: String = ""
@Validated(validate: { $0.count >= 8 ? (true, "") : (false, "Password must be 8+ characters") })
var password: String = ""
}In a SwiftUI view you bind to the wrapped value and read error from the projection. The view reacts automatically because error is @Published.
struct LoginView: View {
@StateObject var form = LoginForm()
var body: some View {
VStack {
TextField("Username", text: $form.username.wrappedValue)
if let err = form.$username.error { Text(err).foregroundColor(.red) }
SecureField("Password", text: $form.password.wrappedValue)
if let err = form.$password.error { Text(err).foregroundColor(.red) }
}
}
}Verification can be done in an Xcode playground or a small test target. Create the wrapper, instantiate a model, assign valid and invalid values, and assert that error is nil or set accordingly. In SwiftUI, open the preview and change the fields to confirm errors appear and clear as input changes.
Trade-offs and limits
The wrapper reduces duplication but adds indirection. Validation logic lives inside a closure captured by the wrapper, which can obscure where a rule originates during debugging. Runtime overhead is small for UI forms but not zero; each set triggers the closure and a potential @Published emission.
Platform constraints matter. Property wrappers require Swift 5.1+. Combine integration requires iOS 13+ or macOS 10.15+. Older targets need alternative state management, such as manual callbacks.
Complex cross-field validation does not fit neatly into a single-field wrapper. Rules like "password must match confirm password" are clearer in the form model with an explicit validateAll method rather than hidden inside two wrappers.
Use @Validated when fields have independent, reusable rules and you want consistent error surfacing across SwiftUI views. Keep complex dependencies explicit in the model, and write unit tests that exercise the closure for boundary values to ensure the error state is correct.
0 replies
A thoughtful contribution can make all the difference. Be the first to share one.