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

// 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
}

Related

Combine conversion from model to viewModel gives error "Type of expression is ambiguous without more context when running .eraseToAnyPublisher()

I am trying to figure out why the last .eraseToAnyPublisher() is giving the aforementioned error, to me it seems all the types are well specified, aren't they?
static func searchUsers(query: String) -> AnyPublisher<[UserViewModel], Never> {
// prepare URL
let urlString = "\(baseURL)/search/users?q=\(query)"
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
}
print(String(data: data, encoding: .utf8) ?? "")
return data
}
.decode(type: SearchUserResponse.self, decoder: JSONDecoder())
.map { $0.items }
.flatMap({ users in
var userViewModels = [UserViewModel]()
users.forEach { user in
userViewModels.append(contentsOf: UserViewModel(with: user))
}
return userViewModels
})
.catch { err -> Just<[UserViewModel]> in
print(err)
return Just([])
}
.eraseToAnyPublisher() // <-- HERE IS THE ERROR
return publisher
}
Unfortunately with those complex Combine pipelines sometimes compliler errors are displayed on the wrong line. In your case there are two problems, but not where the compiler is pointing.
One being use of flatMap instead of map.
This part of the pipeline:
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
}
print(String(data: data, encoding: .utf8) ?? "")
return data
}
.decode(type: SearchUserResponse.self, decoder: JSONDecoder())
.map { $0.items }
returns Publisher<[User], Error>.
Next you want to transforming that into Publisher<[UserViewModel], Error> for which you need a function map:
func map<T>(_ transform: #escaping (Output) -> T) -> Publishers.Map<Upstream, T>
which transforms one type of Output into another type of Output not flatMap:
func flatMap<T, P>(maxPublishers: Subscribers.Demand = .unlimited, _ transform: #escaping (Self.Output) -> P) -> Publishers.FlatMap<P, Self> where T == P.Output, P : Publisher, Self.Failure == P.Failure
which transforms Output into a new Publisher
The second problem is with append(contentsOf;) which expects a Sequence of elements, in case of single elements you should use append(), but even simpler would be just to map the [User] to [UserViewModel]:
{ users in
users.map { user in
UserViewModel(with: user)
}
}
so the whole function should work with those changes:
static func searchUsers(query: String) -> AnyPublisher<[UserViewModel], Never> {
// prepare URL
let urlString = "\(baseURL)/search/users?q=\(query)"
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
}
print(String(data: data, encoding: .utf8) ?? "")
return data
}
.decode(type: SearchUserResponse.self, decoder: JSONDecoder())
.map { $0.items }
.map { users in
users.map { user in
UserViewModel(with: user)
}
}
.catch { err -> Just<[UserViewModel]> in
print(err)
return Just([])
}
.eraseToAnyPublisher()
return publisher
}

refresh token using combine networking 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.

Swift Combine: send completion after send value

I'm working on caching for my network module. My module will return an AnyCancallable back to the caller for each request. If cached data is not available, I use URLSession.dataTaskPublisher, it works fine with 2 events: data received and completion. If cached data is available, I will use a CurrentValueSubject to create the AnyCancallable to return. I send both of the 2 events on this subject, but on the caller side, it only gets notified on the completion, no data.
cacheSubject.send(cachedData.data)
cacheSubject.send(completion: Subscribers.Completion<Error>.finished)
Removing the completion send and now it can receive data, but no completion notification.
Could someone please let me know what I'm doing wrong here?
Here is the full file in case you guys need it:
public class SSNetworkManager {
public static let shared = SSNetworkManager()
private var cache: [String: CachedData] = [:]
private let cacheSubject = CurrentValueSubject<Data, Error>(Data())
#discardableResult public func makeServiceCall<D: Decodable>(forRequest request: SSNetworkRequest<D>, onMainThread: Bool = true) -> AnyPublisher<D, Error>? {
guard let urlRequest = request.urlRequest else {
return nil
}
var cancelable: AnyPublisher<Data, Error>
if let url = urlRequest.url?.absoluteString,
let cachedData = cache[url],
cachedData.isValid {
cancelable = cacheSubject.eraseToAnyPublisher()
cacheSubject.send(cachedData.data)
cacheSubject.send(completion: Subscribers.Completion<Error>.finished)
} else {
cancelable = URLSession.shared.dataTaskPublisher(for: urlRequest).tryMap {[weak self] (data, response) -> Data in
guard let httpResponse = response as? HTTPURLResponse, httpResponse.statusCode == 200 else {
throw SSNetworkError(httpCode: (response as? HTTPURLResponse)?.statusCode ?? 0, data: data)
}
if request.shouldCacheNow,
let url = urlRequest.url?.absoluteString {
self?.cache[url] = CachedData(data: data, expirationTime: request.cacheExpirationTime)
}
return data
}.eraseToAnyPublisher()
}
if onMainThread {
return cancelable
.receive(on: RunLoop.main)
.decode(type: D.self, decoder: JSONDecoder())
.eraseToAnyPublisher()
} else {
return cancelable
.decode(type: D.self, decoder: JSONDecoder())
.eraseToAnyPublisher()
}
}
}
fileprivate struct CachedData {
let data: Data
let expirationTime: Date
var isValid: Bool {
return Date().compare(expirationTime) != .orderedDescending
}
}
This isn't the right case to use a subject. Instead, return the publisher, relevant to each case:
public class SSNetworkManager {
// ...
public func makeServiceCall<D: Decodable>(
forRequest request: SSNetworkRequest<D>,
onMainThread: Bool = true
) -> AnyPublisher<D, Error>? {
// consider just returning Empty().eraseToAnyPublisher() instead of nil
guard let urlRequest = request.urlRequest else {
return nil
}
var resultPublisher: AnyPublisher<D: Error>
if let url = urlRequest.url?.absoluteString,
let cachedData = cache[url],
cachedData.isValid {
resultPublisher = Just(cachedData.data)
.setFailureType(to: Error.self)
.eraseToAnyPublisher()
} else {
resultPublisher = URLSession.shared
.dataTaskPublisher(for: urlRequest)
.tryMap { ... }
.decode(type: D.self, decoder: JSONDecoder())
.eraseToAnyPublisher()
}
return onMainThread
? resultPublisher
.receive(on: DispatchQueue.main)
.eraseToAnyPublisher()
: resultPublisher
.eraseToAnyPublisher()
}
}

