Resolve combine future outside of future's closure - ios

I'm using Future to create a publisher that returns a single value from outside of the future creation closure. For this, I store the promise property locally:
return Future<Something, Never> { promise in
self.somePromise = promise
}.eraseToAnyPublisher()
Then after some work is finished I resolve the promise:
self.somePromise(.success(data))
I've searched the documentation and haven't found any references, is this a correct usage or is there a better way to do it?
To put it more into context, my idea is to have some kind of "broker" class, that returns promises and that at some time in the future will publish a result:
class FruitBroker {
// MARK: - Private Properties
private var applePromise: Future<[Apple], Never>.Promise!
private var orangePromise: Future<[Orange], Never>.Promise!
// MARK: - Methods
func getApples() -> AnyPublisher<[Apple], Never> {
return Future<[Apple], Never> { promise in
self.applePromise = promise
}.eraseToAnyPublisher()
}
func getBananas() -> AnyPublisher<[Orange], Never> {
return Future<[Orange], Never> { promise in
self.orangePromise = promise
}.eraseToAnyPublisher()
}
// MARK: - Private Methods
private func onHarvestFinished() {
applePromise(.success(apples))
orangePromise(.success(oranges))
}
}

This looks like a situation where you should just use PassthroughSubject or CurrentValueSubject:
class FruitBroker {
var applePublisher = PassthroughSubject<[Apple],Never>()
var orangePublisher = PassthroughSubject<[Orange],Never>()
private func onHarvestFinished(apples: [Apple], oranges: [Orange]) {
applePublisher.send(apples)
orangePublisher.send(oranges)
}
}

is this a correct usage
No. What you're doing is not Combine. Combine is publisher -> operators -> subscriber, where this path constitutes a pipeline. You have no pipeline. You're just storing a method and later calling it; you don't need a Combine Future to do that.

Related

Combine bind Publisher to PassthroughSubject

This question has effectively been asked before (Combine assign publisher to PassthroughSubject) but the answer assumed the architecture of the question's example was totally wrong.
I'm faced with the same issue, in what I feel is a different example case, and I'd dearly love clarity on the issue.
I have a viewmodel class which provides a public errorMessage publisher for the client layer to do what it will with. Just a string here. Privately this publisher is backed by a PassthroughSubject - I'm "entering the monad" from different places internally and so I want to send error messages imperatively at that stage.
One of my entry points here happens to be another Combine publisher. I'd like to just map and bind, as I would in RxSwift:
private let _errorMessageSubject = PublishSubject<String>()
public let errorMessageRail = _errorMessageSubject.asObservable()
private func setup() {
...
myEngine.errorMessages
.map { msg in "Internal error: \(msg)" }
.bind(to: _errorMessageSubject)
...
}
private func someOtherMethod() {
_errorMessageSubject.onNext("Surprise")
}
In Combine I'm not sure how to do it other than:
private let _errorMessageSubject = PassthroughSubject<String,Never>()
public let errorMessageRail = _errorMessageSubject.eraseToAnyPublisher()
private func setup() {
...
myEngine.errorMessages
.map { msg in "Internal error: \(msg)" }
.sink { [weak self] msg in
self?._errorMessageSubject.send(msg)
}
...
}
private func someOtherMethod() {
_errorMessageSubject.send("Surprise")
}
Before we get into chatting concurrency issues, let's say I'm always carefully pushing to _errorMessageSubject on a specific dispatch queue. I omit that from the code above for clarity.
So given this example, unless I'm missing something staggeringly obvious a flatMap won't help me here.
Am I stuck with this sink -> send dance?
Or is there some eye-watering code-smell about my public/private publisher/subject pattern (that I use a lot for bridging imperative with reactive architectures) and can some kind soul point me in the direction of self-improvement?
It sounds like what you want your rail to be the merge of _errorMessageSubject and another publisher. So I'll make errorMessageRail a variable so it can be changed (by this class only) after it's initialized:
public private(set) var errorMessageRail = _errorMessageSubject.eraseToAnyPublisher()
Then, in setup, you change the rail so it includes the additional stream:
func setup() {
...
errorMessageRail = _errorMessageSubject.merge( with:
myEngine.errorMessages
.map { msg in "Internal error: \(msg)" }
).eraseToAnyPublisher()
}

Deriving Publisher from different publishers in Combine

