Search code examples
swiftswiftuicombine

Combine's sink receives @Published update twice


I am currently building a search field that should execute some action when its value changes. Here is my view:

struct SearchView: View {
@ObservedObject var vm = SearchViewModel()

var body: some View {
    VStack {
        TextField("Type something", text: $vm.text)
            .font(.title)
            .padding()
            .background(Color.white)
            .cornerRadius(10)
            .shadow(color: .gray, radius: 4, x: 0, y: 2)

        HStack {
            Spacer()

            Button("Paste") {
                vm.pasteText()
            }
            .buttonStyle(.borderedProminent)

            Button("Clear") {
                vm.clearText()
            }
            .buttonStyle(.borderless)
        }
        .padding(.top)

        Spacer()
    }
    .padding(.horizontal)
}
}

And here is the view model:

class SearchViewModel: ObservableObject {
@Published var text = ""
private var subscriptions = Set<AnyCancellable>()

init() {
    $text
        .sink(receiveValue: { value in
            print("receiveValue: \(value)")
        })
        .store(in: &subscriptions)
}

func pasteText() {
    self.text = "Text"
}

func clearText() {
    self.text = ""
}
}

When I run the app and type something, for example, a letter "A", I get the following output:

receiveValue: 
receiveValue: 
receiveValue: 
receiveValue: A
receiveValue: A

The first empty value is received once the view is created. Then, two empty values are received once I focused on the field. Finally, once I typed letter "A", I got two print statements again.

So, I have a few questions:

  1. Is it possible to ignore the first empty string when the view just appeared and nothing is typed yet?
  2. Why do I get empty values when I focus on the field?
  3. Why do I receive duplicated values once I type something?

It's also interesting, when I press the paste/clear buttons I only get a single output per action.


Solution

  • You could use removeDuplicates to avoid receiving identical values a few times in a row and dropFirst to ignore the first empty String but not the subsequent ones.

    $text
        .removeDuplicates()
        .dropFirst()
        .sink { value in
            print("receiveValue: \(value)")
        }
        .store(in: &subscriptions)