skip to Main Content

I have a text field for searching items in an array, and I am using a model, that has a published text property, bound to the field to call the search function every time the text changes.

My current issue is that every time anything happens with the text field, it sends a notification that is has changed, therefore the first time it appears it sends one, every time it is focused, defocused, or a letter was typed it sends two.

This shows how many times the field sends changes:

Animation

This is a simplified example of the code:

struct ContentView: View {
    @ObservedObject var model = Model()
    @FocusState var focused: Bool

    var body: some View {
        VStack {
            TextField("Field", text: $model.text)
                .submitLabel(.search)
                .focused($focused)

            Text("Field has sent changes (model.textChanged) times")
            Button(action: { focused = false }) {
                Text("Defocus")
            }
        }
        .padding()
    }
}


class Model: ObservableObject {
    @Published var text: String = ""
    @Published var textChanged: Int = 0

    private var cancellables = Set<AnyCancellable>()

    init() {
        $text
            .sink { text in
                self.textChanged += 1 // This is where i am doing my search call in the actual code
            }
            .store(in: &cancellables)
    }
}

I have tried using .onReceive on the TextField, and .onChange($text) on the view instead of .sink, but that didn’t change anything. I know that this is the default behaviour of TextField, but I would like to only execute the search, when the text has changed and not every time something happens with the field. Putting the search call in a didSet on the text property is also not an option.

2

Answers


  1. You could just attach .onChange to the TextField (or you can attach it to any part of the body in fact).

    You said you tried this before, but you used a binding. It shouldn’t be a binding:

    @State private var counter = 0
    
    var body: some View {
        VStack {
            TextField("Field", text: $model.text)
                .submitLabel(.search)
                .focused($focused)
                // pre iOS 17: .onChange(of: model.text) { newVal in
                .onChange(of: model.text) { oldVal, newVal in
                    counter += 1
                }
    
            Text("Field has sent changes (model.textChanged) times, counter=(counter)")
            Button(action: { focused = false }) {
                Text("Defocus")
            }
        }
        .padding()
    }
    
    Login or Signup to reply.
  2. If you want to keep your model structure, then with minimal change to your code I’d suggest to use removeDuplicates(). The updated code to your Model will be:

    import Combine
    
    class Model: ObservableObject {
        @Published var text: String = ""
        @Published var textChanged: Int = 0
    
        private var cancellables = Set<AnyCancellable>()
    
        init() {
            $text
                .removeDuplicates() // Just this line is added
                .sink { text in
                    print("Text: (text)")
                    self.textChanged += 1 // This is where i am doing my search call in the actual code
                }
                .store(in: &cancellables)
        }
    }
    
    

    Please note that the first change you get in this sink is because you are assigning an empty string to the text in your model.

    Login or Signup to reply.
Please signup or login to give your own answer.
Back To Top
Search