How to return a failure inside .map function in a Result - swift

I have a method execute that calls an external API with a callback that receives Result<Data?,Error>. How can I map that optional success to an unwrapped result or an Error?
func execute(then handle: #escaping (Result<Data, Error>) -> Void) {
externalAPI.retrieveData { result in
let mappedResult = result
.map {
guard let data = $0 else {
throw NSError(domain: "", code: 0, description: "error")
}
return data
}
handle(mappedResult)
}
}
This code fails with Invalid conversion from throwing function of type '(Optional<Data>) throws -> _' to non-throwing function type '(Data?) -> NewSuccess'
I was able to do this with a simple switch (below), but I was wondering if throwing a failure inside the .map is possible.
func execute(then handle: #escaping (Result<Data, Error>) -> Void) {
externalAPI.retrieveData { result in
switch result {
case .failure(let error):
handle(.failure(error))
case .success(let data):
guard let data = data else {
handle(.failure(NSError(domain: "", code: 0, description: "error")))
return
}
handle(.success(data))
}
}
}

You can convert between throws functions and functions that return Result<Success, Error> by using Result(catching:) and .get().
Here's your original map call:
.map {
guard let data = $0 else {
throw NSError(domain: "", code: 0, description: "error")
}
return data
}
Result.map takes a Result and a function that converts (Success) -> NewSuccess, and returns a Result<NewSuccess, Failure>.
Your map takes a Data (Success), and returns Result<Data, Error> (NewSuccess). So the final type, by plugging in NewSuccess is: Result<Result<Data, Error>, Error>. That's more layers than you want. You want to flatten that to just Result<Data, Error>, and that's where flatMap comes in.
Your answer shows that, but you can also pull this out into a more general-purpose tool. It only works when Failure == Error, because throws is untyped, so you can't limit it to some subset of errors. But that's what you're doing anyway. Here's tryMap:
extension Result where Failure == Error {
func tryMap<NewSuccess>(_ transform: (Success) throws -> NewSuccess) -> Result<NewSuccess, Error> {
self.flatMap { value in
Result<NewSuccess, Error> { try transform(value) }
}
}
}
With that, you can rewrite this as:
func execute(then handle: #escaping (Result<Data, Error>) -> Void) {
externalAPI.retrieveData { result in
handle(result
.tryMap {
guard let data = $0 else {
throw NSError(domain: "", code: 0, description: "error")
}
return data
})
}
}
That said, I'd probably be tempted to write it this way:
func execute(then handle: #escaping (Result<Data, Error>) -> Void) {
externalAPI.retrieveData { result in
handle(result
.flatMap { maybeData in
maybeData.map(Result.success)
?? .failure(NSError(domain: "", code: 0, description: "error"))
})
}
}
Or if I wanted someone to be able to actually read it later:
func execute(then handle: #escaping (Result<Data, Error>) -> Void) {
externalAPI.retrieveData { result in
handle(result
.flatMap {
switch $0 {
case .some(let data): return .success(data)
case .none: return .failure(NSError(domain: "", code: 0, description: "error"))
}
}
)
}
}
The advantage of this switch over yours is that you don't have to unwrap and rewrap previous failures.

Apparently, this can be done using flatmap. So in my case:
func execute(then handle: #escaping (Result<Data, Error>) -> Void) {
externalAPI.retrieveData { result in
let mappedResult = result
.flatMap { data in
Result<Data, Error> {
guard let data = data else {
throw NSError(domain: "", code: 0, description: "error")
}
return data
}
}
handle(mappedResult)
}
}
It's a little confusing, but it is working for me.

Related

Saving string in flatMap block to database in api call using Combine Swift

I am trying to fetch a value from local database and when not found wants to save it to local database and return it. All of these I am doing in Interactor file and actual saving or fetching is done in seperate file. Following is my code:
public func fetchCode(codeId: String) -> AnyPublisher<String?, Error> {
//Get code from localdb
codeStorageProvider.fetchCode(codeId).flatMap { (code) -> AnyPublisher<String?, Error> in
if let code = code {
return Just(code).mapError{ $0 as Error }.eraseToAnyPublisher()
}
//If not found in db, Get code from server
let code = self.voucherCodeProvider.fetchVoucherCode(codeId: codeId)
return code.flatMap { code in
//save the code to local db
self.codeStorageProvider.saveVoucherCode(code, codeId)
return code
}.eraseToAnyPublisher()
//return code to presenter
}.eraseToAnyPublisher()
}
I am getting following error in flatMap:
Type of expression is ambiguous without more context
Can someone please help me?
If your saveVoucher doesn't return a Publisher and you are not interested in knowing when the operation is completed, there's no need to use flatMap but you can use handleEvents and call the side effect to save the code from there. Something like this:
func fetchLocal(codeId: String) -> AnyPublisher<String?, Error> {
return Empty().eraseToAnyPublisher()
}
func fetchRemote(codeId: String) -> AnyPublisher<String, Error> {
return Empty().eraseToAnyPublisher()
}
func saveLocal(code: String, codeId: String) {
// Save to BD
}
func fetch(codeId: String) -> AnyPublisher<String?, Error> {
return fetchLocal(codeId: codeId)
.flatMap { code -> AnyPublisher<String, Error> in
if let code = code {
return Just(code)
.setFailureType(to: Error.self)
.eraseToAnyPublisher()
} else {
return fetchRemote(codeId: codeId)
.handleEvents(receiveOutput: {
saveLocal(code: $0, codeId: codeId)
})
.eraseToAnyPublisher()
}
}
.map(Optional.some)
.eraseToAnyPublisher()
}

