I am studying and trying out a few stuff with Combine to apply on my own and came into the following situation with this contrived example..
let sequencePublisher = [70, 5, 17].publisher
var cancellables = [AnyCancellable]()
sequencePublisher
// .spellOut()
.flatMap { query -> URLSession.DataTaskPublisher in
return URLSession.shared.dataTaskPublisher(for: URL(string: "http://localhost:3000?q=\(query)")!)
}
.compactMap { String(data: $0.data, encoding: .utf8) }
.sink(receiveCompletion: { completion in
switch completion {
case .failure(let error):
print(error.localizedDescription)
default: print("finish")
}
}) { value in
print(value)
}
.store(in: &cancellables)
I have a sequence publisher that emits 3 Integers and I pass it through flatMap and send a Get request request to my local API that simply returns back the same value it got embedded in a string.
It all works fine, I get all 3 API responses in sink, as long as I don't uncomment the spellOut() custom operator, this operator is supposed to fail if the number is smaller than 6, here is what it does:
enum ConversionError: LocalizedError {
case lessThanSix(Int)
var errorDescription: String? {
switch self {
case .lessThanSix(let n):
return "could not convert number -> \(n)"
}
}
}
extension Publisher where Output == Int {
func spellOut() -> Publishers.TryMap<Self, String> {
tryMap { n -> String in
let formatter = NumberFormatter()
formatter.numberStyle = .spellOut
guard n > 6, let spelledOut = formatter.string(from: n as NSNumber) else { throw ConversionError.lessThanSix(n) }
return spelledOut
}
}
}
The code doesn't even compile if I add another map operator before flatMap it works, but with a tryMap it just says
No exact matches in call to instance method 'flatMap'
Is there any way of achieving this or why is it not allowed?
Thank you in advance for the answers
The problem here is that FlatMap requires the returned publisher created in its closure to have the same Failure type as its upstream (unless upstream has a Never failure).
So, a Sequence publisher, like:
let sequencePublisher = [70, 5, 17].publisher
has a failure type of Never and all works.
But TryMap, which is what .spellOut operator returns, has a failure type of Error, and so it fails, because DataTaskPublisher has a URLError failure type.
A way to fix is to match the error type inside the flatMap:
sequencePublisher
.spellOut()
.flatMap { query in
URLSession.shared.dataTaskPublisher(for: URL(...))
.mapError { $0 as Error }
}
// etc...
You have to map the error after the tryMap.
publisher
.tryMap({ id in
if let id = id { return id } else { throw MyError.unknown("noId") }
})
.mapError { $0 as? MyError ?? MyError.unknown("noId") }
.flatMap { id -> AnyPublisher<Model, MyError> in
fetchDataUseCase.execute(id: id)
}
.eraseToAnyPublisher()
In case the error types already match, another point of failure can be, when you work with any Publisher as return values. Then you need to call eraseToAnyPublisher() on the publishers - the first one and the one returned from the flatMap closure.
anyPublisher.eraseToAnyPublisher()
.flatMap { value in
anotherPublisher.eraseToAnyPublisher()
}
Related
I created this function for decoding locally saved data.
private func getLocalCertificates(_ data: [CovidCertificateEntity]) -> [CovidCertificateDomainItem]? {
var newCertificates: [CovidCertificateDomainItem]?
Publishers
.MergeMany(
data.map { result -> AnyPublisher<Result<EUDCC, ErrorType>, Never> in
self.certificateToDelete = result.qrCodeValue ?? ""
return repository.getCertificateDetails(result.qrCodeValue ?? "")
})
.map { result -> CovidCertificateDomainItem? in
switch result {
case .success(let eudcc):
do {
return try CovidCertificateDomainItem(eudcc: eudcc, qrCode: self.certificateToDelete)
}
catch {
return nil
}
case.failure(_):
return nil
}
}
.compactMap { $0 }
.collect()
.sink { result in
newCertificates = result.reversed()
}
.store(in: &cancellables)
return newCertificates
}
I wanted to achieve that value result from data.map inside MergeMany operator is proceeded to .map operator, so I can use it in constructor of CovidCertificateDomainItem
I tried to made this with help variable certificateToDelete, but it always have last value from data.map.
Is there any way to achieve this?
Pass a tuple. Instead of returning
repository.getCertificateDetails(result.qrCodeValue ?? "")
return a tuple:
(result, repository.getCertificateDetails(result.qrCodeValue ?? ""))
You will have many other adjustments to make in order to achieve that, but when you do, you'll be able to receive that tuple in map and thus have all the data you need.
EDIT:
I am trying my level best to make my question simpler,
here what I am trying to get a solution for is, I have an API and if my data is valid the API will give the correct response, for which I need to decode with the respective struct in swift.
also if my data is wrong the API will fail and it will produce an error response which is a different struct.
with the use of combine, I only could decode a single struct type.
SO how do I make my decode accept any type?
Generics is one way I hoped to solve but here the protocol that I need to implement is an issue I believe restricting me from using generics.
thanks for giving it a try.
// MARK: - ResponseStruct Model
struct ResponseStruct: Codable {
}
//MARK: -Protocol
public protocol RequestProtocol{
associatedtype ResponseOutput
func fetchFunction() -> AnyPublisher<ResponseOutput, Error>
}
//Mark: - Implementation
struct RequestStruct: Codable, RequestProtocol {
typealias ResponseOutput = ResponseStruct
func fetchFunction() -> AnyPublisher<ResponseOutput, Error> {
let networkManager = NetworkManager()
do {
return try networkManager.apiCall(url: url, method: .post, body: JSONEncoder().encode(self))
.decode(type: ResponseStruct.self, decoder: JSONDecoder())
.eraseToAnyPublisher()
} catch {
}
}
}
Above this is the code, and this is fine if the API call works but if the call fails I will get an error response, so how to decode that struct in a combined way? I don't want to write another call for that and I am hoping to get something to do with Failure in the combine. or CAN I MAKE THE associated type (see protocol) generic?
I beg for your patience. I think I understand the problem, but I'm having a hard time lining it up to the code you've given. Your fetchFunction is particularly odd and I don't understand what your protocol is trying to accomplish.
Let me start with the problem statement and explore a solution. I'll do it step-by-step so this will be a long response. The tl;dr is a Playground at the end.
I have an API and if my data is valid the API will give the correct response, for which I need to decode with the respective struct in swift.
If my data is wrong the API will fail and it will produce an error response which is a different struct.
So we need two structs. One for success and one for failure. I'll make some up:
struct SuccessfulResult : Decodable {
let interestingText : String
}
struct FailedResult : Decodable {
let errorCode : Int
let failureReason : String
}
Based on that, request to the network can:
Return success data to decode into SuccessfulResult
Return failure data to decode into FailedResult
Fail because of a low-level error (e.g. The network is unreachable).
Let's create a type for "The network worked just fine and gave me either success data or failure data":
enum NetworkData {
case success(Data)
case failure(Data)
}
I'll use Error for low-level errors.
With those types an API request can be represented as a publisher of the type AnyPublisher<NetworkData, Error>
But that's not what you asked for. You want to parse the data into SuccessfulResult or FailedResult. This also raises the possibility that JSON parsing fails which I will also sweep under the rug of a generic Error.
We need a data type to represent the parsed variant of NetworkData:
enum ParsedNetworkData {
case success(SuccessfulResult)
case failure(FailedResult)
}
Which means the real Network request type you've asked for is a publisher of the type AnyPublisher<ParsedNetworkData,Error>
We can write a function to transform a Data bearing network request, AnyPublisher<NetworkData,Error>, into an AnyPublisher<ParsedNetworkData,Error>.
One way to write that function is:
func transformRawNetworkRequest(_ networkRequest: AnyPublisher<NetworkData,Error>) -> AnyPublisher<ParsedNetworkData, Error> {
let decoder = JSONDecoder()
return networkRequest
.tryMap { networkData -> ParsedNetworkData in
switch(networkData) {
case .success(let successData):
return ParsedNetworkData.success(try decoder.decode(SuccessfulResult.self, from: successData))
case .failure(let failureData):
return ParsedNetworkData.failure(try decoder.decode(FailedResult.self, from: failureData))
}
}
.eraseToAnyPublisher()
}
To exercise the code we can write a function to create a fake network request and add some code that tries things out. Putting it all together into a playground you get:
import Foundation
import Combine
struct SuccessfulResult : Decodable {
let interestingText : String
}
struct FailedResult : Decodable {
let errorCode : Int
let failureReason : String
}
enum NetworkData {
case success(Data)
case failure(Data)
}
enum ParsedNetworkData {
case success(SuccessfulResult)
case failure(FailedResult)
}
func transformRawNetworkRequest(_ networkRequest: AnyPublisher<NetworkData,Error>) -> AnyPublisher<ParsedNetworkData, Error> {
let decoder = JSONDecoder()
return networkRequest
.tryMap { networkData -> ParsedNetworkData in
switch(networkData) {
case .success(let successData):
return ParsedNetworkData.success(try decoder.decode(SuccessfulResult.self, from: successData))
case .failure(let failureData):
return ParsedNetworkData.failure(try decoder.decode(FailedResult.self, from: failureData))
}
}
.eraseToAnyPublisher()
}
func fakeNetworkRequest(shouldSucceed: Bool) -> AnyPublisher<NetworkData,Error> {
let successfulBody = """
{ "interestingText" : "This is interesting!" }
""".data(using: .utf8)!
let failedBody = """
{
"errorCode" : -4242,
"failureReason" : "Bogus! Stuff went wrong."
}
""".data(using: .utf8)!
return Future<NetworkData,Error> { fulfill in
let delay = Set(stride(from: 100, to: 600, by: 100)).randomElement()!
DispatchQueue.global(qos: .background).asyncAfter(
deadline: .now() + .milliseconds(delay)) {
if(shouldSucceed) {
fulfill(.success(NetworkData.success(successfulBody)))
} else {
fulfill(.success(NetworkData.failure(failedBody)))
}
}
}.eraseToAnyPublisher()
}
var subscriptions = Set<AnyCancellable>()
let successfulRequest = transformRawNetworkRequest(fakeNetworkRequest(shouldSucceed: true))
successfulRequest
.sink(receiveCompletion:{ debugPrint($0) },
receiveValue:{ debugPrint("Success Result \($0)") })
.store(in: &subscriptions)
let failedRequest = transformRawNetworkRequest(fakeNetworkRequest(shouldSucceed: false))
failedRequest
.sink(receiveCompletion:{ debugPrint($0) },
receiveValue:{ debugPrint("Failure Result \($0)") })
.store(in: &subscriptions)
For scientific reasons I've created a Publisher and a Subscriber so I can dive into Combine.
The Publisher has been converted from a never failing to the failing one.
enum IntegerError: String, Error {
case miltupleOf2 = "We are sorry but the number is a multiple of 2, therefore cannot be used in the process"
}
let integerPublisher = [1,3,3,3,3,3,5,6,7,7].publisher
.setFailureType(to: IntegerError.self)
let subscribtion = integerPublisher
.tryMap { intValue in
if intValue.isMultiple(of: 2) {
throw IntegerError.miltupleOf2
} else {
return intValue
}
}
.sink { completion in
switch completion {
case .finished:
print("success")
case .failure(let error):
if let error = error as? IntegerError {
print(error.rawValue)
} else {
print(error)
}
}
} receiveValue: { value in
print(value)
}
My question is: when using sink, the error type is Error. Why is it not the custom IntegerError that I've used within the .setFailureType modifier?
The need of casting my error to the type that I specified earlier seems a little redundant.
Thank you.
The reason for this is quite straightforward. tryMap returns a Publishers.TryMap<Upstream, Output>, which is a specific kind of publisher with Failure == Error:
typealias Failure = Error
So as soon as you use tryMap, that undoes what setFailureType did.
The reason why Publishers.TryMap has Error as its Failure type is because in Swift, you can't specify what specific type of error a closure can throw (unlike Java for example). Once you mark a closure as throws, like tryMap has done with its transform parameter:
func tryMap<T>(_ transform: #escaping (Self.Output) throws -> T) -> Publishers.TryMap<Self, T>
any Error can be thrown inside the transform closure. You can try changing throw IntegerError.miltupleOf2 to throwing another type of error. Your code will still compile.
Hypothetically, if Swift allowed you to specify what type of error you are allowed to throw in the closure, then tryMap could have been declared as (fake syntax):
func tryMap<T, E: Error>(_ transform: #escaping (Self.Output) throws E -> T) -> Publishers.TryMap<Self, T, E>
and you wouldn't even need setFailureType.
As a workaround, you can use mapError to cast the error to your desired type:
let subscribtion = integerPublisher
.tryMap { intValue -> Int in
if intValue.isMultiple(of: 2) {
throw IntegerError.miltupleOf2
} else {
return intValue
}
}.mapError { $0 as! IntegerError }
.sink { completion in
switch completion {
case .finished:
print("success")
case .failure(let error):
print(error.rawValue)
}
} receiveValue: { value in
print(value)
}
You need to reassure the compiler that you haven't thrown any other type of errors in tryMap.
I'm coming from JS and learning Swift to build an iOS native version of an app.
In JS, I use the following pattern all the time:
...
async function doAyncFunction(item) {
try {
// do async call to fetch data using item
return Promise.resolve(data);
} catch (error) {
return Promise.reject(error);
}
}
const promises = items.map((item) => doAyncFunction(item));
const results = await Promise.all(promises);
...
I've started looking at PromiseKit, but I'm wondering what's are the Swift ways of doing this?
Thanks.
The forthcoming Swift 5.5 in Xcode 13 (still in beta at this point in time) uses a very similar async-await pattern. See The Swift Programming Language: Concurrency.
In the interim, there are a unfortunately dizzying number of alternatives. For example, there are a variety of third-party promise/future frameworks. Or there is the declarative Combine framework, which was launched a few years agar with the advent of the non-imperative patterns of SwiftUI.
All of that having been said, the most common pattern you’ll see in Swift code is the use of escaping “closures” which are effectively units of code that are passed as a parameter to a function, and which the function invokes when the asynchronous task completes. In that pattern you don’t await, but rather just specify what you want to do when the asynchronous task finishes. For example, in this function, it has a parameter called completion which is a closure that is called when the asynchronous task completes:
func fetch(using value: Int, completion: #escaping (Result<Foo, Error>) -> Void) {
let url = …
let task = URLSession.shared.dataTask(with: url) { data, response, error in
// handle errors, if any, e.g.:
if let error == error else {
completion(.failure(error))
return
}
// parse data into `Foo` here, and when done, call the `completion closure:
…
completion(.success(foo))
}
task.resume()
}
And then you would call it like so:
fetch(using: 42, completion: { result in
// this is called when the fetch finishes
switch result {
case .failure(let error): // do something with `error`
case .success(let foo): // do something with `foo`
}
})
// note, execution will continue here, and the above closure will
// be called later, so do not try to use `foo` here
Or, using a more concise “trailing closure” syntax:
fetch(using: 42) { result in
// this is called when the fetch finishes
switch result {
case .failure(let error): // do something with `error`
case .success(let foo): // do something with `foo`
}
}
// note, execution will continue here, and the above closure will
// be called later, so do not try to use `foo` here
And if you wanted to be notified when a series of calls was done, you could use a DispatchGroup, e.g.
let group = DispatchGroup()
for value in values {
group.enter()
fetch(using: value) { result in
// do something with result
group.leave()
}
}
group.notify(queue: .main) {
// this is called when every `enter` call is matched up with a `leave` Call
}
It is up to you whether you stick to the beta version of Swift 5.5 with a very familiar async-await pattern, use a third-party future/promise library, use Combine, or use the traditional closure-based pattern, shown above.
At the very least, I would suggest familiarizing yourself with this latter pattern as it is the predominant technique in Swift right now. But rest assured that the familiar async-await pattern is coming soon, so if you are willing to wait for it to finish going through the beta process (or join that beta process), then check that out.
Using the aforementioned builtin Combine framework, you have several options. The one that you probably want is Publishers.Merge:
let publishers = ... // multiple functions that implement the Publisher protocol
let combined = Publishers.MergeMany(publishers)
Alternatives to MergeMany are Merge, Merge3, Merge4 up to Merge8 when the amount of publishers is set. If the number of outputs is variable, use MergeMany.
Other options include merge on the publishers themselves:
let publisher1 = ...
let publisher2 = ...
publisher1.merge(publisher2)
CombineLatest or, in the case of a publisher that immediately completes, Zip can be used to receive a tuple when everything is done:
let publisher1 = ...
let publisher2 = ...
Publishers.CombineLatest(publisher1, publisher2)
For the moment there is a great framework that is closest to async/await, it's SwiftCoroutine https://github.com/belozierov/SwiftCoroutine (much better than promiseKit, I tested the 2..)
Swift coroutine with your example:
func doFutureFunction() -> CoFuture<Int> {
CoFuture { promise in
myRequest { error, data in
if let error = error {
promise(.failure(error))
} else {
promise(.success(data))
}
}
}
}
let futures = items.map { item in doFutureFunction(item) } // [CoFuture<Int>]
DispatchQueue.main.startCoroutine {
let results = promises.compactMap { try? $0.await() } // [Int]
}
The equivalent of
consts value = await future.value
consts value1 = await future.value
consts value2 = await future.value
console.log("Value " + value + ", value1 " + value1 + ", value2 " + value2)
is
DispatchQueue.main.startCoroutine {
do {
let value = try future.await()
let value1 = try future.await()
let value2 = try future.await()
print("Value \(value), value1 \(value1), value2 \(value2)")
} catch {
print(error.localizedDescription)
}
}
While waiting for swift 5.5 and official async / await from Apple
You can look at PromiseQ it's javascript style promises for Swift. It implements all javascript's Promise features: resolve/reject, then, finally, fetch etc. and appends some additional ones: suspend/resume, cancel, retry, timeout etc.
It also supports all, race, any e.g.:
// Fetch avatars of first 30 GitHub users.
struct User : Codable {
let login: String
let avatar_url: String
}
async {
let response = try fetch("https://api.github.com/users").await()
guard response.ok else {
throw response.statusCodeDescription
}
guard let data = response.data else {
throw "No data"
}
let users = try JSONDecoder().decode([User].self, from: data)
let images =
try Promise.all(
users
.map { $0.avatar_url }
.map { fetch($0) }
).await()
.compactMap { $0.data }
.compactMap { UIImage(data: $0) }
async(.main) {
print(images.count)
}
}
.catch { error in
print(error.localizedDescription)
}
Swift's concurrency such as Dispatch queues, Combine and the newest async\await (Swift 5.5) are different from javascript Promises and you can not find many convenient approaches that you used before.
I'm answering myself here with a solution, using PromiseKit, in case it might help someone.
The below is obviously not a full implementation, but it shows how the pattern can be implemented.
func doManyAsyncRequests(userIds: [String], accessToken: String) -> Promise<Void> {
Promise { seal in
let promises = spotifyUserIds.map {
doSingleAsyncRequest(userId: $0.id, accessToken: accessToken) // this function returns a promise
}
when(fulfilled: promises).done { results in
print("Results: \(results)")
// process results
}.catch { error in
print("\(error)")
// handle error
}
}
}
I am trying to create a Result variable with a custom error type with the builtin Result type in Foundation for Swift 5, but I can't quite get the type system to understand the kind of error I want to throw.
The code below does not compile.
import Foundation
enum CustomError: String, Error {
case somethingBadHappened
}
struct Model {
let value: Int
}
class Request {
func execute(number: Int, completion: #escaping (Result<Model, CustomError>) -> Void) {
let result = Result { () throws -> Model in
if (number < 20) {
throw CustomError.somethingBadHappened
} else {
return Model(value: number)
}
}
// compiler complains here about: Cannot convert value of type 'Result<Model, Error>' to expected argument type 'Result<Model, CustomError>'
completion(result)
}
}
let request = Request()
request.execute(number: 19) { result in
switch result {
case .success(let value): print("Succeded with \(value)")
case .failure(let error): print("Failed with \(error)")
}
}
Changing the signature of the completion closure to completion: #escaping (Result<Model, Error>) -> Void works, but then I am not using the custom error type.
How can I make the type system understand I would like to use the custom error type?
Apologies for giving a second answer, but there needs to be a corrective for fphilipe's answer.
You can use init(catching:) to form the Result and yet return it as a Result<Model, CustomError>. That is what mapError is for! Like this:
enum CustomError: String, Error {
case somethingBadHappened
}
struct Model {
let value: Int
}
class Request {
func execute(number: Int, completion: #escaping (Result<Model, CustomError>) -> Void) {
let result = Result { () throws -> Model in
if (number < 20) {
throw NSError()
} else {
return Model(value: number)
}
}.mapError { err in
return CustomError.somethingBadHappened
}
completion(result)
}
}
I have to throw something in order to form the initial Result<Model, Error>, so I just throw an NSError as a kind of placeholder. But then mapError comes along and transforms this into a Result<Model, CustomError>. The power of mapError is that it changes only what happens in case of failure.
Thus we are able to keep the original form of the code.
Changing the signature of the completion closure to completion: #escaping (Result<Model, Error>) -> Void works, but then I am not using the custom error type.
Yes, you are! Change the signature in exactly that way, so that you compile, and then run your code. When we get to this line:
case .failure(let error): print("Failed with \(error)")
... we print "Failed with somethingBadHappened". That proves that your CustomError.somethingBadHappened instance came through just fine.
If the problem is that you want to separate out your CustomError explicitly, then separate it out explicitly as you catch it:
case .failure(let error as CustomError): print(error)
default : fatalError("oops, got some other error")
Or if you want to winnow it down still further and catch only the .somethingBadHappened case, specify that:
case .failure(CustomError.somethingBadHappened): print("Something bad happened")
default : fatalError("oops, got some other error")
Those examples are artificial, but they demonstrate what they are intended to demonstrate — that your CustomError instance is coming through with full integrity.
Just create Result manually:
let result: Result<Model, CustomError>
if (number < 20) {
result = .failure(.somethingBadHappened)
} else {
result = .success(Model(value: number))
}
completion(result)