How to replicate PromiseKit-style chained async flow using Combine + Swift - swift

I was using PromiseKit successfully in a project until Xcode 11 betas broke PK v7. In an effort to reduce external dependencies, I decided to scrap PromiseKit. The best replacement for handling chained async code seemed to be Futures using the new Combine framework.
I am struggling to replicate the simple PK syntax using Combine
ex. simple PromiseKit chained async call syntax
getAccessCodeFromSyncProvider.then{accessCode in startSync(accessCode)}.then{popToRootViewController}.catch{handleError(error)}
I understand:
A Swift standard library implementation of async/await would solve this problem (async/await does not yet exist, despite lots of chatter and involvement from Chris Latter himself)
I could replicate using Semaphores (error-prone?)
flatMap can be used to chain Futures
The async code I'd like should be able to be called on demand, since it's involved with ensuring user is logged in. I'm wrestling with two conceptual problems.
If I wrap Futures in a method, with sink to handle result, it seems that the method goes out of scope before subscriber is called by sink.
Since Futures execute only once, I worry that if I call the method multiple times I'll only get the old, stale, result from the first call. To work around this, maybe I would use a PassthroughSubject? This allows the Publisher to be called on demand.
Questions:
Do I have to retain every publisher and subscriber outside of the
calling method
How can I replicate simple chained async using the Swift standard library and then embed this in a swift instance method I can call on-demand to restart the chained async calls from the top??
//how is this done using Combine?
func startSync() {
getAccessCodeFromSyncProvider.then{accessCode in startSync(accessCode)}.catch{\\handle error here}
}

This is not a real answer to your whole question — only to the part about how to get started with Combine. I'll demonstrate how to chain two asynchronous operations using the Combine framework:
print("start")
Future<Bool,Error> { promise in
delay(3) {
promise(.success(true))
}
}
.handleEvents(receiveOutput: {_ in print("finished 1")})
.flatMap {_ in
Future<Bool,Error> { promise in
delay(3) {
promise(.success(true))
}
}
}
.handleEvents(receiveOutput: {_ in print("finished 2")})
.sink(receiveCompletion: {_ in}, receiveValue: {_ in print("done")})
.store(in:&self.storage) // storage is a persistent Set<AnyCancellable>
First of all, the answer to your question about persistence is: the final subscriber must persist, and the way to do this is using the .store method. Typically you'll have a Set<AnyCancellable> as a property, as here, and you'll just call .store as the last thing in the pipeline to put your subscriber in there.
Next, in this pipeline I'm using .handleEvents just to give myself some printout as the pipeline moves along. Those are just diagnostics and wouldn't exist in a real implementation. All the print statements are purely so we can talk about what's happening here.
So what does happen?
start
finished 1 // 3 seconds later
finished 2 // 3 seconds later
done
So you can see we've chained two asynchronous operations, each of which takes 3 seconds.
How did we do it? We started with a Future, which must call its incoming promise method with a Result as a completion handler when it finishes. After that, we used .flatMap to produce another Future and put it into operation, doing the same thing again.
So the result is not beautiful (like PromiseKit) but it is a chain of async operations.
Before Combine, we'd have probably have done this with some sort of Operation / OperationQueue dependency, which would work fine but would have even less of the direct legibility of PromiseKit.
Slightly more realistic
Having said all that, here's a slightly more realistic rewrite:
var storage = Set<AnyCancellable>()
func async1(_ promise:#escaping (Result<Bool,Error>) -> Void) {
delay(3) {
print("async1")
promise(.success(true))
}
}
func async2(_ promise:#escaping (Result<Bool,Error>) -> Void) {
delay(3) {
print("async2")
promise(.success(true))
}
}
override func viewDidLoad() {
print("start")
Future<Bool,Error> { promise in
self.async1(promise)
}
.flatMap {_ in
Future<Bool,Error> { promise in
self.async2(promise)
}
}
.sink(receiveCompletion: {_ in}, receiveValue: {_ in print("done")})
.store(in:&self.storage) // storage is a persistent Set<AnyCancellable>
}
As you can see, the idea that is our Future publishers simply have to pass on the promise callback; they don't actually have to be the ones who call them. A promise callback can thus be called anywhere, and we won't proceed until then.
You can thus readily see how to replace the artificial delay with a real asynchronous operation that somehow has hold of this promise callback and can call it when it completes. Also my promise Result types are purely artificial, but again you can see how they might be used to communicate something meaningful down the pipeline. When I say promise(.success(true)), that causes true to pop out the end of the pipeline; we are disregarding that here, but it could be instead a downright useful value of some sort, possibly even the next Future.
(Note also that we could insert .receive(on: DispatchQueue.main) at any point in the chain to ensure that what follows immediately is started on the main thread.)
Slightly neater
It also occurs to me that we could make the syntax neater, perhaps a little closer to PromiseKit's lovely simple chain, by moving our Future publishers off into constants. If you do that, though, you should probably wrap them in Deferred publishers to prevent premature evaluation. So for example:
var storage = Set<AnyCancellable>()
func async1(_ promise:#escaping (Result<Bool,Error>) -> Void) {
delay(3) {
print("async1")
promise(.success(true))
}
}
func async2(_ promise:#escaping (Result<Bool,Error>) -> Void) {
delay(3) {
print("async2")
promise(.success(true))
}
}
override func viewDidLoad() {
print("start")
let f1 = Deferred{Future<Bool,Error> { promise in
self.async1(promise)
}}
let f2 = Deferred{Future<Bool,Error> { promise in
self.async2(promise)
}}
// this is now extremely neat-looking
f1.flatMap {_ in f2 }
.receive(on: DispatchQueue.main)
.sink(receiveCompletion: {_ in}, receiveValue: {_ in print("done")})
.store(in:&self.storage) // storage is a persistent Set<AnyCancellable>
}

