alessionossa
alessionossa

Reputation: 991

Combine @Published property send values also when not updated

I was trying to create a dynamic Form using SwiftUI and Combine, that loads options of an input (in the example, number) based on another input (in the example, myString).

The problem is that the Combine stack get executed continuously, making lots of network requests (in the example, simulated by the delay), even if the value is never changed.

I think that the expected behavior is that $myString publishes values only when it changes.

class MyModel: ObservableObject {

    // My first choice on the form
    @Published var myString: String = "Jhon"

    // My choice that depends on myString
    @Published var number: Int?

    var updatedImagesPublisher: AnyPublisher<Int, Never> {
        return $myString
            .removeDuplicates()
            .print()
            .flatMap { newImageType in
                return Future<Int, Never> { promise in

                    print("Executing...")

                    // Simulate network request
                    DispatchQueue.main.asyncAfter(deadline: .now() + 2.0) {
                        let newNumber = Int.random(in: 1...200)
                        return promise(.success(newNumber))
                    }
                }
        }
        .receive(on: DispatchQueue.main)
        .eraseToAnyPublisher()
    }
}

struct ContentView: View {

    @ObservedObject var model: MyModel = MyModel()

    var body: some View {
        Text("\(model.number ?? -100)")
            .onReceive(model.updatedImagesPublisher) { newNumber in
                self.model.number = newNumber
            }
    }
}

Upvotes: 0

Views: 1895

Answers (2)

Alexander Gaidukov
Alexander Gaidukov

Reputation: 759

The problem is the updatedImagesPublisher is a computed property. It means that you create a new instance every time you access it. What happens in your code. The Text object subscribes to updatedImagesPublisher, when it receives a new value, it updates the number property of the Model. number is @Published property, it means that objectWillChange method will be called every time you change it and the body will be recreated. New Text will subscribe to new updatedImagesPublisher (because it is computed property) and receive the value again. To avoid such behaviour just use lazy property instead of computed property.

lazy var updatedImagesPublisher: AnyPublisher<Int, Never> = {
    return $myString
        .removeDuplicates()
        .print()
        .flatMap { newImageType in
            return Future<Int, Never> { promise in

                print("Executing...")

                // Simulate network request
                DispatchQueue.main.asyncAfter(deadline: .now() + 2.0) {
                    let newNumber = Int.random(in: 1...200)
                    return promise(.success(newNumber))
                }
            }
    }
    .receive(on: DispatchQueue.main)
    .eraseToAnyPublisher()
}()

Upvotes: 3

Asperi
Asperi

Reputation: 258413

I assume it is because you create new publisher for every view update, try the following instead. (Tested with Xcode 11.4)

class MyModel: ObservableObject {

    // My first choice on the form
    @Published var myString: String = "Jhon"

    // My choice that depends on myString
    @Published var number: Int?

    lazy var updatedImagesPublisher: AnyPublisher<Int, Never> = {
        return $myString
            .removeDuplicates()
            .print()
            .flatMap { newImageType in
                return Future<Int, Never> { promise in

                    print("Executing...")

                    // Simulate network request
                    DispatchQueue.main.asyncAfter(deadline: .now() + 2.0) {
                        let newNumber = Int.random(in: 1...200)
                        return promise(.success(newNumber))
                    }
                }
        }
        .receive(on: DispatchQueue.main)
        .eraseToAnyPublisher()
    }()
}

Upvotes: 1

Related Questions