how to invoke a URLSession publisher when a timer publisher fires

Viewed 134

I want to invoke a URLSession publisher based on a timer publisher every 60 minutes.

It seems like this should be possible but not aware how to do it:

For instance;


struct Response: Codable {
    // ... some fields here
}


class MyClass: ObservableObject {
  @Published var response: Response? = nil
  var cancelable : AnyCancelable? 
  func connectPoller() {
    let timer = Timer.TimerPublisher(interval: 60 * 60, runLoop: .main, mode: .default)
    // how to chain this?
    // want to connect the timer to invoke the data task
    URLSession.shared.dataTaskPublisher(for: URL(string: "some-url-here")!).tryMap() { element -> Data in
            guard let httpResponse = element.response as? HTTPURLResponse,
                httpResponse.statusCode == 200 else {
                    throw URLError(.badServerResponse)
                }
            return element.data
        }.decode(type: Response?.self, decoder: JSONDecoder()).eraseToAnyPublisher()
        .replaceError(with: nil).assign(to: \.response, on: self)
    }
}
2 Answers
class MyClass: ObservableObject {
    @Published var response: Response? = nil
    
    var cancelable : AnyCancellable?
    
    func connectPoller() {
        let timer = Timer.TimerPublisher(interval: 60 * 60, runLoop: .main, mode: .default)
            .autoconnect()  //<= here
        
        cancelable = timer
            .flatMap { _ in
                URLSession.shared.dataTaskPublisher(for: URL(string: "some-url-here")!).tryMap() { element -> Data in
                    guard let httpResponse = element.response as? HTTPURLResponse,
                          httpResponse.statusCode == 200 else {
                        throw URLError(.badServerResponse)
                    }
                    return element.data
                }
                .decode(type: Response?.self, decoder: JSONDecoder()).eraseToAnyPublisher()
                .replaceError(with: nil)
            }
            .receive(on: RunLoop.main) //<= here
            .assign(to: \.response, on: self)
    }
}

In order to transform the Timer publisher into the URLSession dataTask publisher, you'll want to use flatMap. In addition, you'll want to make sure that the publisher chain can't complete, because the timer will stop firing at that point. So, you'll need to do a little more aggressive cleansing of the errors inside the flatMap so that they don't propagate further down the chain.

class MyClass: ObservableObject {
  @Published var response: Response? = nil
  var cancellable : AnyCancellable?
  func connectPoller() {
    cancellable = Timer.publish(every: 5, on: .main, in: .default)
        .autoconnect()
        .flatMap { _ in
            URLSession.shared.dataTaskPublisher(for: URL(string: "https://google.com")!)
                .flatMap { element -> AnyPublisher<Data,Never> in
                    guard let httpResponse = element.response as? HTTPURLResponse,
                                    httpResponse.statusCode == 200 else {
                        return Just(Data()).eraseToAnyPublisher()
                                    }
                    return Just(element.data).eraseToAnyPublisher()
                }
                .decode(type: Response?.self, decoder: JSONDecoder())
                .replaceError(with: nil)
        }
        .print()
        .receive(on: RunLoop.main)
        .assign(to: \.response, on: self)
    }
}
Related