I have a property with a fixed type
var statusPublisher: Published<Status>.Publisher
and some other statuses like substatus1, substatus2
and I want it to combine values of other statuses like
var statusPublisher: Published<Status>.Publisher {
$substatus1.combineLatest($substatus2).map { s1, s2 -> Status in ... }
}
but it says Cannot convert return expression of type 'Publishers.CombineLatest<Published<Substatus1>.Publisher, Published<Substatus2>.Publisher>' to return type 'Published<Status>.Publisher'
I managed to workaround it with one extra
var connectionStatusSubject = PassthroughSubject<Status, Never>()
that I put into
var connectionStatusPublisher: AnyPublisher<Status, Never> {
connectionStatusSubject.eraseToAnyPublisher()
}
with new AnyPublisher here, but it seems not so neat.
Is there any way to make it the right way?
Published<T>.Publisher is just a specific type of a publisher, created by #Published property wrapper. There's no reason at all to use that as the type of the computed property.
Type-erase to AnyPublisher - like you did with PassthroughSubject - except you don't need a PassthroughSubject:
var statusPublisher: AnyPublisher<Status, Never> {
$substatus1
.combineLatest($substatus2)
.map { s1, s2 -> Status in ... }
.eraseToAnyPublisher()
}

Why this produces compiler error in Combine?

