In my application I have two View Models that make some work and publish their result via the @Published
property. To simplify some code, I use another property alongside the published value to check if the work is complete.
EDIT: I misused the Result
object, forgetting it is present in the swift standard library. Here now I use the Dummy
object to represent the data. Furthermore, I've added some code in receiveValue
to justify it's presence
For example:
class FirstViewModel: ObservableObject {
var subscriptions: Set<AnyCancellable> = []
@Published private(set) var results: [Dummy]?
@Published private(set) var isComplete: Bool = false
func fetch() {
self.isLoading = true
self.isComplete = false
APIService.fetch()
.receive(on: DispatchQueue.main)
.sink(receiveCompletion: { [weak self] value in
guard let self = self else { return }
if case let .failure(error) = value {
print(error)
}
}, receiveValue: { (data: [Dummy]) in
// make some work
data.forEach { d in
print(d.title)
}
self.results = data
self.isComplete = true
})
.store(in: &self.subscriptions)
}
}
(the other one is almost identical)
EDIT: here's the APIService.fetch
implementation
struct Agent {
func fetch<T: Decodable>(_ request: URLRequest) -> AnyPublisher<T, Error> {
let decoder = JSONDecoder()
return URLSession.shared.dataTaskPublisher(for: request)
.tryMap { response in
if let httpURLResponse = response.response as? HTTPURLResponse, !(200 ... 299 ~= httpURLResponse.statusCode) {
throw Error.message("Got an HTTP \(httpURLResponse.statusCode) error.")
}
return response.data
}
.decode(type: T.self, decoder: decoder)
.mapError { Error.map($0) }
.eraseToAnyPublisher()
}
}
enum APIService {
static let agent = Agent()
static func fetch(languageCode: String) -> AnyPublisher<[Dummy], Error> {
let request = URLRequest(url: "https://example.com/dummy")
let res: AnyPublisher<[Dummy], Error> = agent.fetch(request)
return res.eraseToAnyPublisher()
}
}
Somewhere in my code I consume both of them like this
EDIT: forgot to add the code to call the various $isComplete
@EnvironmentObject var firstViewModel: FirstViewModel
@EnvironmentObject var secondViewModel: SecondViewModel
...
@State private var subscriptions: Set<AnyCancellable> = []
...
Button("Press me with no reason") {
// I FORGOT TO ADD THE CALLERS
self.firstViewModel.fetch()
self.secondViewModel.fetch()
Publishers.CombineLatest(
self.firstViewModel.$isComplete.first { $0 == true },
self.secondViewModel.$isComplete.first { $0 == true }
)
.receive(on: DispatchQueue.main)
.sink { firstComplete, secondComplete in
print("is first complete: \(firstComplete) - is second complete: \(secondComplete)")
}
.store(in: &self.subscriptions)
}
This code works flawlessly.
Where is the strange behaviour? Imagine this use case: the device is offline, i.e. because of airplane mode and you clicked the button. In the console you can see the error that says you're offline.
Then you toggle the airplane mode (or go online whatever), click again the button and the print
is called twice.
Seems like is cached instead of completed. I haven't tried yet, but I think that any error will trigger this problem (FWIW).
Any idea?
EDIT:
I forgot to cancel the Publishers.CombineLatest
after the failed execution. So instead of
@State private var subscriptions: Set<AnyCancellable> = []
...
.store(in: &self.subscriptions)
I now use
@State private var subscription: AnyCancellable?
...
self.subscription = Publishers.CombineLatest(...)
and cancel it when the error raises, resolving the issue