I have a method that returns a Future:
func getItem(id: String) -> Future<MediaItem, Error> {
return Future { promise in
// alamofire async operation
}
}
I want to use it in another method and covert MediaItem to NSImage, which is a synchronous operation. I was hoping to simply do a map or flatMap on the original Future but it creates a long Publisher that I cannot erased to Future<NSImage, Error>.
func getImage(id: String) -> Future<NSImage, Error> {
return getItem(id).map { mediaItem in
// some sync operation to convert mediaItem to NSImage
return convertToNSImage(mediaItem) // this returns NSImage
}
}
I get the following error:
Cannot convert return expression of type 'Publishers.Map<Future<MediaItem, Error>, NSImage>' to return type 'Future<NSImage, Error>'
I tried using flatMap but with a similar error. I can eraseToAnyPublisher but I think that hides the fact that getImage(id: String returns a Future.
I suppose I can wrap the body of getImage in a future but that doesn't seem as clean as chaining and mapping. Any suggestions would be welcome.
You can't use dribs and drabs and bits and pieces from the Combine framework like that. You have to make a pipeline — a publisher, some operators, and a subscriber (which you store so that the pipeline will have a chance to run).
Publisher
|
V
Operator
|
V
Operator
|
V
Subscriber (and store it)
So, here, getItem is a function that produces your Publisher, a Future. So you can say
getItem (...)
.map {...}
( maybe other operators )
.sink {...} (or .assign(...))
.store (...)
Now the future (and the whole pipeline) will run asynchronously and the result will pop out the end of the pipeline and you can do something with it.
Now, of course you can put the Future and the Map together and then stop, vending them so someone else can attach other operators and a subscriber to them. You have now assembled the start of a pipeline and no more. But then its type is not going to be Future; it will be an AnyPublisher<NSImage,Error>. And there's nothing wrong with that!
You can always wrap one future in another. Rather than mapping it as a Publisher, subscribe to its result in the future you want to return.
func mapping(futureToWrap: Future<MediaItem, Error>) -> Future<NSImage, Error> {
var cancellable: AnyCancellable?
return Future<String, Error> { promise in
// cancellable is captured to assure the completion of the wrapped future
cancellable = futureToWrap
.sink { completion in
if case .failure(let error) = completion {
promise(.failure(error))
}
} receiveValue: { value in
promise(.success(convertToNSImage(mediaItem)))
}
}
}
This could always be generalized to
extension Publisher {
func asFuture() -> Future<Output, Failure> {
var cancellable: AnyCancellable?
return Future<Output, Failure> { promise in
// cancellable is captured to assure the completion of the wrapped future
cancellable = self.sink { completion in
if case .failure(let error) = completion {
promise(.failure(error))
}
} receiveValue: { value in
promise(.success(value))
}
}
}
}
Note above that if the publisher in question is a class, it will get retained for the lifespan of the closure in the Future returned. Also, as a future, you will only ever get the first value published, after which the future will complete.
Finally, simply erasing to AnyPublisher is just fine. If you want to assure you only get the first value (similar to getting a future's only value), you could just do the following:
getItem(id)
.map(convertToNSImage)
.eraseToAnyPublisher()
.first()
The resulting type, Publishers.First<AnyPublisher<Output, Failure>> is expressive enough to convey that only a single result will ever be received, similar to a Future. You could even define a typealias to that end (though it's probably overkill at that point):
typealias AnyFirst<Output, Failure> = Publishers.First<AnyPublisher<Output, Failure>>
Related
I am a RxSwift beginner and making a app with RxSwift + MVVM.
I have a method which calls API and converts to RxCocoa.Driver in ViewModel class like below.
func fetch() -> Driver<HomeViewEntity> {
apiUseCase.fetch(query: HomeViewQuery())
.map { data in
HomeViewEntity(userName: data.name,
emailAddress: data.email
}
.asDriver(onErrorRecover: { [weak self] error in
if let printableError = error as? PrintableError {
self?.errorMessageRelay.accept(AlertPayload(title: printableError.title, message: printableError.message))
}
return Driver.never()
})
}
Now, I'd like to call this fetchListPlace() method at regular intervals a.k.a polling (e.g. each 5 minutes) at ViewController.
How to do that????
I think interval is suit in this case, but I can't get an implementation image....
Here you go:
func example(_ fetcher: Fetcher) -> Driver<HomeViewEntity> {
Driver<Int>.interval(.seconds(5 * 60))
.flatMap { _ in fetcher.fetch() }
}
Also note,
Returning a Driver.never() from your recovery closure is probably a bad idea. Prefer Driver.empty() instead.
I'm not a fan of putting a side effect in the recovery closure in the first place. I think it would be better to have the fetch() return a Driver<Result<HomeViewEntity, Error>> instead and move the side effect to the end of the chain (in a subscribe or a flatMap.)
Here is a pseudo code of what I need to achieve:
func apiRequest1() -> Future<ResultType1, Error> { ... }
func apiRequest2() -> Future<ResultType2, Error> { ... }
func transform(res1: ResultType1, res2: ResultType2) -> ResultType3 { ... }
func combinedApiRequests() -> Future<ResultType3, Error> {
(resultType1, resultType2) = execute apiRequest1() and apiRequest2() asynchronously
resultType3 = transform(resultType1, resultType2)
return a Future publisher with resultType3
}
How would combinedApiRequests() look?
There's no need to return a Future publisher. Future publisher is a specific publisher, but as far as a downstream is concerned, a publisher is defined by its output and failure types. Instead, return a AnyPublisher<ResultType3, Error>.
Zip is a publisher that waits for all results to arrive to emit a value. This is probably what you'd need (more on this later). This is how your function could look:
func combinedApiRequests() -> AnyPublisher<ResultType3, Error> {
Publishers.Zip(apiRequest1, apiRequest2)
.map { transform(res1: $0, res2: $1) }
.eraseToAnyPublisher()
}
There is also CombineLatest publisher. For the first result from each upstream, it behaves the same as Zip, but for subsequent results it differs. In your case, it doesn't matter since Future is a one-shot publisher, but if the upstream publishers emitted multiple values, then you'd have to decide for your specific use case whether to use Zip - which always waits for all upstreams to emit a value before it emits a combined value, or CombineLatest - which emits with each new upstream value and combines it with the latest for other upstreams.
I know in general a publisher is more powerful than a closure, however I want to ask and discuss a specific example:
func getNotificationSettingsPublisher() -> AnyPublisher<UNNotificationSettings, Never> {
let notificationSettingsFuture = Future<UNNotificationSettings, Never> { (promise) in
UNUserNotificationCenter.current().getNotificationSettings { (settings) in
promise(.success(settings))
}
}
return notificationSettingsFuture.eraseToAnyPublisher()
}
I think this is a valid example of a Future publisher and it could be used here instead of using a completion handler. Let's do something with it:
func test() {
getNotificationSettingsPublisher().sink { (notificationSettings) in
// Do something here
}
}
This works, however it will tell me that the result of sink (AnyCancellable) is unused. So whenever I try to get a value, I need to either store the cancellable or assign it until I get a value.
Is there something like sinkOnce or an auto destroy of cancellables? Sometimes I don't need tasks to the cancelled. I could however do this:
func test() {
self.cancellable = getNotificationSettingsPublisher().sink { [weak self] (notificationSettings) in
self?.cancellable?.cancel()
self?.cancellable = nil
}
}
So once I receive a value, I cancel the subscription. (I could do the same in the completion closure of sink I guess).
What's the correct way of doing so? Because if I use a closure, it will be called as many times as the function is called, and if it is called only once, then I don't need to cancel anything.
Would you say normal completion handlers could be replaced by Combine and if so, how would you handle receiving one value and then cancelling?
Last but not least, the completion is called, do I still need to cancel the subscription? I at least need to update the cancellable and set it to nil right? I assume storing subscriptions in a set is for long running subscriptions, but what about single value subscriptions?
Thanks
Instead of using the .sink operator, you can use the Sink subscriber directly. That way you don't receive an AnyCancellable that you need to save. When the publisher completes the subscription, Combine cleans everything up.
func test() {
getNotificationSettingsPublisher()
.subscribe(Subscribers.Sink(
receiveCompletion: { _ in },
receiveValue: ({
print("value: \($0)")
})
))
}
My app has a status area at the top that shows progress information (similar to Xcode and iTunes). I want to update it by injecting side effects into an event stream, using a closure that converts the stream's value into the ProgressUpdate value. I'm using an extension on SignalProducer so any signal producer in my app can update the app's status area (there is a lot more involved to allow for multiple signals at once, but that doesn't affect this problem).
I'm basing it on SignalProducer's on(starting:, started:, ...). It requires the latest swift 3.1 beta to allow the constraint on the error type, but this is straight from a playground.
import ReactiveSwift
struct Rc2Error: Error {
}
struct ProgressUpdate {
let message: String
let value: Double = -1
}
class MacAppStatus {
fileprivate func process(event: (Event<ProgressUpdate, Rc2Error>) -> Void)
{
//update UI based on the event
}
}
extension SignalProducer where Error == Rc2Error {
func updateProgress<Value>(status: MacAppStatus, converter: #escaping (Value) -> ProgressUpdate) -> SignalProducer<Value, Error>
{
return SignalProducer<Value, Error> { observer, compositeDisposable in
self.startWithSignal { signal, disposable in
compositeDisposable += disposable
compositeDisposable += signal
.on(event: { (orignal) in
switch original {
case .completed:
status.process(Event<ProgressUpdate, Rc2Error>.completed)
case .interrupted:
status.process(Event<ProgressUpdate, Rc2Error>.interrupted)
case .failed(let err):
status.process(Event<ProgressUpdate, Rc2Error>.failed(err))
case .value(let val):
status.process(Event<ProgressUpdate, Rc2Error>.value(converter(val)))
}
})
.observe(observer)
}
}
}
}
```
The last line of .observe(observer) produces an error:
error: cannot convert value of type 'Observer<Value, Rc2Error>' to expected argument type 'Observer<_, Rc2Error>'
Any ideas why this conversion fails? Suggestions on a different way to accomplish this?
It looks like it was just bad error reporting from the compiler. The actual problem was that process() should take an Event, not a closure that takes an event. It also needed an empty external parameter name.
Changing the signature to
fileprivate func process(_ event: Event<ProgressUpdate, Rc2Error>)
and fixing the original typo Mike Taverne pointed out fixed it.
I'm still a reactive newbie and I'm looking for help.
func doA() -> Observable<Void>
func doB() -> Observable<Void>
enum Result {
case Success
case BFailed
}
func doIt() -> Observable<Result> {
// start both doA and doB.
// If both complete then emit .Success and complete
// If doA completes, but doB errors emit .BFailed and complete
// If both error then error
}
The above is what I think I want... The initial functions doA() and doB() wrap network calls so they will both emit one signal and then Complete (or Error without emitting any Next events.) If doA() completes but doB() errors, I want doIt() to emit .BFailed and then complete.
It feels like I should be using zip or combineLatest but I'm not sure how to know which sequence failed if I do that. I'm also pretty sure that catchError is part of the solution, but I'm not sure exactly where to put it.
--
As I'm thinking about it, I'm okay with the calls happening sequentially. That might even be better...
IE:
Start doA()
if it completes start doB()
if it completes emit .Success
else emit .BFailed.
else forward the error.
Thanks for any help.
I believe .flatMapLatest() is what you're looking for, chaining your observable requests.
doFirst()
.flatMapLatest({ [weak self] (firstResult) -> Observable<Result> in
// Assuming this doesn't fail and returns result on main scheduler,
// otherwise `catchError` and `observeOn(MainScheduler.instance)` can be used to correct this
// ...
// do something with result #1
// ...
return self?.doSecond()
}).subscribeNext { [weak self] (secondResult) -> Void in
// ...
// do something with result #2
// ...
}.addDisposableTo(disposeBag)
And here is .flatMapLatest() doc in RxSwift.
Projects each element of an observable sequence into a new sequence of observable sequences and then
transforms an observable sequence of observable sequences into an observable sequence producing values only from the most recent observable sequence. It is a combination of map + switchLatest operator.
I apologize that I don't know the syntax for swift, so I'm writing the answer in c#. The code should be directly translatable.
var query =
doA
.Materialize()
.Zip(doB.Materialize(), (ma, mb) => new { ma, mb })
.Select(x =>
x.ma.Kind == NotificationKind.OnError
|| x.mb.Kind == NotificationKind.OnError
? Result.BFailed
: Result.Success);
Basically the .Materialize() operator turns the OnNext, OnError, and OnCompleted notifications for an observable of type T into OnNext notifications for an observable of type Notification<T>. You can then .Zip(...) these and check for your required conditions.
I've learned RxSwift well enough to answer this question now...
func doIt() -> Observable<Result> {
Observable.zip(
doA().map { Result.Success },
doB().map { Result.Success }
.catch { _ in Observable.just(Result.BFailed) }
) { $1 }
}