Error is not triggering on url session using combine

enum FailureReason : Error {
case sessionFailed(error: URLError)
case decodingFailed
case other(Error)
}
the custom error enum
private func performOperation<T: Decodable>(requestUrl: URLRequest, responseType: T.Type)->AnyPublisher<T, FailureReason>
{
return URLSession.shared.dataTaskPublisher(for: requestUrl)
.map(\.data)
.decode(type: T.self, decoder: JSONDecoder())
.mapError({ error -> FailureReason in
switch error {
case is Swift.DecodingError:
return .decodingFailed
case let urlError as URLError:
return .sessionFailed(error: urlError)
default:
return .other(error)
}
})
.eraseToAnyPublisher()
}
this is how my urlsession publisher looks like
func validateLogin(username : String , password :String) {
let url = "\(Constants.baseUrl)api/v1/auth/login/"
let htppbodyRequest = EmailLogin(username: username, password: password)
let httpBody = try! JSONEncoder().encode(htppbodyRequest)
cancellable = webservice.apiRequest(url: URL(string: url)!, resultType: User.self, httpMethodType: .post, requestBody: httpBody)
.map{ $0 }
.receive(on: RunLoop.main)
.sink(receiveCompletion: {
print("Received completion: \($0).")
}, receiveValue: { (user) in
print("user name is :\(user)")
self.subject.send(user)
})
User is the decodable struct. Even if i enter invalid username and password the Received completion of sink prints finished error is never thrown.
Subject is a passthroughSubject.
It seems you have a misunderstanding regarding when a URLSession.DataTaskPublisher should fail with an error. A data task only fails with an error in case there is a network error (such as no internet connection, SSL error, etc).
Inputting an incorrect username or password is not a network error and hence will not result in the data task throwing an error. Depending on your backend implementation, it might result in an error status code (not in the 200..<300 range) and an error response in the body of the request.
To check the status code of the HTTPURLResponse and throw an error in case it's incorrect, you can use tryMap on the dataTaskPublisher.
Here's how you can define convenience methods on URLSession.DataTaskPublisher that handle the status code of the HTTPURLResponse and throw an error in case it's incorrect.
enum NetworkingError: Error {
case decoding(DecodingError)
case incorrectStatusCode(Int)
case network(URLError)
case nonHTTPResponse
case unknown(Error)
}
extension Publisher {
func mapErrorToNetworkingError() -> AnyPublisher<Output, NetworkingError> {
mapError { error -> NetworkingError in
switch error {
case let decodingError as DecodingError:
return .decoding(decodingError)
case let networkingError as NetworkingError:
return networkingError
case let urlError as URLError:
return .network(urlError)
default:
return .unknown(error)
}
}
.eraseToAnyPublisher()
}
}
extension URLSession.DataTaskPublisher {
func emptyBodyResponsePublisher() -> AnyPublisher<Void, NetworkingError> {
httpResponseValidator()
.map { _ in Void() }
.eraseToAnyPublisher()
}
}
extension URLSession.DataTaskPublisher {
func httpResponseValidator() -> AnyPublisher<Output, NetworkingError> {
tryMap { data, response in
guard let httpResponse = response as? HTTPURLResponse else { throw NetworkingError.nonHTTPResponse }
let statusCode = httpResponse.statusCode
guard (200..<300).contains(statusCode) else { throw NetworkingError.incorrectStatusCode(statusCode) }
return (data, httpResponse)
}
.mapErrorToNetworkingError()
}
func httpResponseValidatorDataPublisher() -> AnyPublisher<Data, NetworkingError> {
httpResponseValidator()
.map(\.data)
.eraseToAnyPublisher()
}
func jsonDecodingPublisher<T:Decodable>(type: T.Type) -> AnyPublisher<T, NetworkingError> {
httpResponseValidatorDataPublisher()
.decode(type: T.self, decoder: JSONDecoder())
.mapErrorToNetworkingError()
}
}
And then you can simplify your performOperation function as below and it will throw an error in case the status code of the response is not in the expected range.
private func performOperation<T: Decodable>(requestUrl: URLRequest, responseType: T.Type) -> AnyPublisher<T, NetworkingError> {
URLSession.shared.dataTaskPublisher(for: requestUrl)
.jsonDecodingPublisher(type: T.self)
}

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()

Resources