Reputation: 991
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
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
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