Changes to the bound value update the string displayed by the text field.

Is it possible to have a TextField that validates its input on every character entered by the user so that the string displayed by the text field updates only on successful validation?

The documentation on the TextField seems to suggest that this is possible by using a bound value. In my testing this is not the case. Even when validation fails, the text field always updates the string displayed.

Changes to the bound value update the string displayed by the text field. Editing the text field updates the bound value, as long as the formatter can parse the text. If the format style can’t parse the input, the bound value remains unchanged.

https://developer.apple.com/documentation/swiftui/textfield/init(_:value:formatter:)-9jw0i

The way I understand it, in case of an error thrown by ParseStrategy/parse(_ value: String) the bound value remains unchanged. Since the bound value did not change the string displayed by the text field should not be updated.

I have tried the code example used in the documentation overview of the TextField to create a sample app.

@State private var nameComponents = PersonNameComponents()

var body: some View {
    TextField(
        "Proper name",
        value: $nameComponents,
        format: .name(style: .medium)
    )
    .onSubmit {
        validate(components: nameComponents)
    }
    .disableAutocorrection(true)
    .border(.secondary)
    Text(nameComponents.debugDescription)
}

The bound value doesn’t have to be a string. By using a FormatStyle, you can bind the text field to a nonstring type, using the format style to convert the typed text into an instance of the bound type. The following example uses a PersonNameComponents.FormatStyle to convert the name typed in the text field to a PersonNameComponents instance. A Text view below the text field shows the debug description string of this instance.

https://developer.apple.com/documentation/swiftui/textfield

Answered by DTS Engineer in 803130022

Hello @qnoid,

One approach here is to use view encapsulation to separate the "real text" and the text that the TextField is editing. Then, after validating in onSubmit, you can take the appropriate action depending on whether the submitted text is valid or not:

struct ValidatedTextField<Label>: View where Label: View {
    
    @Binding var text: String
    
    private let label: () -> Label
    
    private let isValid: (String) -> Bool
    
    @State private var internalText: String
    
    
    init(text: Binding<String>, @ViewBuilder label: @escaping () -> Label, isValid: @escaping (String) -> Bool) {
        _text = text
        self.label = label
        self.isValid = isValid
        
        // Ensure the original text is valid.
        precondition(isValid(text.wrappedValue))
        
        internalText = text.wrappedValue
    }
    
    var body: some View {
        TextField(text: $internalText, label: label)
            .onSubmit {
                if isValid(internalText) {
                    text = internalText
                } else {
                    internalText = text
                }
            }
    }
}

Best regards,

Greg

Hello @qnoid,

One approach here is to use view encapsulation to separate the "real text" and the text that the TextField is editing. Then, after validating in onSubmit, you can take the appropriate action depending on whether the submitted text is valid or not:

struct ValidatedTextField<Label>: View where Label: View {
    
    @Binding var text: String
    
    private let label: () -> Label
    
    private let isValid: (String) -> Bool
    
    @State private var internalText: String
    
    
    init(text: Binding<String>, @ViewBuilder label: @escaping () -> Label, isValid: @escaping (String) -> Bool) {
        _text = text
        self.label = label
        self.isValid = isValid
        
        // Ensure the original text is valid.
        precondition(isValid(text.wrappedValue))
        
        internalText = text.wrappedValue
    }
    
    var body: some View {
        TextField(text: $internalText, label: label)
            .onSubmit {
                if isValid(internalText) {
                    text = internalText
                } else {
                    internalText = text
                }
            }
    }
}

Best regards,

Greg

@DTS Engineer thank you for the prompt response. Should I raise a Radar for this? It's not clear to me if this is a bug or if it works as expected.

Is there a workaround or an alternative on how to perform string validation as the user types a string so that the string displayed by the text field updates only on successful validation?

Thus onChange only fires upon successful validation (i.e. change) of the string.

Hello @qnoid,

You are welcome to file an enhancement request using Feedback Assistant, to request that TextField more directly support text validation.

In the meantime, the snippet I posted above works today.

Best regards,

Greg

Changes to the bound value update the string displayed by the text field.
 
 
Q