1

I'm trying to validate textFields to only allow numbers, a single period, or a single comma. In the code below I have two TextFields, textFieldOne and textFieldTwo, in textFieldOne I'm validating the field by calling the validateField() method, everything works as expected for textFieldOne since I'm modifying its value inside the validateField() method, but what I'm trying to do is make the validateField() method more modular and be able to pass a State variable to be able to use it with multiple fields to avoid code duplication but I haven't been able to figure out a way to pass the State variable as a parameter in the validateField() method. I tried passing it as validateField(textFieldName: Binding<String> ,newValue:String) but I got errors saying that it couldn't convert type 'String' to expected type 'Binding<String>.

How can I pass @State textFieldOne and textFieldOne respectably as a parameter in the validateField() method?

Again, what I'm trying to do is be able to call the validateField() method inside the .onChange modifier of each field, something like this... validateField(fieldName:textFieldOne, newValue: newValue). Basically, make the method reusable.

    struct TextFieldNumbersOnly: View {
        @State private var textFieldOne = ""
        @State private var textFieldTwo = ""
        
        var body: some View {
            Form{
                TextField("field one", text: $textFieldOne)
                    .keyboardType(.decimalPad)
                    .onChange(of: textFieldOne){newValue in
                        validateField(newValue: newValue)
                    }
                
                TextField("field two", text: $textFieldTwo)
                    .keyboardType(.decimalPad)
                    .onChange(of: textFieldTwo){newValue in
                        //validateField(newValue: newValue)
                    }
            }

        }
        
        func validateField(newValue:String){
            let periodCount = newValue.components(separatedBy: ".").count - 1
            let commaCount = newValue.components(separatedBy: ",").count - 1
            
            if newValue.last == "." && periodCount > 1 || newValue.last == "," && commaCount > 1{
                //it's a second period or comma, remove it
                textFieldOne = String(newValue.dropLast())
            }else{
                let filtered = newValue.filter { "0123456789.,".contains($0) }
                if filtered != newValue{
                    self.textFieldOne = filtered
                }
            }
        }
    }
fs_tigre
  • 10,650
  • 13
  • 73
  • 146
  • 2
    A “bare” access to a state variable just reads out its currently value. To pass a binding, you need to prefix it with `$`. (This accesses the `projectedValue` of a property wrapper, [which for `@State` is a Binding](https://developer.apple.com/documentation/swiftui/state/projectedvalue).) See https://stackoverflow.com/questions/56551131/what-does-the-dollar-sign-do-in-swift-swiftui – Alexander Jun 09 '23 at 03:08
  • 1
    Oh, the docs for State explain this: https://developer.apple.com/documentation/swiftui/state/projectedvalue – Alexander Jun 09 '23 at 03:10
  • Ah, that makes sense but when I pass it as `validateField(fieldName: $textFieldTwo)` I get error, `Cannot assign to value: 'fieldName' is a 'let' constant` when I try to assign a new value inside the `validateField` methods. Thanks. – fs_tigre Jun 09 '23 at 03:16

2 Answers2

0

You could try this alternative approach, to achieve what you want, ...to validate textFields to only allow numbers, a single period, or a single comma and to make the function validateField(...) more modular.

The approach simply uses a modified function validateField(...) that returns the validated String, as shown in the example code. It's clean and do what the function is supposed to do, take a String input and returns a validated one as output.

struct TextFieldNumbersOnly: View {
    @State private var textFieldOne = ""
    @State private var textFieldTwo = ""
    
    var body: some View {
        Form {
            TextField("field one", text: $textFieldOne)
                .keyboardType(.decimalPad)
                .onChange(of: textFieldOne){ newValue in
                    textFieldOne = validateField(newValue) // <-- here
                }
            
            TextField("field two", text: $textFieldTwo)
                .keyboardType(.decimalPad)
                .onChange(of: textFieldTwo){ newValue in
                    textFieldTwo = validateField(newValue) // <-- here
                }
        }
    }

    func validateField(_ newValue: String) -> String {  // <-- here
        let periodCount = newValue.components(separatedBy: ".").count - 1
        let commaCount = newValue.components(separatedBy: ",").count - 1
        
        if newValue.last == "." && periodCount > 1 || newValue.last == "," && commaCount > 1 {
            //it's a second period or comma, remove it
            return String(newValue.dropLast())  // <-- here
        } else {
            let filtered = newValue.filter { "0123456789.,".contains($0) }
            if filtered != newValue{
                return filtered // <-- here
            }
        }
        return newValue // <-- here
    }
}
0

Create a new View and put one TextField and the validation in it. FYI SwiftUI is designed to do validation in onSubmit instead of onChange to allow pasting. Then use two of these Views.

TextFieldNumbersOnly(text: $one)
TextFieldNumbersOnly(text: $two)

struct TextFieldNumbersOnly: View {
    @Binding private var text: String
    
    var body: some View {
        
        TextField("field", text: $text)
            .keyboardType(.decimalPad)
            .onSubmit {
                validate()
            }
    }
            
    func validate(){
…
malhal
  • 26,330
  • 7
  • 115
  • 133