Why recieveValue block is called twice?

75 Views Asked by At

I have this code:

import SwiftUI
import Combine


final class ViewModel: ObservableObject {
    @Published var text = ""
    var cancellables = Set<AnyCancellable>()

    init() {
        $text
            .sink { completion in

            } receiveValue: { text in
                print("Text \(text)")
            }.store(in: &cancellables)

    }
}

struct ContentView: View {

    @StateObject private var vm = ViewModel()
    var body: some View {
        TextField("Title", text: $vm.text)
    }
}

#Preview {
    ContentView()
}

What I have noticed, if I type one letter, i receive two values. So at first it will just say Text. And if I type one letter, say letter A, output will look like this:

Text A

Text A

In some cases I saw it prints same value even more than two times.

I know I can use removeDuplicates but why is this actually happening?

1

There are 1 best solutions below

3
malhal On

Lots of mistakes:

  1. In SwiftUI the View struct hierarchy is the view model already don't try to use classes or you'll run into major issues.
  2. In a Combine ObservableObject you shouldn't use sink or cancellables, instead use .assign to complete the pipeline to an @Published. That way the lifetime is correct, i.e. the pipeline will be cancelled and tore down when the object deinits, which is what @StateObject does. This can be used for a fetcher type object but not for view data.
  3. @StateObject is for when you need a reference type in a @State, e.g. for a delegate or an async Combine pipeline, don't try to use them to make view models because that is the job of the View struct. If you upgrade to async/await and the .task modifier you don't need @StateObject anymore.

Your code fixed looks like this:

struct Content {
    var text = ""
    /// other related vars

   // other testable funcs
   mutating func reset() {
       text = ""
   }
}

struct ContentView: View {

    @State private var content = Content()

    var body: some View {
        TextField("Title", text: $content.text)
    }
}

#Preview {
    ContentView()
}