I was wondering is it the expected behaviour for a subscriber to have its sink triggered twice when in this scenario?
func testExample() throws {
let service = MockAPIService(isSuccessful: true)
let sut = ContentViewModel(apiService: service)
let fetchImagesExpectation = expectation(description: "Fetching images")
sut.$images
.sink { value in
print("## count: ", value?.count)
// fetchImagesExpectation.fulfill()
// XCTAssertEqual(value?.count ?? 0, 5)
}
.store(in: &cancellables)
sut.fetch()
wait(for: [fetchImagesExpectation], timeout: 1)
}
Console output:
## count: nil
## count: Optional(5)
Here is ContentViewModel:
final class ContentViewModel: ObservableObject {
@Published private (set) var images: [ImageModel]?
private let apiService: NetworkingService
private var cancellable: Set<AnyCancellable>
init(apiService: NetworkingService = APIService()) {
self.cancellable = Set<AnyCancellable>()
self.apiService = apiService
}
func fetch() {
apiService.fetchImages()
.receive(on: RunLoop.main)
.sink(receiveCompletion: { result in
if case let .failure(error) = result {
Logger.logError(error)
}
}, receiveValue: { [weak self] response in
self?.handleRespose(response)
})
.store(in: &cancellable)
}
private func handleRespose(_ response: SampleImagesResponse) {
guard let responseImages = response.sample else {
return
}
self.images = responseImages.compactMap { item in
if let urlString = item.imageURL,
let url = URL(string: urlString),
let imageID = item.id,
let title = item.description {
return ImageModel(imageID: imageID, title: title, url: url)
}
return nil
}
}
}
When sut.fetch()
is commented console is still printing ## count: nil
.
it the expected behaviour for a subscriber to have its sink triggered twice when in this scenario?
Yes. The nil
value arises from merely connecting the sink
to the $images
in the first place, as you can readily discover by means of this much simpler scenario (no test, no ObservableObject, no fetch
, no ImageModel, no nothing):
import UIKit
import Combine
final class ContentViewModel {
@Published private (set) var images: [Int]?
}
class ViewController: UIViewController {
var cancellables = Set<AnyCancellable>()
override func viewDidLoad() {
super.viewDidLoad()
ContentViewModel().$images.sink { value in print(value) }.store(in: &cancellables)
}
}
That prints nil
. This behavior is simply a feature of @Published
, as I explain in my online book:
the values emitted will be the initial value at subscription time followed by new values when the property changes [emphasis mine]
If you don't want to receive that value, insert a dropFirst()
operator before the sink
.