Urlsession.Shared.Datatask VS Datataskpublisher, When to Use Which

URLSession.shared.dataTask vs dataTaskPublisher, when to use which?

The first one is the classic. It has been present for quite some time now and most if not all developers are familiar with it.

The second is a wrapper around the first one and allows combining it with other publishers (e.g. Perform some request only when first two requests were performed). Combination of data tasks using the first approach would be far more difficult.

So in a gist: use first one for one-shot requests. Use second one when more logic is needed to combine/pass results with/to other publishers (not only from URLSession). This is, basically, the idea behind Combine framework - you can combine different ways of async mechanisms (datatasks utilising callbacks being one of them).

More info can be found in last year's WWDC video on introducing combine.

Perform multiple URLSessionDataTask in a single publisher chain

This is what flatMap is for. flatMap acts on each value, but instead of mapping it to another value, it maps it to a publisher, which in your case would be a second url request.

The general idea is:

let jwtRequest = URLRequest(...)

let fetchDataPublisher = URLSession.shared
.dataTaskPublisher(for: jwtRequest)
.map(\.data)
.decode(type: Token.self, decoder: JSONDecoder()) // get the token
.flatMap { token -> AnyPublisher<Data, Error> in
let apiRequest = URLRequest(...) // with token

return URLSession.shared
.dataTaskPublisher(for: apiRequest)
.map(\.data)
.mapError { $0 as Error }
.eraseToAnyPublisher()
}
.decode(type: Something.self, decoder: JSONDecoder())

This is a simplified example, since I didn't handle errors and took liberties with how you extract the token.

dataTask in networking is not executing

A standard dataTask of URLSession must be resumed

URLSession
.shared
.dataTask(with: url) { data, response, error in

...

}
.resume()

An alternative is Combine's .dataTaskPublisher(for

Swift Combine Return Int From URLSession.shared.dataTaskPublisher

My suggestion is to decode the data and create the Core Data records within the pipeline and return a publisher. In another function subscribe to the publisher and sink the number of items and/or handle the error.

I don't have your custom items, you have to manage self properly

func importUsers(url: URL) -> AnyPublisher<Int,Error> {
URLSession.shared.dataTaskPublisher(for: url)
.map(\.data)
.tryMap{data -> Int in
var users: [GitUser] = []
var cdError : Error?
self.importContext.performAndWait {
do {
let users = try self.decoder.decode([GitUser].self, from: data)
try self.importContext.save()
} catch {
cdError = error
}
}
if let error = cdError { throw error }
return users.count
}
.eraseToAnyPublisher()
}

However you could also use async/await

func importUsers(url: URL) async throws -> Int {
let (data, _) = try await URLSession.shared.data(from: url)
let users = try await self.importContext.perform {
try self.decoder.decode([GitUser].self, from: data)
try self.importContext.save()
}
return users.count
}

Or an iOS 13 compatible async version, here perform can be asynchronous

func importUsers(url: URL) async throws -> Int {
try await withCheckedThrowingContinuation { continuation in
let task = URLSession.shared.dataTask(with: url) { [unowned self] (data, _ , error) in
if let error = error { continuation.resume(with: .failure(error)); return }
self.importContext.perform {
do {
let users = try self.decoder.decode([GitUser].self, from: data!)
try self.importContext.save()
continuation.resume(with: .success(users.count))
} catch {
continuation.resume(with: .failure(error))
}
}
}
task.resume()
}
}

Why doesn't URLSession.DataTaskPublisher ever publish values?

I think that there are two problems with your code, firstly you only have a publisher (handleEvent returns a publisher) and secondly that publisher goes out of scope and disappears. This works although it isn't exactly elegant.


import Combine
import SwiftUI

var pub: AnyPublisher<(data: Data, response: URLResponse), URLError>? = nil
var sub: Cancellable? = nil

var data: Data? = nil
var response: URLResponse? = nil

func combineTest() {
guard let url = URL(string: "https://apple.com") else {
return
}
pub = URLSession.shared.dataTaskPublisher(for: url)
.print("Test")
.eraseToAnyPublisher()
sub = pub?.sink(
receiveCompletion: { completion in
switch completion {
case .finished:
break
case .failure(let error):
fatalError(error.localizedDescription)
}
},
receiveValue: { data = $0.data; response = $0.response }
)
}

struct ContentView: View {
var body: some View {
Button(
action: { combineTest() },
label: { Text("Do It").font(.largeTitle) }
)
}
}

I did it in SwiftUI so that I would have less to worry about and I used 3 variables so that I could follow better. You need to use the 2 parameter sink as the publisher's error isn't Never. Finally the print() is just for test and works really well.

Swift Combine in UIKit. URLSession dataTaskPublisher NSURLErrorDomain -1 for some users

I don't know for sure but I see a couple of issues in the code you presented... I commented below.

A 499 implies that your Cancellable is getting deleted before the network request completes. Maybe that will help you track it down.

Also, you don't need the subscribe(on:) and it likely doesn't do what you think it does anyway. It could be causing the problem but there's no way to know for sure.

Using subscribe(on:) there is like doing this:

DispatchQueue.background.async {
URLSession.shared.dataTask(with: request) { data, response, error in
<#code#>
}
}

If you understand about how URLSession works, you will see that dispatch is completely unnecessary and doesn't affect what thread the data task will emit on.

func fetch<R>(_ type: R.Type, at endpoint: Endpoint, page: Int, force: Bool) -> AnyPublisher<R, TheError> where R : Decodable {
guard let request = request(for: endpoint, page: page, force: force) else {
return Fail(error: TheError.Network.cantEncodeParameters).eraseToAnyPublisher() // your failure here is way more complex than it needs to be. A simple Fail will do what you need here.
}

let decoder = JSONDecoder()
decoder.keyDecodingStrategy = .convertFromSnakeCase

return URLSession.shared
.dataTaskPublisher(for: request)
// you don't need the `subscribe(on:)` here.
.tryMap { element in
guard
let httpResponse = element.response as? HTTPURLResponse,
httpResponse.statusCode == 200 else
{ throw URLError(.badServerResponse) }

return element.data
}
.decode(type: type, decoder: decoder)
.mapError { error in
// We map error to present in UI
switch error {
case is Swift.DecodingError:
return TheError.Network.cantDecodeResponse

default:
return TheError(title: nil, description: error.localizedDescription, status: -2)
}
}
.eraseToAnyPublisher()
}

Combine's DataTaskPublisher does not output when passed through flatMap in Playgrounds

Swift playgrounds finish execution when all synchronous code in them returned. However, you are executing a network request asynchronously, so you need to tell the playground to wait for the async result.

Call this before starting the network request:

import PlaygroundSupport

PlaygroundPage.current.needsIndefiniteExecution = true

And in the sink, you can finish execution by calling

PlaygroundPage.current.finishExecution()


Related Topics



Leave a reply



Submit