Combine: Convert Closure into Publisher - ios

How to convert:
func getResults(completion: ([Result]?, Error) -> Void)
Into
var resultsPublisher: AnyPublisher<[Result], Error>
Just a scheme how I see it is (this syntax doesn't exist):
var resultsPublisher: AnyPublisher<[Result], Error> {
let publisher: AnyPublisher = ... // init
getResults { results, error in
guard let results = results else {
publisher.produce(error: error) // this syntax doesn't exist
return
}
publisher.produce(results: results) // this syntax doesn't exist
}
return publisher
}
I need that because some 3d party SDKs use completion closures and I want to write extensions to them that return Publishers.

The answer is Future Publisher as matt explained:
var resultsPublisher: AnyPublisher<[Result], Error> {
// need deferred when want
// to start request only when somebody subscribes
// + without Deferred it's impossible to .retry() later
Deferred {
Future { promise in
self.getResults { results, error in
guard let results = results else {
promise(.failure(error))
return
}
promise(.success(results))
}
}
}
.eraseToAnyPublisher()
}

Related

Handle error in flatmap when using chaining of publishers

I am trying use Combine in my Swift application and have problem in my following code:
//Get it from local storage(realm)
voucherCodeStorageProvider.fetchVoucherCode(voucherId).flatMap { (code) -> AnyPublisher<String?, Error> in
if let code = code {
return Just(code).setFailureType(to: Error.self).eraseToAnyPublisher()
}
//If not found in storage, Get it from api
return self.voucherCodeProvider.fetchVoucherCode(voucherId: voucherId).handleEvents( receiveOutput: { code in
guard let code = code else { return }
_ = self.voucherCodeStorageProvider.saveVoucherCode(code, voucherId)
}).mapError{ $0 as Error }.eraseToAnyPublisher()
}.eraseToAnyPublisher()
Above fetchVoucherCode is currently publishing an error, now I want to catch that error and do task that I perform after nil check in my code. But I am not able to catch error here. How can I catch an error in flatmap and can perform some operation like I have above?
I did it using catch before flatmap. Below is my working code:
voucherCodeStorageProvider.fetchVoucherCode(voucherId).catch { _ in
return self.voucherCodeProvider.fetchVoucherCode(voucherId: voucherId).handleEvents( receiveOutput: { code in
guard let code = code else { return }
_ = self.voucherCodeStorageProvider.saveVoucherCode(code, voucherId)
}).mapError{ $0 as Error }.eraseToAnyPublisher()
}.flatMap { (code) -> AnyPublisher<String?, Error> in
return Just(code).setFailureType(to: Error.self).eraseToAnyPublisher()
}.eraseToAnyPublisher()

EXC_BAD_ACCESS when initializing Dictionary of CurrentValueSubject in Swift

I am trying to create a class that executes data loading once and returns the data to all callers of the method while the data was loading to not perform the data loading for the same item (identifier) more than once. The issue I am having is that it seems to crash on the first initialization of CurrentValueSubject for an identifier. This only happens if the downloadStuff returns an Error I have no idea what's wrong. Here is a reproduction of the issue.
Class that does the synchronization:
class FetchSynchronizer<T, ItemIdentifier: Hashable> {
typealias CustomParams = (isFirstLoad: Bool, result: Result<T, Error>)
enum FetchCondition {
// executes data fetching only once
case executeFetchOnlyOnce
// re-executes fetch if request failed
case retryOnlyIfFailure
// always executes fetch even if response is cached
case noDataCache
// custom condition
case custom((CustomParams) -> Bool)
}
struct LoadingState<T> {
let result: Result<T, Error>
let isLoading: Bool
init(result: Result<T, Error>? = nil, isLoading: Bool = false) {
self.result = result ?? .failure(NoResultsError())
self.isLoading = isLoading
}
}
private var cancellables = Set<AnyCancellable>()
private var isLoading: [ItemIdentifier: CurrentValueSubject<LoadingState<T>, Never>] = [:]
func startLoading(identifier: ItemIdentifier,
fetchCondition: FetchCondition = .executeFetchOnlyOnce,
loaderMethod: #escaping () async -> Result<T, Error>) async -> Result<T, Error> {
// initialize loading tracker for identifier on first execution
var isFirstExecution = false
if isLoading[identifier] == nil {
print("----0")
isLoading[identifier] = CurrentValueSubject<LoadingState<T>, Never>(LoadingState<T>())
isFirstExecution = true
}
guard let currentIsLoading = isLoading[identifier] else {
assertionFailure("Should never be nil because it's set above")
return .failure(NoResultsError())
}
if currentIsLoading.value.isLoading {
// loading in progress, wait for finish and call pending callbacks
return await withCheckedContinuation { continuation in
currentIsLoading.filter { !$0.isLoading }.sink { currentIsLoading in
continuation.resume(returning: currentIsLoading.result)
}.store(in: &cancellables)
}
} else {
// no fetching in progress, check if it can be executed
let shouldFetchData: Bool
switch fetchCondition {
case .executeFetchOnlyOnce:
// first execution -> fetch data
shouldFetchData = isFirstExecution
case .retryOnlyIfFailure:
// no cached data -> fetch data
switch currentIsLoading.value.result {
case .success:
shouldFetchData = false
case .failure:
shouldFetchData = true
}
case .noDataCache:
// always fetch
shouldFetchData = true
case .custom(let completion):
shouldFetchData = completion((isFirstLoad: isFirstExecution,
result: currentIsLoading.value.result))
}
if shouldFetchData {
currentIsLoading.send(LoadingState(isLoading: true))
// fetch data
return await withCheckedContinuation { continuation in
Task {
// execute loader method
let result = await loaderMethod()
let state = LoadingState(result: result,
isLoading: false)
currentIsLoading.send(state)
continuation.resume(returning: result)
}
}
} else {
// use existing data
return currentIsLoading.value.result
}
}
}
}
Example usage:
class Executer {
let fetchSynchronizer = FetchSynchronizer<Data?, String>()
func downloadStuff() async -> Result<Data?, Error> {
await fetchSynchronizer.startLoading(identifier: "1") {
return await withCheckedContinuation { continuation in
sleep(UInt32.random(in: 1...3))
print("-------request")
continuation.resume(returning: .failure(NSError() as Error))
}
}
}
init() {
start()
}
func start() {
Task {
await downloadStuff()
print("-----3")
}
DispatchQueue.global(qos: .utility).async {
Task {
await self.downloadStuff()
print("-----2")
}
}
DispatchQueue.global(qos: .background).async {
Task {
await self.downloadStuff()
print("-----1")
}
}
}
}
Start the execution:
Executer()
Crashes at
isLoading[identifier] = CurrentValueSubject<LoadingState<T>, Never>(LoadingState<T>())
Any guidance would be appreciated.
Swift Dictionary is not thread-safe.
You need to make sure it is being accessed from only one thread (i.e queue) or using locks.
EDIT - another solution suggested by #Bogdan the question writer is to make the class an actor class which the concurrency safety is taken care of by the compiler!
By dispatching to a global queue, you increase the chance that two threads will try and write into the dictionary “at the same time” which probably causes the crash
Take a look at these examples.
How to implement a Thread Safe HashTable (PhoneBook) Data Structure in Swift?
https://github.com/iThink32/Thread-Safe-Dictionary/blob/main/ThreadSafeDictionary.swift

Swift/iOS - How to use a value from one scope/function and pass it into another?

I am trying to pass the value of gyroX to another function but it just ends up in it having a value of 0 when I use it as gyroX in that other function.
Here is the code:
var gyroX = Float()
motion.startGyroUpdates(to: .main) { (data, error) in
if let myData = data {
gyroX = Float(myData.rotationRate.x)
}
}
With Xcode 13 Beta and Swift 5.5
This is a problem that we can now solve with Async/Await's Continuations
We would first make a function that converts the callback into an awaitable result like:
func getXRotation(from motion: CMMotionManager) async throws -> Float {
try await withCheckedThrowingContinuation { continuation in
class GyroUpdateFailure: Error {} // make error to throw
motion.startGyroUpdates(to: .main) { (data, error) in
if let myData = data {
continuation.resume(returning: Float(myData.rotationRate.x))
} else {
throw GyroUpdateFailure()
}
}
}
}
Then we can assign the variable and use it like so:
let gyroX = try await getXRotation(from: motion)
callSomeOtherFunction(with: gyroX)
With Xcode <= 12 and Combine
In the current release of Swift and Xcode we can use the Combine framework to make callback handling a little easier for us. First we'll convert the closure from the motion manager into a "Future". Then we can use that future in a combine chain.
func getXRotation(from motion: CMMotionManager) -> Future<CMGyroData, Error> {
Future { promise in
class GyroUpdateFailure: Error {} // make error to throw
motion.startGyroUpdates(to: .main) { (data, error) in
if let myData = data {
promise(.success(myData))
} else {
promise(.failure(GyroUpdateFailure()))
}
}
}
}
// This is the other function you want to call
func someOtherFunction(_ x: Float) {}
// Then we can use it like so
_ = getXRotation(from: motion)
.eraseToAnyPublisher()
.map { Float($0.rotationRate.x) }
.map(someOtherFunction)
.sink { completion in
switch completion {
case .failure(let error):
print(error.localizedDescription)
default: break
}
} receiveValue: {
print($0)
}
There are some important parts to the combine flow. The _ = is one of them. The result of "sinking" on a publisher is a "cancellable" object. If we don't store that in a local variable the system can clean up the task before it fishes executing. So you will want to do that for sure.
I highly recommend you checkout SwiftBySundell.com to learn more about Combine or Async/Await and RayWenderlich.com for mobile development in general.

PromiseKit 6 iOS chaining

I am trying to chain some API calls and I think I am confusing some concepts. Would love some clarification & code samples.
I have implemented these functions...
func promiseFetchPayments(for accountId: String) -> Promise <[OperationResponse]> {
return Promise <[OperationResponse]> { seal in
payments(for: accountId) { (records, error) in
if let recs = records {
seal.resolve(.fulfilled(recs))
return
}
if let e = error {
seal.reject(e)
return
}
}
}
}
and
func payments(for accountId: String, completion: #escaping (_ records: [OperationResponse]?, _ error: Error?) -> Void) {
stellar.payments.getPayments(
forAccount: accountId,
order: Order.descending,
limit: 10
) { response in
switch response {
case .success(let paymentsResponse):
DispatchQueue.main.async {
completion(paymentsResponse.records, nil)
}
case .failure(let error):
DispatchQueue.main.async {
completion(nil, error)
}
}
}
}
I am trying to use it like so:
firstly {
promiseFetchPayments(for: "XXX")
}.done { records in
print(records)
} .catch { error in
print(error)
}
Now this actually ^^^ works OK!!!! My problem is I want to be able to change done to then and be able to chain another function / response or many more.
But the error I keep getting is:
Cannot conform to Thenable.
I am looking for something very similar to this (I know the syntax isn't right just logically follow the chain....
firstly {
stellar.promiseFetchPayments(for: "")
}.done { records in
print(records)
}.then {
// call some other method
}.done { data in
// more data
}.catch { error in
print(error)
}
Is this actually possible? Can't seem to get any tutorials on the interwebs to compile. Seems Swift compiler really doesn't like PMK syntax or something.
Any ideas?
The problem is because you're chaining off of a done, which doesn't like that you're trying to then do a call to then off of that.
Instead, you'll want to save the promise and use it for the later calls. You can do something like this:
let promise = firstly {
stellar.promiseFetchPayments(for: "")
}
promise.done { records in
print(records)
}
promise.then {
// call some other method
}.done { data in
// more data
}.catch { error in
print(error)
}
You can even return that promise from a method to use in other places, or pass it around to another method.

Swift combine retry only for some error types

I have a custom pipeline where I want to have 3 retry attempt for some error codes which are recoverable plus I want to add some short delay for the recoverable error. Anyone has an idea how I can do it?
func createRequest(for message: Message) -> AnyPublisher<ResponseMessage, Error> {
Future<ResponseMessage, Error> { promise in
.....
}
.tryCatch({ error -> AnyPublisher<ResponseMessage, Error> in
// If error is a recoverable error retry, otherwise fail directly
if case let MessageBusError.messageError(responseError) = error {
if responseError.isRecoverable {
// Make a next attempt only for recoverable error
throw error
}
}
//Should fail directly if the error code is not recoverable
return Fail<ResponseMessage, Error>(error: error)
.eraseToAnyPublisher()
})
.retry(3)
.eraseToAnyPublisher()
}
Basically, you need a retryIf operator, so you can provide a closure to tell Combine which errors should be retried, and which not. I'm not aware of such an operator, but it's not hard to build one for yourself.
The idiomatic way is to extend the Publishers namespace with a new type for your operator, and then extend Publisher to add support for that operator so that yo can chain it along with other operators.
The implementation could look like this:
extension Publishers {
struct RetryIf<P: Publisher>: Publisher {
typealias Output = P.Output
typealias Failure = P.Failure
let publisher: P
let times: Int
let condition: (P.Failure) -> Bool
func receive<S>(subscriber: S) where S : Subscriber, Failure == S.Failure, Output == S.Input {
guard times > 0 else { return publisher.receive(subscriber: subscriber) }
publisher.catch { (error: P.Failure) -> AnyPublisher<Output, Failure> in
if condition(error) {
return RetryIf(publisher: publisher, times: times - 1, condition: condition).eraseToAnyPublisher()
} else {
return Fail(error: error).eraseToAnyPublisher()
}
}.receive(subscriber: subscriber)
}
}
}
extension Publisher {
func retry(times: Int, if condition: #escaping (Failure) -> Bool) -> Publishers.RetryIf<Self> {
Publishers.RetryIf(publisher: self, times: times, condition: condition)
}
}
Usage:
func createRequest(for message: Message) -> AnyPublisher<ResponseMessage, Error> {
Deferred {
Future<ResponseMessage, Error> { promise in
// future code
}
}
.retry(times: 3) { error in
if case let MessageBusError.messageError(responseError) = error, responseError.isRecoverable {
return true
}
return false
}
.eraseToAnyPublisher()
}
Note that I wrapped your Future within a Deferred one, otherwise the retry operator would be meaningless, as the closure will not be executed multiple times. More details about that behaviour here: Swift. Combine. Is there any way to call a publisher block more than once when retry?.
Alternatively, you can write the Publisher extension like this:
extension Publisher {
func retry(_ times: Int, if condition: #escaping (Failure) -> Bool) -> Publishers.RetryIf<Self> {
Publishers.RetryIf(publisher: self, times: times, condition: condition)
}
func retry(_ times: Int, unless condition: #escaping (Failure) -> Bool) -> Publishers.RetryIf<Self> {
retry(times, if: { !condition($0) })
}
}
, which enables some funky stuff, like this:
extension Error {
var isRecoverable: Bool { ... }
var isUnrecoverable: Bool { ... }
}
// retry at most 3 times while receiving recoverable errors
// bail out the first time when encountering an error that is
// not recoverable
somePublisher
.retry(3, if: \.isRecoverable)
// retry at most 3 times, bail out the first time when
// an unrecoverable the error is encountered
somePublisher
.retry(3, unless: \.isUnrecoverable)
Or even funkier, ruby-style:
extension Int {
var times: Int { self }
}
somePublisher
.retry(3.times, unless: \.isUnrecoverable)
Typically, I try to avoid building new publishers, and instead prefer to compose publishers from built-in operators. I found it to be rather tricky to do here. Maybe someone can suggest a better approach.
Retry resubscribes on any failure, so to trick it, I packaged any non-recoverable errors into a Result value containing the error, but leaving recoverable errors as failures to .retry; then eventually unpack the Result back into the corresponding value/error.
Here's how it would work in your case:
func createRequest(for message: Message)-> AnyPublisher<ResponseMessage, Error> {
Future<ResponseMessage, Error> { promise in
.....
}
// pack a value into Result
.map { v -> Result<ResponseMessage, Error> in .success(v) }
.tryCatch { error -> AnyPublisher<Result<ResponseMessage, Error>, Error> in
if case let MessageBusError.messageError(responseError) = error {
if responseError.isRecoverable {
// Keep recoverable errors as failures
throw error
}
}
// pack a non-recoverable error into Result with a failure
return Just(.failure(error)).setFailureType(Error.self)
.eraseToAnyPublisher()
}
.retry(3)
// unpack back
.flatMap { result in result.publisher }
.eraseToAnyPublisher()
}
For completeness, to extend Publisher with the above approach:
extension Publisher {
private func retryOnly<U: Publisher>(
upstream: U,
retries: Int,
when predicate: #escaping (U.Failure) -> Bool
) -> AnyPublisher<U.Output, U.Failure> {
upstream
.map { v -> Result<U.Output, U.Failure> in .success(v) }
.catch { err -> AnyPublisher<Result<U.Output, U.Failure>, U.Failure> in
if predicate(err) {
return Fail(error: err).eraseToAnyPublisher()
} else {
return Just(.failure(err))
.setFailureType(to: U.Failure.self)
.eraseToAnyPublisher()
}
}
.retry(retries)
.flatMap { result in result.publisher }
.eraseToAnyPublisher()
}
func retry(_ retries: Int, when predicate: #escaping (Failure) -> Bool)
-> AnyPublisher<Output, Failure> {
return retryOnly(upstream: self, retries: retries, when: predicate)
}
}
failingPublisher.retry(3, when: { $0 is RecoverableError })

Resources