refresh token using combine networking ios - ios

this question is straight forward :
My code :
return urlSession.dataTaskPublisher(for: urlRequest)
.tryMap { (data: Data, response: URLResponse) -> Data in
//TODO: hide loader
GRP.hideLoader()
if let httpURLResponse = response as? HTTPURLResponse {
if !(200...299 ~= httpURLResponse.statusCode) {
var error = NetworkingError(errorCode: httpURLResponse.statusCode)
if let json = try? JSONSerialization.jsonObject(with: data, options: []) {
error.jsonPayload = json
}
throw error
}
}
if withErrorMessage, let errorCheckModel = try? JSONDecoder().decode(ErrorModel.self, from: data)
{
if let statusIsSuccess = errorCheckModel.success, let errorMessage = errorCheckModel.message, !errorMessage.isEmpty
{
if(!statusIsSuccess)
{
print(urlString)
GRP.showToast(failure: true, message: errorMessage)
}
}
}
return data
}.mapError { error -> NetworkingError in
return NetworkingError(error: error)
}
.decode(type: T.self, decoder: decoder)
.receive(on: RunLoop.main)
.eraseToAnyPublisher()
}
i made this task buikder but i am stuck, i want to know how can i implement refresh token i. Thank you.

The question is kind of confusing as written. Do you mean I have a request that returns an AnyPublisher<SomeDecodable, NetworkError> and if it fails for a specific reason then I want to make another call (to refresh) and then retry the request? If so it looks something like this:
let authenticatedRequest = URLSession.shared.dataTaskPublisher(for: urlRequest)
return authenticatedRequest
.map { (data, response) -> AnyPublisher<(Data, URLResponse), Error> in
isUnanthenticated(response)
? refetchToken.map { _ in
authenticatedRequest
}
.switchToLatest()
.eraseToAnyPublisher()
: Just((data, response)).eraseToAnyPublisher()
}
.switchToLatest()
.decode(T.self, from: decoder)
.mapError { error -> NetworkingError in
return NetworkingError(error: error)
}
.eraseToAnyPublisher()
}
We make the authenticated request
We map the request and if it failed then we make a reauthrequest and retry. Otherwise we just return out input.
Either way we now have a Publisher of Publishers and we don't want that so we call switch to latest to flatten it and we continue.

Related

Combine: How to stream an array of parsed objects via a publisher?

// get handle of native data task publisher
let publisher = URLSession.shared.dataTaskPublisher(for: URL)
.handleEvents(
receiveSubscription: { _ in
activityIndicatorPublisher.send(true)
}, receiveCompletion: { _ in
activityIndicatorPublisher.send(false)
}, receiveCancel: {
activityIndicatorPublisher.send(false)
})
.tryMap { data, response -> Data in
guard let httpResponse = response as? HTTPURLResponse,
httpResponse.statusCode == 200 else {
throw NetworkError.httpError
}
return data
}
.decode(type: Repository.self, decoder: JSONDecoder())
.map { $0 }
.catch { err in
return Just([])
}
.eraseToAnyPublisher()
return publisher
I am new to Combine and I canĀ“t figure out what should I put inside the .map{} closure in order to return an array of Repository objects. The error I get at compile time is: Cannot convert value of type 'Repository' to closure result type '[Any]'
P.S. return type here should be:
-> AnyPublisher<[Repository], Never>
Can anyone share a light here? Many thanks in advance.
It was a silly mistake on my side, as Joakim Danielson pointed out, it was enought to change return type to an array of Repositories and publisher was working fine, final version of it is the following:
static func fetchRepositories(urlString: String) -> AnyPublisher<[Repository], Never> {
// prepare URL
guard let URL = URL(string: urlString) else {
return Just([]).eraseToAnyPublisher()
}
// get handle of native data task publisher
let publisher = URLSession.shared.dataTaskPublisher(for: URL)
.handleEvents(
receiveSubscription: { _ in
activityIndicatorPublisher.send(true)
}, receiveCompletion: { _ in
activityIndicatorPublisher.send(false)
}, receiveCancel: {
activityIndicatorPublisher.send(false)
})
.tryMap { data, response -> Data in
guard let httpResponse = response as? HTTPURLResponse,
httpResponse.statusCode == 200 else {
throw NetworkError.httpError
}
return data
}
.decode(type: [Repository].self, decoder: JSONDecoder())
.map { $0 }
.catch { err in
return Just([])
}
.eraseToAnyPublisher()
return publisher
}

