History
Loading...
Loading...
September 19, 2025
@FocusState with validation to automatically highlight invalid fields when users navigate away. Combine it with onChange(of: focusedField) to trigger validation only after the user finishes editing a field, providing a smoother UX than real-time validation on every keystroke.This approach encapsulates validation logic within a property wrapper and creates reusable validated field components. The wrapper automatically validates input and exposes both the value and validation state, while the custom view provides consistent error display across your app.
@propertyWrapper
struct ValidatedField<T> {
private var value: T
private let validator: (T) -> String?
@State private var errorMessage: String?
var wrappedValue: T {
get { value }
nonmutating set {
value = newValue
errorMessage = validator(newValue)
}
}
var projectedValue: (value: T, error: String?, isValid: Bool) {
(value, errorMessage, errorMessage == nil)
}
init(wrappedValue: T, validator: @escaping (T) -> String?) {
self.value = wrappedValue
self.validator = validator
}
}
struct ValidatedTextField: View {
@Binding var field: (value: String, error: String?, isValid: Bool)
let title: String
var body: some View {
VStack(alignment: .leading, spacing: 4) {
TextField(title, text: .constant(field.value))
.textFieldStyle(.roundedBorder)
.overlay(
RoundedRectangle(cornerRadius: 8)
.stroke(field.error != nil ? .red : .clear, lineWidth: 1)
)
if let error = field.error {
Text(error)
.font(.caption)
.foregroundColor(.red)
}
}
}
}Property wrappers eliminate validation boilerplate and ensure consistency across forms. By separating validation logic from view code, you create testable, reusable components that maintain single responsibility principles. The projected value pattern provides clean access to validation state without exposing internal implementation details.