0

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

Valerio
  • 3,297
  • 3
  • 27
  • 44
  • This might be irrelevant but your use of `isComplete` is wrong. What makes a pipeline complete is that it gets a completion, not a value. – matt Oct 07 '21 at 14:45
  • @matt I see your point. You suggest to move it to receiveCompletion? – Valerio Oct 07 '21 at 14:57
  • Indeed yes. Might be irrelevant to the actual question but at least it will be telling the truth. :) – matt Oct 07 '21 at 14:58
  • Of course it would be even better if you eliminated the Published middlemen and just vended the open pipeline so that others can subscribe to it directly. The way to know a publisher is complete is not that some bool flag says so but that I receive the completion myself. :)) – matt Oct 07 '21 at 15:00
  • Well, as you may guessed, my code does more than what you see here. Code has been simplified for sake of clarity, but in the real app does more into recivevalue, and results is consumed in different places. Here's why this design. – Valerio Oct 08 '21 at 07:51
  • Your use of `isComplete` here is a bit confusing because it's hard to tell if it's referring to the API operation being completed, or the publisher's stream being completed (i.e in the sense of `receiveCompletion`). – Scott Thompson Oct 08 '21 at 14:52
  • We're also missing some detail about how `APIService.fetch()` behaves. From the code it looks like it's a publisher of type `AnyPublisher<[Result],Error>` but since `Result` is a type defined by the framework and requires type parameters (e.g. `Result`) it's not totally clear what you're getting at here. – Scott Thompson Oct 08 '21 at 15:09
  • All I'm saying is that real or at least realistic code would be helpful. Try to help us to help you. – matt Oct 08 '21 at 15:34
  • And, when is `fetch` called on the ViewModel(s)? – Scott Thompson Oct 08 '21 at 17:44
  • Im out of office for the whole weekend, Ill catch you up on monday – Valerio Oct 09 '21 at 20:06
  • I've had an epiphany during the weekend, and turns out I forgot to cancel the Publishers. I've edited the question – Valerio Oct 11 '21 at 07:31
  • I've also edited the question to include all the code to clarify some aspects, as you requested – Valerio Oct 11 '21 at 07:52

0 Answers0