POST request using Swift Combine without return doesn't work with AnyPublisher<Void, Error>

I have a POST request to send a text message to the user that doesn't return anything, but the status code. I try to return AnyPublisher<Void, CustomError> but it won't work.
enter image description here
This is what my generic request method looks like:
func request<T>(_ req: NetworkRequest) -> AnyPublisher<T, NetworkError> where T: Decodable, T: Encodable {
let sessionConfig = URLSessionConfiguration.default
sessionConfig.timeoutIntervalForRequest = TimeInterval(req.requestTimeout ?? requestTimeout)
guard let url = URL(string: req.url) else {
// Return a fail publisher if the url is invalid
return AnyPublisher(
Fail<T, NetworkError>(error: NetworkError.badURL("Invalid Url"))
)
}
// We use the dataTaskPublisher from the URLSession which gives us a publisher to play around with.
return URLSession.shared
.dataTaskPublisher(for: req.buildURLRequest(with: url))
.tryMap { output in
// throw an error if response is nil
guard output.response is HTTPURLResponse else {
throw NetworkError.serverError(code: 0, error: "Server error")
}
return output.data
}
.decode(type: T.self, decoder: JSONDecoder())
.mapError { error in
// return error if json decoding fails
NetworkError.invalidJSON(String(describing: error))
}
.eraseToAnyPublisher()
}
Looks like the primary issue is that you're trying to turn AnyPublisher<T,NetworkError> into AnyPublisher<Void,NetworkError>.
You could do:
service.request(request).map { _ in () }.eraseToAnyPublisher()

How can I decode custom Error with Combine?

The API I'm making calls to can return JSON containing error message.
How can I tell Combine to try and decode this custom error if I'm expecting another Decodable object to be returned on successful request?
My code currently looks like this:
private var cancellable: AnyCancellable?
internal func perform<T>(request: URLRequest, completion: #escaping (Result<T, Error>) -> Void) where T: Decodable {
cancellable = session.dataTaskPublisher(for: request)
.tryMap { output in
guard let response = output.response as? HTTPURLResponse, response.statusCode == 200 else {
throw HTTPError.statusCode
}
return output.data
}
.decode(type: T.self, decoder: JSONDecoder())
.eraseToAnyPublisher()
.sink(receiveCompletion: { _completion in
guard case .failure(let error) = _completion else {
return
}
completion(.failure(error))
}, receiveValue: { value in
completion(.success(value))
})
}
With URLSession I would do something like this:
URLSession.shared.dataTask(with: request) { data, response, error in
// Check for any connection errors
if let error = error {
completion(.failure(error))
return
}
// Read data
guard let data = data, !data.isEmpty else {
completion(.failure(SPTError.noDataReceivedError))
return
}
// Check response's status code, if it's anything other than 200 (OK), try to decode SPTError from the data.
guard let httpResponse = response as? HTTPURLResponse,
httpResponse.statusCode == 200 else {
let sptError = (try? JSONDecoder().decode(SPTError.self, from: data)) ?? SPTError.badRequest
completion(.failure(sptError))
return
}
// Decode requested objects
do {
let object = try JSONDecoder().decode(T.self, from: data)
completion(.success(object))
} catch {
print(completion(.failure(error)))
}
}.resume()
SPTError is just a struct that contains code and message, it conforms to Codable
When you have conditional branching, you can use the .flatMap to determine which publisher to return based on whatever conditions you check.
FlatMap has to match the failure type of the upstream and the returned publisher, so need to .mapError first to a generic Error. And because different branch is a different publisher chain, type erase them all to AnyPublisher:
URLSession.shared.dataTaskPublisher(for: url)
.mapError { $0 as Error }
.flatMap() { output -> AnyPublisher<T, Error> in
if output.data.isEmpty {
return Fail(error: SPTError.noDataReceivedError).eraseToAnyPublisher()
}
guard let httpResponse = output.response as? HTTPURLResponse else {
return Fail(error: HTTPError.statusCode).eraseToAnyPublisher()
}
if httpResponse.statusCode == 200 {
return Just(output.data)
.decode(type: T.self, decoder: JSONDecoder())
.eraseToAnyPublisher()
} else {
return Just(output.data)
.decode(type: SPTError.self, decoder: JSONDecoder())
.flatMap { Fail(error: $0) }
.eraseToAnyPublisher()
}
}
.eraseToAnyPublisher()

