SwiftUI validate input in textfields

I am trying to validate user input in a TextField by removing certain characters using a regular expression. Unfortunately, I am running into problems with the didSet method of the text var calling itself recursively.

import SwiftUI
import Combine

class TextValidator: ObservableObject {

    @Published var text = "" {
        didSet {
            print("didSet")
            text = text.replacingOccurrences(
                of: "\\W", with: "", options: .regularExpression
            ) // `\W` is an escape sequence that matches non-word characters.
        }
    }

}


struct ContentView: View {
    
    @ObservedObject var textValidator = TextValidator()
    
    var body: some View {
        TextField("Type Here", text: $textValidator.text)
            .padding(.horizontal, 20.0)
            .textFieldStyle(RoundedBorderTextFieldStyle())
            
    }
}

On the swift docs (see the AudioChannel struct), Apple provides an example in which a property is re-assigned within its own didSet method and explicitly notes that this does not cause the didSet method to be called again. I did some testing in a playground and confirmed this behavior. However, things seem to work differently when I use an ObservableObject and a Published variable.

How do I prevent the didSet method from calling itself recursively?

Also, setting the text back to oldValue within the didSet method upon encountering invalid characters would mean that if a user pastes text, then the entire text would be removed, as opposed to only the invalid characters being removed. So that option won't work.

Reason for recursion:

Updating the value in the didSet is not a problem when it is not coupled to TextField.

In your example, when the text in Textfield changes, it updates the textValidator.text. Inside didSet if you update textValidator.text, it will update the text in TextField

Textfield => textValidator.text => Textfield => textValidator.text ... recursively

In the AudioChannel example there is no direct coupling and so there is no problem.

Approach:

  • TextField needs a binding don't directly provide the binding to a variable.
  • You need a way to replace the unwanted text before updating the variable.
  • You can create a Binding by providing get and set closures.

Code:

import SwiftUI

class Model : ObservableObject {
    
    @Published var text : String = "" {
        didSet {
            print("text = \(text)")
        }
    }
}


struct ContentView: View {
    
    @ObservedObject var model : Model
    
    var body: some View {
        TextField("testing", text: Binding(get: {self.model.text},
                                           set: {self.model.text = $0.replacingOccurrences(of: "\\W", with: "", options:.regularExpression) }))
    }
}

struct ContentView_Previews: PreviewProvider {
    static var previews: some View {
        ContentView(model: Model())
    }
}

@somu: Thanks for that solution! Unfortunately there is a small flicker, it means that you can see the non-allowed chars for a short second. Do you also know a way to prevent this?

@Lupurus Take a look at this post that someone with a very similar name to mine posted