Cannot map error after flatMap usage (Never result type)

I have RestManager class which is used for fetching data from Internet and is returning AnyPublisher
class RestManager {
func fetchData<T: Decodable>(url: URL) -> AnyPublisher<T, ErrorType> {
URLSession
.shared
.dataTaskPublisher(for: url)
.tryMap({ data, _ in
let value = try JSONDecoder().decode(T.self, from: data)
if let array = value as? Array<Any>, array.isEmpty {
throw ErrorType.empty
}
return value
})
.mapError { error -> ErrorType in
switch error {
case is ErrorType:
return ErrorType.empty
case let urlError as URLError:
switch urlError.code {
case .notConnectedToInternet, .networkConnectionLost, .timedOut:
return .noInternetConnection
case .cannotDecodeRawData, .cannotDecodeContentData:
return .empty
default:
return .general
}
default:
return .general
}
}
.eraseToAnyPublisher()
}
}
Repository has two functions (getWorldwideData and getCountryData returning AnyPublisher<(WorldwideResponse item or CountryResponse item), ErrorType>)
In viewModel, I made these functions.
private func getData() {
$useCaseSelection
.flatMap { value -> AnyPublisher<Covid19StatisticsDomainItem, ErrorType> in
self.loader = true
self.error = nil
switch value {
case let .country(name):
return self.countryPipeline(name: name)
case .worldwide:
return self.worldwidePipeline()
}
}
.mapError { error in
self.error = error
}
.assign(to: &$homeScreenDomainItem)
}
private func worldwidePipeline() -> AnyPublisher<Covid19StatisticsDomainItem, ErrorType> {
repository
.getWorldwideData()
.map { response -> Covid19StatisticsDomainItem in
self.error = nil
self.loader = false
return Covid19StatisticsDomainItem(worldwideResponseItem: response)
}
.eraseToAnyPublisher()
}
private func countryPipeline(name: String) -> AnyPublisher<Covid19StatisticsDomainItem, ErrorType> {
repository
.getCountryData(for: name)
.map { response -> Covid19StatisticsDomainItem in
self.error = nil
self.loader = false
return Covid19StatisticsDomainItem(countryDayOneStatsResponse: response)
}
.eraseToAnyPublisher()
}
I wanted to make clean code, so I split code into two separate function based on useCaseSelection.
useCaseSelection is enum with two types.
error is ErrorType? value wrapped with #Published, in which I want to save error type if there is any error.
homeScreenDomainItem is Covid19StatisticsDomainItem instance wrapped with #Published.
Problem is in getData function where in MapError pipeline I am getting:
Cannot convert value of type () to closure result type Never
I tried to use setFailureType(to: ErrorType.self) but that is not helping.

Swift: Extending Combine operators

I'm trying to wrap a tryMap operator along the lines of this article.
extension Publisher where Output == Data {
func decode<T: Decodable>(as type: T.Type = T.self, using decoder: JSONDecoder = .init()) -> Publishers.Decode<Self, T, JSONDecoder> {
decode(type: type, decoder: decoder)
}
}
extension Publisher where Output == URLSession.DataTaskPublisher.Output {
func processData(_: #escaping (Self.Output) throws -> Data) -> Publishers.TryMap<Self, Data> {
tryMap { element -> Data in
guard let httpResponse = element.response as? HTTPURLResponse,
httpResponse.statusCode == 200
else {
throw URLError(.badServerResponse)
}
return element.data
}
}
}
While using it I'm getting a compiler error which I'm struggling with:
return urlSession
.dataTaskPublisher(for: request)
.processData // <- Value of type '(#escaping (URLSession.DataTaskPublisher.Output) throws -> Data) -> Publishers.TryMap<URLSession.DataTaskPublisher, Data>' (aka '(#escaping ((data: Data, response: URLResponse)) throws -> Data) -> Publishers.TryMap<URLSession.DataTaskPublisher, Data>') has no member 'decode'
.decode(as: InstantResponse.self)
.eraseToAnyPublisher()
What would be the correct way of doing it?
Thanks!
First of all, you aren't calling processData - you are missing the parentheses, which would actually execute the function. Second, your processData declaration is incorrect, it should not take a closure as its input argument, since you aren't using that closure anyways.
extension Publisher where Output == URLSession.DataTaskPublisher.Output {
func processData() -> Publishers.TryMap<Self, Data> {
tryMap { element -> Data in
guard let httpResponse = element.response as? HTTPURLResponse,
httpResponse.statusCode == 200
else {
throw URLError(.badServerResponse)
}
return element.data
}
}
}
return urlSession
.dataTaskPublisher(for: request)
.processData() // parentheses necessary here
.decode(as: InstantResponse.self)
.eraseToAnyPublisher()