matt's answer is correct, use flatMap to chain promises. I got in the habit of returning promises when using PromiseKit, and carried it over to Combine (returning Futures).
I find it makes the code easier to read. Here's matt's last example with that recommendation:
var storage = Set<AnyCancellable>()
func async1() -> Future<Bool, Error> {
Future { promise in
delay(3) {
print("async1")
promise(.success(true))
}
}
}
func async2() -> Future<Bool, Error> {
Future { promise in
delay(3) {
print("async2")
promise(.success(true))
}
}
}
override func viewDidLoad() {
print("start")
async1()
.flatMap { _ in async2() }
.receive(on: DispatchQueue.main)
.sink(receiveCompletion: {_ in}, receiveValue: {_ in print("done")})
.store(in:&self.storage) // storage is a persistent Set<AnyCancellable>
}
Note that AnyPublisher will work as a return value as well, so you could abstract away the Future and have it return AnyPublisher<Bool, Error> instead:
func async2() -> AnyPublisher<Bool, Error> {
Future { promise in
delay(3) {
print("async2")
promise(.success(true))
}
}.eraseToAnyPubilsher()
}

Also if you want to use the PromiseKit-like syntax, here are some extensions for Publisher
I am using this to seamlessly switch from PromiseKit to Combine in a project
extension Publisher {
func then<T: Publisher>(_ closure: #escaping (Output) -> T) -> Publishers.FlatMap<T, Self>
where T.Failure == Self.Failure {
flatMap(closure)
}
func asVoid() -> Future<Void, Error> {
return Future<Void, Error> { promise in
let box = Box()
let cancellable = self.sink { completion in
if case .failure(let error) = completion {
promise(.failure(error))
} else if case .finished = completion {
box.cancellable = nil
}
} receiveValue: { value in
promise(.success(()))
}
box.cancellable = cancellable
}
}
#discardableResult
func done(_ handler: #escaping (Output) -> Void) -> Self {
let box = Box()
let cancellable = self.sink(receiveCompletion: {compl in
if case .finished = compl {
box.cancellable = nil
}
}, receiveValue: {
handler($0)
})
box.cancellable = cancellable
return self
}
#discardableResult
func `catch`(_ handler: #escaping (Failure) -> Void) -> Self {
let box = Box()
let cancellable = self.sink(receiveCompletion: { compl in
if case .failure(let failure) = compl {
handler(failure)
} else if case .finished = compl {
box.cancellable = nil
}
}, receiveValue: { _ in })
box.cancellable = cancellable
return self
}
#discardableResult
func finally(_ handler: #escaping () -> Void) -> Self {
let box = Box()
let cancellable = self.sink(receiveCompletion: { compl in
if case .finished = compl {
handler()
box.cancellable = nil
}
}, receiveValue: { _ in })
box.cancellable = cancellable
return self
}
}
fileprivate class Box {
var cancellable: AnyCancellable?
}
And here's an example of use:
func someSync() {
Future<Bool, Error> { promise in
delay(3) {
promise(.success(true))
}
}
.then { result in
Future<String, Error> { promise in
promise(.success("111"))
}
}
.done { string in
print(string)
}
.catch { err in
print(err.localizedDescription)
}
.finally {
print("Finished chain")
}
}

You can use this framework for Swift coroutines, it's also can be used with Combine - https://github.com/belozierov/SwiftCoroutine
DispatchQueue.main.startCoroutine {
let future: Future<Bool, Error>
let coFuture = future.subscribeCoFuture()
let bool = try coFuture.await()
}

Related

Swift 5.5 test async Task in init

I would like to test if my init function works as expected. There is an async call in the init within a Task {} block. How can I make my test wait for the result of the Task block?
class ViewModel: ObservableObject {
#Published private(set) var result: [Item]
init(fetching: RemoteFetching) {
self.result = []
Task {
do {
let result = try await fetching.fetch()
self.result = result // <- need to do something with #MainActor?
} catch {
print(error)
}
}
}
}
Test:
func testFetching() async {
let items = [Item(), Item()]
let fakeFetching = FakeFetching(returnValue: items)
let vm = ViewModel(fetching: FakeFetching())
XCTAssertEqual(vm.result, [])
// wait for fetching, but how?
XCTAssertEqual(vm.result, items])
}
I tried this, but setting the items, only happens after the XCTWaiter. The compiler warns that XCTWaiter cannot be called with await, because it isn't async.
func testFetching() async {
let items = [Item(), Item()]
let fakeFetching = FakeFetching(returnValue: items)
let expectation = XCTestExpectation()
let vm = ViewModel(fetching: FakeFetching())
XCTAssertEqual(vm.result, [])
vm.$items
.dropFirst()
.sink { value in
XCTAssertEqual(value, items)
expectation.fulfill()
}
.store(in: &cancellables)
let result = await XCTWaiter.wait(for: [expectation], timeout: 1)
XCTAssertEqual(result, .completed)
}
Expectation-and-wait is correct. You're just using it wrong.
You are way overthinking this. You don't need an async test method. You don't need to call fulfill yourself. You don't need a Combine chain. Simply use a predicate expectation to wait until vm.result is set.
Basically the rule is this: Testing an async method requires an async test method. But testing the asynchronous "result" of a method that happens to make an asynchronous call, like your init method, simply requires good old-fashioned expectation-and-wait test.
I'll give an example. Here's a reduced version of your code; the structure is essentially the same as what you're doing:
protocol Fetching {
func fetch() async -> String
}
class MyClass {
var result = ""
init(fetcher: Fetching) {
Task {
self.result = await fetcher.fetch()
}
}
}
Okay then, here's how to test it:
final class MockFetcher: Fetching {
func fetch() async -> String { "howdy" }
}
final class MyLibraryTests: XCTestCase {
let fetcher = MockFetcher()
func testMyClassInit() {
let subject = MyClass(fetcher: fetcher)
let expectation = XCTNSPredicateExpectation(
predicate: NSPredicate(block: { _, _ in
subject.result == "howdy"
}), object: nil
)
wait(for: [expectation], timeout: 2)
}
}
Extra for experts: A Bool predicate expectation is such a common thing to use, that it will be found useful to have on hand a convenience method that combines the expectation, the predicate, and the wait into a single package:
extension XCTestCase {
func wait(
_ condition: #escaping #autoclosure () -> (Bool),
timeout: TimeInterval = 10)
{
wait(for: [XCTNSPredicateExpectation(
predicate: NSPredicate(block: { _, _ in condition() }), object: nil
)], timeout: timeout)
}
}
The outcome is that, for example, the above test code can be reduced to this:
func testMyClassInit() {
let subject = MyClass(fetcher: fetcher)
wait(subject.result == "howdy")
}
Convenient indeed. In my own code, I often add an explicit assert, even when it is completely redundant, just to make it perfectly clear what I'm claiming my code does:
func testMyClassInit() {
let subject = MyClass(fetcher: fetcher)
wait(subject.result == "howdy")
XCTAssertEqual(subject.result, "howdy") // redundant but nice
}
Tnx to matt this is the correct way. No need for async in the test function and just using a predicate did the job.
func testFetching() {
let items = [Item(), Item()]
let fakeFetching = FakeFetching(returnValue: items)
let expectation = XCTestExpectation()
let vm = ViewModel(fetching: FakeFetching())
let pred = NSPredicate { _, _ in
vm.items == items
}
let expectation = XCTNSPredicateExpectation(predicate: pred, object: vm)
wait(for: [expectation], timeout: 1)
}
Slight variation on Matt's excellent answer. In my case, I've broken out his extension method into even more granular extensions for additional convenience.
Helper Framework
public typealias Predicate = () -> Bool
public extension NSPredicate {
convenience init(predicate: #escaping #autoclosure Predicate) {
self.init{ _, _ in predicate() }
}
}
public extension XCTNSPredicateExpectation {
convenience init(predicate: #escaping #autoclosure Predicate, object: Any) {
self.init(predicate: NSPredicate(predicate: predicate()), object: object)
}
convenience init(predicate: #escaping #autoclosure Predicate) {
self.init(predicate: NSPredicate(predicate: predicate()))
}
convenience init(predicate: NSPredicate) {
self.init(predicate: predicate, object: nil)
}
}
public extension XCTestCase {
func XCTWait(for condition: #escaping #autoclosure Predicate, timeout: TimeInterval = 10) {
let expectation = XCTNSPredicateExpectation(predicate: condition())
wait(for: [expectation], timeout: timeout)
}
}
With the above in place, the OP's code can be reduced to this...
Unit Test
func testFetching() {
let items = [Item(), Item()]
let fakeFetching = FakeFetching(returnValue: items)
let vm = ViewModel(fetching: FakeFetching())
XCTWait(for: vm.items == items, timeout: 1)
}
Notes on Naming
Above, I'm using a somewhat controversial name in calling my function XCTWait. This is because the XCT prefix should be considered reserved for Apple's XCTest framework. However, the decision to name it this way stems from the desire to improve its discoverability. By naming it as such, when a developer types XCT In their code editor, XCTWait is now presented as one of the offered auto-complete entries** making finding and using much more likely.
However, some purists may frown on this approach, citing if Apple ever added something named similar, this code may suddenly break/stop working (although unlikely unless the signatures also matched.)
As such, use such namings at your own discretion. Alternately, simply rename it to something you prefer/that meets your own naming standards.
(** Provided it is in the same project or in a library/package they've imported somewhere above)

Swift Combine return result of first future after evaluation of second

I have the following situation:
2 futures, one returns a value I am interested in, the other does some operations and returns void. The 2 are not related to each other (so the code should not be mixed), but both need to be executed in the right order in the application logic.
What I want to do is subscribe to a publisher that does the following:
future one executes and gives a value
future two executes and returns nothing
the subscriber receives the value of future one after the execution of future two.
Here is a small code example that does not compile, that shows what I would like to achieve:
import Combine
func voidFuture() -> Future<Void, Error> {
return Future<Void, Error> { promise in
promise(.success(()))
}
}
func intFuture() -> Future<Int, Error> {
return Future<Int, Error> { promise in
promise(.success(1))
}
}
func combinedFuture() -> AnyPublisher<Int, Error> {
var intValue: Int!
return intFuture().flatMap { result in
intValue = result
return voidFuture()
}.flatMap{ _ in
return CurrentValueSubject(intValue).eraseToAnyPublisher()
}.eraseToAnyPublisher()
}
var subscriptions = Set<AnyCancellable>()
combinedFuture()
.sink(receiveCompletion: { _ in }, receiveValue: { val in print(val)})
.store(in: &subscriptions)
You need to .map the Void result of the second publisher (voidFuture) back to the result of the first publisher (intFuture), which is what the .flatMap would emit:
func combinedFuture() -> AnyPublisher<Int, Error> {
intFuture().flatMap { result in
voidFuture().map { _ in result }
}
.eraseToAnyPublisher()
}

Publisher emitting progress of operation and final value

Given I have an SDK which provides the functionality below
class SDK {
static func upload(completion: #escaping (Result<String, Error>) -> Void) {
DispatchQueue.main.asyncAfter(deadline: .now() + 1) {
completion(.success("my_value"))
}
}
}
I am able to a create a wrapper around to make its usage more functional
class CombineSDK {
func upload() -> AnyPublisher<String, Error> {
Future { promise in
SDK.upload { result in
switch result {
case .success(let key):
promise(.success(key))
case .failure(let error):
promise(.failure(error))
}
}
}.eraseToAnyPublisher()
}
}
Now I'm trying to understand how my CombineSDK.upload method should look like if the SDK upload method also provides a progress block like below:
class SDK {
static func upload(progress: #escaping (Double) -> Void, completion: #escaping (Result<String, Error>) -> Void) {
DispatchQueue.main.asyncAfter(deadline: .now() + 0.5) {
progress(0.5)
}
DispatchQueue.main.asyncAfter(deadline: .now() + 1) {
progress(1)
}
DispatchQueue.main.asyncAfter(deadline: .now() + 1) {
completion(.success("s3Key"))
}
}
}
We need an Output type for your publisher that represents either the progress, or the final value. So we should use an enum. Since the Foundation framework already defines a type named Progress, we'll name ours Progressable to avoid a name conflict. We might as well make it generic:
enum Progressable<Value> {
case progress(Double)
case value(Value)
}
Now we need to think about how the publisher should behave. A typical publisher, like URLSession.DataTaskPublisher, doesn't do anything until it gets a subscription, and it starts its work fresh for each subscription. The retry operator only works if the upstream publisher behaves like this.
So our publisher should behave that way, too:
extension SDK {
static func uploadPublisher() -> UploadPublisher {
return UploadPublisher()
}
struct UploadPublisher: Publisher {
typealias Output = Progressable<String>
typealias Failure = Error
func receive<S>(subscriber: S) where S : Subscriber, Self.Failure == S.Failure, Self.Output == S.Input {
<#code#>
}
}
}
Creating the publisher (by calling SDK.uploadPublisher()) doesn't start any work. We'll replace <#code#> with code to start the upload:
extension SDK {
static func uploadPublisher() -> UploadPublisher {
return UploadPublisher()
}
struct UploadPublisher: Publisher {
typealias Output = Progressable<String>
typealias Failure = Error
func receive<S>(subscriber: S) where S : Subscriber, Self.Failure == S.Failure, Self.Output == S.Input {
let subject = PassthroughSubject<Output, Failure>()
subject.receive(subscriber: subscriber)
upload(
progress: { subject.send(.progress($0)) },
completion: {
switch $0 {
case .success(let value):
subject.send(.value(value))
subject.send(completion: .finished)
case .failure(let error):
subject.send(completion: .failure(error))
}
}
)
}
}
}
Note that we call subject.receive(subscriber: subscriber) before we start the upload. This is important! What if upload calls one of its callbacks synchronously, before returning? By passing the subscriber to the subject before calling upload, we ensure that the subscriber has the chance to be notified even if upload calls its callbacks synchronously.
Note: started writing an answer that's has a largely similar intent to #robmayoff's answer, but using Deferred, so posting here for completeness.
Swift Combine only works with values and errors - there's no separate type for progress. But you can model the progress as part of the output, either as a tuple, as was suggested in another answer, or as a custom enum with both progress and result as cases, which would be my preferred approach.
class CombineSDK {
enum UploadProgress<T> {
case progress(Double)
case result(T)
}
func upload() -> AnyPublisher<UploadProgress<String>, Error> {
Deferred { () -> AnyPublisher<UploadProgress<String>, Error> in
let subject = PassthroughSubject<UploadProgress<String>, Error>()
SDK.upload(
progress: { subject.send(.progress($0)) },
completion: { r in
let _ = r.map(UploadProgress.result).publisher.subscribe(subject)
})
return subject.eraseToAnyPublisher()
}
.eraseToAnyPublisher()
}
}
EDIT
Based on #robmayoff's comment, the above solution doesn't handle synchronous case where subject.send is called before subject is returned.
The solution is largely the same, but it does introduce a small complication of having to capture these values, just in case. This can be done with Record, which will provide a temporary sink to subject
func upload() -> AnyPublisher<UploadProgress<String>, Error> {
Deferred { () -> AnyPublisher<UploadProgress<String>, Error> in
let subject = PassthroughSubject<UploadProgress<String>, Error>()
var recording = Record<UploadProgress<String>, Error>.Recording()
subject.sink(
receiveCompletion: { recording.receive(completion: $0) },
receiveValue: { recording.receive($0) })
SDK.upload(
progress: { subject.send(.progress($0)) },
completion: { r in
let _ = r.map(UploadProgress.result).publisher.subscribe(subject)
})
return Record(recording: recording).append(subject).eraseToAnyPublisher()
}
.eraseToAnyPublisher()
}
Here is possible approach
extension CombineSDK {
func upload() -> AnyPublisher<(Double, String?), Error> {
let publisher = PassthroughSubject<(Double, String?), Error>()
SDK.upload(progress: { value in
publisher.send((value, nil))
}, completion: { result in
switch result {
case .success(let key):
publisher.send((1.0, key))
publisher.send(completion: .finished)
case .failure(let error):
publisher.send(completion: .failure(error))
}
})
return publisher.eraseToAnyPublisher()
}
}

Swift Combine Chunk Operator

I'm trying to create chunks of a stream in Apple's Combine framework.
What I'm going for is something like this:
Stream a:
--1-2-3-----4-5--->
Stream b:
--------0-------0->
a.chunk(whenOutputFrom: b)
-------[1, 2, 3]---[4, 5]-->
Can this be implemented in Combine?
What you are looking for is the buffer operator in the ReactiveX world.
There is no built in buffer operator (in the ReactiveX sense) in Combine. The built-in buffer is seems to be more like a bufferCount in ReactiveX.
I found this answer by Daniel T, which recreates the buffer operator in RxSwift, and also this cheatsheet, which tells you how to port RxSwift to Combine.
However, the answer by Daniel T uses Observable.create, which isn't available in Combine. I looked a bit deeper, and found this other answer, that recreates Observable.create in Combine.
Combining the three things I've found (no pun intended), this is what I came up with:
// -------------------------------------------------
// from https://stackoverflow.com/a/61035663/5133585
struct AnyObserver<Output, Failure: Error> {
let onNext: ((Output) -> Void)
let onError: ((Failure) -> Void)
let onCompleted: (() -> 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)) },
onCompleted: { subject.send(completion: .finished) }
))
}, receiveCancel: { disposable?.dispose() })
.eraseToAnyPublisher()
}
}
// -------------------------------------------------
// -------------------------------------------------
// adapted from https://stackoverflow.com/a/43413167/5133585
extension Publisher {
/// collects elements from the source sequence until the boundary sequence fires. Then it emits the elements as an array and begins collecting again.
func buffer<T: Publisher, U>(_ boundary: T) -> AnyPublisher<[Output], Failure> where T.Output == U {
return AnyPublisher.create { observer in
var buffer: [Output] = []
let lock = NSRecursiveLock()
let boundaryDisposable = boundary.sink(receiveCompletion: {
_ in
}, receiveValue: {_ in
lock.lock(); defer { lock.unlock() }
observer.onNext(buffer)
buffer = []
})
let disposable = self.sink(receiveCompletion: { (event) in
lock.lock(); defer { lock.unlock() }
switch event {
case .finished:
observer.onNext(buffer)
observer.onCompleted()
case .failure(let error):
observer.onError(error)
buffer = []
}
}) { (element) in
lock.lock(); defer { lock.unlock() }
buffer.append(element)
}
return Disposable {
disposable.cancel()
boundaryDisposable.cancel()
}
}
}
}
// -------------------------------------------------
I think you would be interested in Combine collect() method.
there is variation it it as well such as by time, count or both.
.collect(.byTimeOrCount(DispatchQueue.global(), 1.0, 10))
where we pass context -> for example global queue
time to wait for it such as 1s in above example
and the count of 10 elements.
Use case would look something like this:
let bufferSubject = PassthroughSubject<Int, Never>()
let cancelBag = Set<AnyCancellable>()
let subscriber = bufferSubject.eraseToAnyPublisher()
.collect(.byTimeOrCount(DispatchQueue.global(), 1.0, 10))
.sink { value in
print("🚀🚀 value: \(value)")
}
.store(in: &cancelBag)
be sure to test it :)
bufferSubject.send(1)
bufferSubject.send(2)
bufferSubject.send(3)
...
DispatchQueue.asyncAfter(...) {
bufferSubject.send(4)
bufferSubject.send(5)
bufferSubject.send(6)
}