Handling errors in Combine (Swift, iOS)

I don't know how to deal with errors in a Combine flow. I would like to be able to catch errors from a Combine function.
Could anyone help in explaining what I'm doing wrong here and how I should handle catching an error with Combine?
Note: The function below is just an example to illustrate a case where an error could be caught instead of crashing the app.
func dataFromURL<T: Decodable>(_ url: String, _ decoder: JSONDecoder = JSONDecoder()) -> AnyPublisher<T, Error> {
// 1) Example: If the URL is not well-formatted, I would like to /create/raise/return an error (in a Combine way)
// 2) Instead of the forced unwrapping here, I would also prefer to raise a catchable error if the creation of the request fails
let request = URLRequest(url: URL(string:url)!)
// 3) Any kind of example dealing with potential errors, etc
return urlSession
.dataTaskPublisher(for: request)
.tryMap { result -> T in
return try decoder.decode(T.self, from: result.data)
}
.receive(on: DispatchQueue.main)
.eraseToAnyPublisher()
}
// function in another file:
func result() {
// I would like to be able to catch or handle errors in this function
dataFromURL("test").print()
// Example : if error 1), else if error 2) etc
}
As explained in the comments, I would like to be able to catch any error outside the dataFromURL function, but in a "Combine way".
I used a URL data fetching as an example, but it could be with anything else.
What is the recommended way to raise and catch errors with the Combine flow? Is it to return a Publisher with a specific error for example? If so, how can I do it?
EDIT
Without Combine, I would just have thrown an error, added the throws keyword to the function, and would have caught the error in the result function.
But I would have expected Combine to have a simpler or more elegant way to achieve this. For example, maybe something that can be thrown at any time:
guard <url is valid> else {
return PublisherError(URLError.urlNotValid)
}
And could have been caught like this:
dataFromURL
.print()
.onError { error in
// handle error here
}
.sink { result in
// no error
}
If the URL(string:) initializer fails (returning nil), you have to decide what error you want to turn that into. Let's say you want to turn it into a URLError. So, if URL(string:) returns nil, create the URLError and use a Fail publisher to publish it:
func jsonContents<T: Decodable>(
ofUrl urlString: String,
as type: T.Type,
decodedBy decoder: JSONDecoder = JSONDecoder()
) -> AnyPublisher<T, Error> {
guard let url = URL(string: urlString) else {
let error = URLError(.badURL, userInfo: [NSURLErrorKey: urlString])
return Fail(error: error).eraseToAnyPublisher()
}
return URLSession.shared
.dataTaskPublisher(for: url)
.tryMap { result -> T in
return try decoder.decode(T.self, from: result.data)
}
.receive(on: DispatchQueue.main)
.eraseToAnyPublisher()
}
But if you really want to shovel more Combine into it, you can use a Result.Publisher instead of Fail:
func jsonContents<T: Decodable>(
ofUrl urlString: String,
as type: T.Type,
decodedBy decoder: JSONDecoder = JSONDecoder()
) -> AnyPublisher<T, Error> {
return (
URL(string: urlString)
.map { Result.success($0) } // This is Optional.map
?? Result.failure(URLError(.badURL, userInfo: [NSURLErrorKey: urlString]))
)
.publisher
.flatMap({
URLSession.shared
.dataTaskPublisher(for: $0)
.tryMap { result -> T in
return try decoder.decode(T.self, from: result.data)
}
})
.receive(on: DispatchQueue.main)
.eraseToAnyPublisher()
}
But things get hard to read. We could factor out the use of Result into a new operator, unwrapOrFail(with:):
extension Publisher {
func unwrapOrFail<Wrapped>(with error: Failure) -> Publishers.FlatMap<Result<Wrapped, Self.Failure>.Publisher, Self> where Output == Wrapped? {
return self
.flatMap ({
$0
.map { Result.success($0).publisher }
?? Result.failure(error).publisher
})
}
}
And then use it like this:
func jsonContents<T: Decodable>(
ofUrl urlString: String,
as type: T.Type,
decodedBy decoder: JSONDecoder = JSONDecoder()
) -> AnyPublisher<T, Error> {
return Result.success(urlString).publisher
.map { URL(string: $0) }
.unwrapOrFail(with: URLError(.badURL, userInfo: [NSURLErrorKey: urlString]))
.flatMap({
URLSession.shared
.dataTaskPublisher(for: $0)
.tryMap { result -> T in
return try decoder.decode(T.self, from: result.data)
}
})
.receive(on: DispatchQueue.main)
.eraseToAnyPublisher()
}
Note, though, that if you make any mistake along the way, you'll probably get an inscrutable error message and have to pick apart your long pipeline to get Swift to tell you what's really wrong.

