Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

Swift Combine alternative to Rx Observable.create

Tags:

swift

combine

I have some code that is built using RxSwift, and I'm playing around with converting it to use Apple's Combine framework.

One pattern which is very common is the use of Observable.create for one-shot observables (usually network requests). Something like this:

func loadWidgets() -> Observable<[Widget]> {
  return Observable.create { observer in
    // start the request when someone subscribes
    let loadTask = WidgetLoader.request("allWidgets", completion: { widgets in
      // publish result on success
      observer.onNext(widgets)
      observer.onComplete()
    }, error: { error in
      // publish error on failure
      observer.onError()
    })
    // allow cancellation
    return Disposable {
      loadTask.cancel()
    }
  }
}

I'm trying to map that across to Combine and I haven't been able to quite figure it out. The closest I've been able to get is using Future for something like this:

func loadWidgets() -> AnyPublisher<[Widget], Error> {
  return Future<[Widget], Error> { resolve in
    // start the request when someone subscribes
    let loadTask = WidgetLoader.request("allWidgets", completion: { widgets in
      // publish result on success
      resolve(.success(widgets))
    }, error: { error in
      // publish error on failure
      resolve(.failure(error))
    })
    // allow cancellation ???
  }
}

As you can see, it does most of it, but there's no ability to cancel. Secondarily, future doesn't allow multiple results.

Is there any way to do something like the Rx Observable.create pattern which allows cancellation and optionally multiple results?

like image 577
Orion Edwards Avatar asked Oct 14 '19 20:10

Orion Edwards


People also ask

Is combine better than RxSwift?

RxSwift supports iOS 9 and higher, while Combine requires iOS 13 and higher. Both frameworks are available on all Apple platforms, but Combine still lacks Linux support. OpenCombine can come to the rescue in these cases, featuring the same API but with an open-source implementation and support for more platforms.

When should I use RxSwift?

RxSwift helps when you need to combine complex asynchronous chains. RxSwift also has types such as Subject, a kind of bridge between the imperative and declarative worlds. The subject can act as an Observable, and at the same time, it can be an Observer, i.e. accept objects and issue events.

What is difference between Swift and RxSwift?

Rx-Swift is a reactive programming library in iOS app development. It is a multi-platform standard, its difficult-to-handle asynchronous code in Swift, that becomes much easier in Rx-Swift. RxSwift makes easy to develop dynamic applications that respond to changes in data and respond to user events.

What is combine Swift used for?

Overview. The Combine framework provides a declarative Swift API for processing values over time. These values can represent many kinds of asynchronous events. Combine declares publishers to expose values that can change over time, and subscribers to receive those values from the publishers.


2 Answers

I think I found a way to mimic Observable.create using a PassthroughSubject in Combine. Here is the helper I made:

struct AnyObserver<Output, Failure: Error> {
    let onNext: ((Output) -> Void)
    let onError: ((Failure) -> Void)
    let onComplete: (() -> Void)
}

struct Disposable {
    let dispose: () -> Void
}

extension AnyPublisher {
    static func create(subscribe: @escaping (AnyObserver<Output, Failure>) -> Disposable) -> Self {
        let subject = PassthroughSubject<Output, Failure>()
        var disposable: Disposable?
        return subject
            .handleEvents(receiveSubscription: { subscription in
                disposable = subscribe(AnyObserver(
                    onNext: { output in subject.send(output) },
                    onError: { failure in subject.send(completion: .failure(failure)) },
                    onComplete: { subject.send(completion: .finished) }
                ))
            }, receiveCancel: { disposable?.dispose() })
            .eraseToAnyPublisher()
    }
}

And here is how it looks in usage:

func loadWidgets() -> AnyPublisher<[Widget], Error> {
    AnyPublisher.create { observer in
        let loadTask = WidgetLoader.request("allWidgets", completion: { widgets in
          observer.onNext(widgets)
          observer.onComplete()
        }, error: { error in
          observer.onError(error)
        })
        return Disposable {
          loadTask.cancel()
        }
    }
}
like image 64
ccwasden Avatar answered Oct 13 '22 00:10

ccwasden


From what I've learned, the support for initializing an AnyPublisher with a closure has been dropped in Xcode 11 beta 3. This would be a corresponding solution for Rx's Observable.create in this case, but for now I believe that the Future is a goto solution if you only need to propagate single value. In other cases I would go for returning a PassthroughSubject and propagating multiple values this way, but it will not allow you to start a task when the observation starts and I believe it's far from ideal compared to Observable.create.

In terms of cancellation, it does not have an isDisposed property similar to a Disposable, so it's not possible to directly check the state of it and stop your own tasks from executing. The only way that I can think of right now would be to observe for a cancel event, but it's surely not as comfortable as a Disposable. Also, I'd assume that cancel might in fact stop tasks like network requests from URLSession based on the docs here: https://developer.apple.com/documentation/combine/cancellable

like image 2
Eluss Avatar answered Oct 12 '22 22:10

Eluss