Swift Combine alternative to Rx Observable.create

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?
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()
}
}
}
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
Add an isCancelled operation outside the closure and check it in the future's closure. isCancelled can be toggled with the handleEvent() operator.
var isCancelled = false
func loadWidgets() -> AnyPublisher<[Widget], Error> {
return HandleEvents<Future<Any, 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))
}
if isCancelled {
loadTask.cancel()
}
).handleEvents(receiveCancel: {
isCancelled = true
})
}
}
and somewhere in the app you do this to cancel the event
loadWidgets().cancel()
Also check this article
Thanks to ccwasden for the inspiration. This replicates Observable.create semantics with a pure Combine implementation without any superfluous entities.
AnyPublisher.create() Swift 5.6 Extension
public extension AnyPublisher {
static func create<Output, Failure>(_ subscribe: #escaping (AnySubscriber<Output, Failure>) -> AnyCancellable) -> AnyPublisher<Output, Failure> {
let passthroughSubject = PassthroughSubject<Output, Failure>()
var cancellable: AnyCancellable?
return passthroughSubject
.handleEvents(receiveSubscription: { subscription in
let subscriber = AnySubscriber<Output, Failure> { subscription in
} receiveValue: { input in
passthroughSubject.send(input)
return .unlimited
} receiveCompletion: { completion in
passthroughSubject.send(completion: completion)
}
cancellable = subscribe(subscriber)
}, receiveCompletion: { completion in
}, receiveCancel: {
cancellable?.cancel()
})
.eraseToAnyPublisher()
}
}
Usage
func doSomething() -> AnyPublisher<Int, Error> {
return AnyPublisher<Int, Error>.create { subscriber in
// Imperative implementation of doing something can call subscriber as follows
_ = subscriber.receive(1)
subscriber.receive(completion: .finished)
// subscriber.receive(completion: .failure(myError))
return AnyCancellable {
// Imperative cancellation implementation
}
}
}