We started using Combine for Networks calls in our Application. The user is supposed to input a code, and we want to validate that code via a network call, then perform actions based on the result of said network call.
To our understanding, if we had a published string variable and used that to lazily create another publisher in which we made the API call, we'd only make API calls every time the string changed.
However, the more SwiftUI views that listen to the publisher via onReceive
, the more API calls get made. Here's a simplified example:
View
struct ContentView: View {
@ObservedObject var viewModel: ResultBenchmarkViewModel
@State var one: String = "ONE"
@State var two: String = "TWO"
@State var three: String = "THREE"
var body: some View {
VStack {
TextField("INPUT", text: $viewModel.inputString)
Text(one)
.onReceive(viewModel.inputStringPublisher, perform: { string in
one = string
})
Text(two)
.onReceive(viewModel.inputStringPublisher, perform: { string in
two = string
})
Text(three)
.onReceive(viewModel.inputStringPublisher, perform: { string in
three = string
})
}
}
}
ViewModel
class ResultBenchmarkViewModel: Identifiable, ObservableObject {
@Published var inputString: String = ""
lazy var inputStringPublisher: AnyPublisher<String, Never> = {
return $inputString
.map { string in
print("API CALL")
return string
}
.eraseToAnyPublisher()
}()
}
Using onReceive
three times also prints "API CALL" three times every time the user inputs a character. What we expected to happen was that "API CALL" would only be printed once, since inputString
only changes once, and then the three views that use onReceive
would only receive the final result published by the publisher.
A solution we're now implementing is listening to the inputString, making the api call and saving the result to a local published variable, which the views then update according to. However, that seems like boilerplate to me that could be obsolete if we properly implemented the original request.
Could we achieve our expected result with some minor changes to the implementation? Or is there a fundamental misunderstanding of combine/swiftui here?
You can 'share' the output of a publisher. This is what the Share Publisher is for.
https://developer.apple.com/documentation/combine/fail/share()
With the Share Publisher, your inputStringPublisher
would look like this:
lazy var inputStringPublisher: AnyPublisher<String, Never> = {
return $inputString
.map { string in
print("API CALL")
return string
}
.share()
.eraseToAnyPublisher()
}()
Personally, I would continue to use the published variable solution, as there is more logic in the view model.