Search code examples
combineurlsession

How to add a function that prints out Output for URLSession.DataTaskPublisher but not calling sink?


I want to add a function you can call when using URLSession.DataTaskPublisher to fetch output from a URLRequest, that can print out the request and the data (whenever they are received.

URLSession.shared.dataTaskPublisher(for: URL(string: "https://jsonplaceholder.typicode.com/posts")!)
            .logEvents()  
            .map { data, _ in return data }
            .decode(type: [Post].self, decoder: JSONDecoder())
            .eraseToAnyPublisher()
            .sink { ///  .. handles the sinked values }
     

where the logEvents() function should print out the data, and the response object to console, or error if it fails. For URLSession.dataTask i've written it like this:

func dataTask(withLogging request: URLRequest, completionHandler: @escaping (Data?, URLResponse?, Error?) -> ()) -> URLSessionDataTask {
    return self.dataTask(with: request) { data, response, error in
        URLRequest.debugPrintYAML(request: request, response: response, received: data, error: error)
        completionHandler(data, response, error)
    }
}

this just replaces dataTask(with:, with dataTask(withLogging:, and makes it really easy to see good console logs of all requests that are called using it. The goal i'm doing is to write a similar function for the Publisher version, dataTaskPublisher

what i would like the .logEvents() to do is calling the same

URLRequest.debugPrintYAML(request: request, response: response, received: data, error: error)

What i've tried:

doing it in .map:

I have access to data and response there, but i have to change the return type of the function, and i don't really want to to that in a generic logging function. Also having it as a side effect in a map function isn't really optimal, that function should only map. This is what i've tried in the example given below

doing it in .sink:

Since i allready have mapped to responses by the time i get to the .sink, i have lost the information about response and data, which means i ofc cant write them out.

doing it in .handleEvents inside a function:

If i use .handleEvents, i don't really have a place to store the cancellable, and i'm allready creating a Cancellable when calling .sink later, so having two cancellables is not optimal.

extension Publisher where Output == (data: Data, response: URLResponse),
                        Self == URLSession.DataTaskPublisher
{
func logEvents() -> URLSession.DataTaskPublisher {
        let cancellable = self.handleEvents { values in
            print(values.description)
        }
        // the cancellable is not retained, and therefore this will never fire
        return self
    }
}

This extension is open source and located here


Solution

  • Try it this way:

    import UIKit
    import Combine
    
    fileprivate func logOneEvent(data: Data, response: URLResponse) {
        print("Got \(data.count) bytes")
        print("Response: \(response)")
    }
    
    extension Publisher where Output == URLSession.DataTaskPublisher.Output
    {
        func logEvents() -> Publishers.HandleEvents<Self> {
            return self.handleEvents(receiveOutput: logOneEvent)
        }
    }
    
    let jsonURL = URL(string: "https://jsonplaceholder.typicode.com/posts")!
    let subscription = URLSession.shared.dataTaskPublisher(for: jsonURL)
        .logEvents()
        .sink { completion in
            switch completion {
            case .finished:
                print("Done")
            case .failure(let error):
                print("Failed \(error)")
            }
    
        } receiveValue:  {
            debugPrint($0)
        }
    

    A publisher (in this case a Publishers.HandleEvents instance) is a thing you can return from a function like logEvents. You can just let that thing handle subscriptions normally, there's no need to try and inject and maintain your own subscription.