Throwing errors from closure

I have this piece of code in my app:
func saveContact2(contact: String) throws {
let contactStore = CNContactStore()
contactStore.requestAccess(for: .contacts, completionHandler: {(granted, error) in
if granted && error == nil {
//...
} else {
if !granted {
throw contactErrors.contactAccessNotGranted(["Error","Access to Contacts is not granted."])
}
}
})
}
I'd like to throw all errors raising in closure to calling function.
Compiler shows error:
Invalid conversion from throwing function of type '(_, _) throws -> ()' to non-throwing function type '(Bool, Error?) -> Void'
Could anyone help me please with the right syntax?
You cannot throw errors from an #escaping closure that is called asynchronously. And this makes sense because your app has carried on with its execution and there’s no where to catch the error.
So, instead, adopt completion handler pattern yourself:
func saveContact2(_ contact: String, completion: #escaping: (Result<Bool, Error>) -> Void) {
let contactStore = CNContactStore()
contactStore.requestAccess(for: .contacts) { (granted, error) in
guard granted else {
completion(.failure(error!)
return
}
//...
completion(.success(true))
}
}
And then you’d call it like:
saveContact2(contactName) { result in
switch result {
case .failure:
// handler error here
case .success:
// handle confirmation of success here
}
}
If you’re using an old compiler that doesn’t have the Result type, it’s basically:
enum Result<Success, Failure> where Failure: Error {
case success(Success)
case failure(Failure)
}

Rejecting a the returned promise inside a then block

Say I have two promises I want to combine with a when(resolved:). I want to reject the promise if there was a problem with the first promise, but resolve otherwise. Essentially, this is what I want to do:
func personAndPetPromise() -> Promise<(Person, Pet?)> {
let personPromise: Promise<Person> = ...
let petPromise: Promise<Pet> = ...
when(resolved: personPromise, petPromise).then { _ -> (Person, Pet?) in
if let error = personPromise.error {
return Promise(error: error) // syntax error
}
guard let person = personPromise.value else {
return Promise(error: myError) // syntax error
}
return (person, petPromise.value)
}
}
such that externally I can do something like this:
personAndPetPromise().then { person, pet in
doSomethingWith(person, pet)
}.catch { error in
showError(error)
}
The problem lies within the the then { _ in block in personAndPetPromise. There's no way that method can return both a Promise(error:) and a (Person, Pet?).
How else can I reject the block?
The problem is that there are two overloads of the then function:
public func then<U>(on q: DispatchQueue = .default, execute body: #escaping (T) throws -> U) -> Promise<U>
public func then<U>(on q: DispatchQueue = .default, execute body: #escaping (T) throws -> Promise<U>) -> Promise<U>
The first one's body returns a U and causes then to return Promise<U>.
The second one's body returns a Promise<U> and causes then to return Promise<U>.
Since in this case we want to return an error or a valid response, we're forced to use the second overload.
Here's a working version. The main difference is I changed it from -> (Person, Pet?) to -> Promise<(Person, Pet?)>:
func personAndPetPromise() -> Promise<(Person, Pet?)> {
let personPromise: Promise<Person> = ...
let petPromise: Promise<Pet> = ...
when(resolved: personPromise, petPromise).then { _ -> Promise<(Person, Pet?)> in
if let error = personPromise.error {
return Promise(error: error)
}
guard let person = personPromise.value else {
return Promise(error: myError)
}
return Promise(value: (person, petPromise.value))
}
}
Another way to do the same thing is by throwing the error rather than attempting to return it:
func personAndPetPromise() -> Promise<(Person, Pet?)> {
let personPromise: Promise<Person> = ...
let petPromise: Promise<Pet> = ...
when(resolved: personPromise, petPromise).then { _ -> (Person, Pet?) in
if let error = personPromise.error {
throw error
}
guard let person = personPromise.value else {
throw myError
}
return (person, petPromise.value)
}
}