just trying to implement SwiftUI and Combine in my new project.
But stuck in this:
func task() -> AnyPublisher<Int, Error> {
return AnyPublisher { subscriber in
subscriber.receive(Int(arc4random()))
subscriber.receive(completion: .finished)
}
}
This produces the following compiler error:
Type '(_) -> ()' does not conform to protocol 'Publisher'
Why?
Update
Actually Random here is just as an example. The real code will look like this:
func task() -> AnyPublisher<SomeCodableModel, Error> {
return AnyPublisher { subscriber in
BackendCall.MakeApiCallWithCompletionHandler { response, error in
if let error == error {
subscriber.receive(.failure(error))
} else {
subscriber.receive(.success(response.data.filter))
subscriber.receive(.finished)
}
}
}
}
Unfortunately, I don't have access to BackendCall API since it is private.
It's kind of pseudocode but, it pretty close to the real one.
You cannot initialise an AnyPublisher with a closure accepting a Subscriber. You can only initialise an AnyPublisher from a Publisher. If you want to create a custom Publisher that emits a single random Int as soon as it receives a subscriber and then completes, you can create a custom type conforming to Publisher and in the required method, receive(subscriber:), do exactly what you were doing in your closure.
struct RandomNumberPublisher: Publisher {
typealias Output = Int
typealias Failure = Never
func receive<S>(subscriber: S) where S : Subscriber, Failure == S.Failure, Output == S.Input {
subscriber.receive(Int.random(in: 0...Int.max))
subscriber.receive(completion: .finished)
}
}
Then in your task method, you simply need to create a RandomNumberPublisher and then type erase it.
func task() -> AnyPublisher<Int, Never> {
return RandomNumberPublisher().eraseToAnyPublisher()
}
If all you want is a single random value, use Just
fun task() -> AnyPublisher<Int, Never> {
return Just(Int.random(in: 0...Int.max)).eraseToAnyPublisher()
}
Sidenote: don't use Int(arc4random()) anymore.
You're likely better off wrapping this in a Future publisher, possibly also wrapped with Deferred if you want it to response when subscriptions come in. Future is an excellent way to wrap external async API calls, especially ones that you can't fully control or otherwise easily adapt.
There's an example in Using Combine for "wrapping an async call with a Future to create a one-shot publisher" that looks like it might map quite closely to what you're trying to do.
If you want it to return more than a single value, then you may want to compose something out of PassthoughSubject or CurrentValueSubject that gives you an interface of -> AnyPublisher<YourType, Error> (or whatever you're looking for).

Run 3 observables sequentially, using result from first in the last one

CONTEXT
I would like to run 3 different operations sequentially using RxSwift:
Fetch products
When products fetching is done, delete cache
When cache delete is done, save new cache with products from step 1
These are the function definitions in my services:
struct MyService {
static func fetchProducts() -> Observable<[Product]> {...}
static func deleteCache() -> Observable<Void> {...}
static func saveCache(_ products: [Product]) -> Observable<Void> {...}
}
I implement that behavior usually with flatMapLatest.
However, I will lose the result of the 1st observable ([Product]) with that approach, because the operation in the middle (deleteCache) doesn't receive arguments and returns Void when completed.
struct CacheViewModel {
static func refreshCache() -> Observable<Void> {
return MyService.fetchProducts()
.flatMapLatest { lostProducts in MyService.deleteCache() }
.flatMapLatest { MyService.saveCache($0) } // Compile error*
}
// * Cannot convert value of type 'Void' to expected argument type '[Product]'
}
The compile error is absolutely fair, since the operation in the middle 'breaks' the passing chain for the first result.
QUESTION
What mechanism is out there to achieve this serial execution with RxSwift, accumulating results of previous operations?
service
.fetchProducts()
.flatMap { products in
return service
.deleteCache()
.flatMap {
return service
.saveCache(products)
}
}
The easiest solution would be, just to return a new Observable of type Observable<Products> using the static method in the Rx framework just within the second flatMap(), passing in the lostProducts you captured in the flatmap-closure, i.e.:
static func refreshCache() -> Observable<Void> {
return MyService.fetchProducts()
.flatMapLatest { lostProducts -> Observable<[Product]> in
MyService.deleteCache()
return Observable.just(lostProducts)
}
.flatMapLatest { MyService.saveCache($0) } // No compile error
}
That way you are not losing the result of the first call in the flatMap, but just pass it through after having cleared the cache.
you can use do(onNext:) for deleting the cache data and then in flatMapLatest you can save the products. Optionally SaveCache and DeleteCache should return Completable so that you can handle error if the save or delete operation failed.
struct CacheViewModel {
static func refreshCache() -> Observable<Void> {
return MyService.fetchProducts()
.do(onNext: { _ in
MyService.deleteCache()
}).flatMap { products in
MyService.saveCache(products)
}
}
}

Calling async function in lazy var property

Is there a way to call an async function from a lazy or computed property?
struct Item {
lazy var name: String = {
API.requestThing({ (string: String) in // Xcode didn't like this
return string // this would not work here
})
}()
}
class API {
class func requestThing(completion: String -> Void) {
completion("string")
}
}
Your completion handler in API.requestThing returns a String, yet it is supposed to have no return value:
(completion: String -> Void)
I got this to work:
struct Item {
lazy var name: String = {
API.requestThing({ (string: String) in
return string
})
}()
}
class API {
class func requestThing(completion: String -> String) -> String {
return completion("string")
}
}
There is no good reason to use "lazy" in this case. lazy is for initialization. Just create a normal func and pass a completion handler.
First, requestThing returns () (ie void) and not String. So the type of the following expression is also () and not String:
API.requestThing { string in
return string
}
Second, the call to requestThing is asynchronous, so even if you defined name as a lazy var, the call to the var body function is still synchronousand will return immediately.
So if you can transform name into a function like this:
func name(completion: String -> ()) {
API.requestThing { string in
completion(string)
}
}
// Later you call it in this way
myItem.name { name in
// use the value of name
}
If in addition you want to cache the retrieved value you can modify Item to a class and use the following code
class Item {
private var nameValue: String?
func name(completion: String -> ()) {
if let value = nameValue {
// return cached value
return completion(value)
} else {
// request value
API.requestThing { string in
// cache retrieved value
self.nameValue = string
// return retrieved value
completion(string)
}
}
}
}
There's probably no compelling reason to do this, but the following approach seems to be reasonable:
Instead having a variable of type String - we sometimes require a "Future" of that thing, e.g. Future<String>. A future represents the eventual result of an asynchronous operation - that is exactly what's given in your question.
The future itself is a "normal" variable and can be lazy evaluated, too. It just doesn't yet have its eventual value. That means, the underlying task will only be started when explicitly requested (e.g. lazily). From a design or architectural point of view, this may make sense.
func fetchString() -> Future<String> { ... }
lazy var name: Future<String> = fetchString()
Later in your code, you obtain the variable as follows:
item.name.map { string in
print(string)
}
If this is the first access to the lazy property, it will start the underlying asynchronous operation which calculates the string. Then, when the variable is available, the mapping function as provided in the map function will be called with the variable as an argument - possibly some time later, too.
Otherwise (if this is not the first access), it will just provide the string in the parameter when it is available, possibly immediately.
Since operations may fail, a "Future" also provides means to handle this:
item.name.map { string in
print(string)
}.onFailure { error in
print("Error: \(error)")
}
See also: https://en.wikipedia.org/wiki/Futures_and_promises
There are implementations for Futures in Swift and Objective-C, also often called "Promise".

Resources