Input and output type mismatch in Combine data fetching

I am new to reactive programming and Combine, I have the following method which fetching the weather Data from the API.
First, I am checking that if I get 200 if not then throw an error.
If I get the proper data I am decoding it via JSONDecoder but just to
check if there is some problem JSON decoding I am returning the default Object.
Finally mapping the error which is thrown in the
first step but I am getting the following error in flatMAp function
instance method
flatMap(maxPublishers:_:)' requires the types 'Publishers.TryMap.Failure' (aka 'Error') and 'Just.Failure' (aka 'Never') be equivalent
private func fetchDataFor(urlStr: String) -> AnyPublisher<WeatherData, Error> {
let url = URL(string: urlStr)!
return URLSession.shared.dataTaskPublisher(for: url)
.tryMap({ (data, response) in
let response = (response as? HTTPURLResponse)
if response?.statusCode != 200 {
throw NSError(domain: "Error", code: response!.statusCode, userInfo: .none)
}
return data
})
.flatMap{ data in
Just(data)
.decode(type: WeatherData.self, decoder: JSONDecoder())
.catch{ error in
return Just(defaultWeatherData)
}
}
.mapError{ error in
return error
}
.receive(on: RunLoop.main)
.eraseToAnyPublisher()
}
Can somebody guide what is wrong here or I am using some wrong approach. Thanks
Change your function to this:
private func fetchDataFor(urlStr: String) -> AnyPublisher<WeatherData, Error> {
let url = URL(string: urlStr)!
return URLSession.shared.dataTaskPublisher(for: url)
.tryMap({ (data, response) in
let response = (response as? HTTPURLResponse)
if response?.statusCode != 200 {
throw NSError(domain: "Error", code: response!.statusCode, userInfo: .none)
}
return data
})
.decode(type: WeatherData.self, decoder: JSONDecoder())
.receive(on: RunLoop.main)
.eraseToAnyPublisher()
}
Combine has helpful build-in .decode method, where you can convert your data to the local model.
Also, a good practice is not to use .receive(on: RunLoop.main) in the request methods, but let your ViewModel/Interactor (simply a consumer of response) decide on which thread it wants to receive the response.

Resources