Why .collect() operator in swift Combine always sends .unlimited demand regardless of the demand of upstream publisher? - swift

I have been playing with Combine to understand how it works in more details and I create a custom Publisher, Subscription and Subscriber.
Here's how it looks..
The emoji beamer publisher along with subscription:
struct EmojiBeamerPublisher: Publisher {
typealias Output = String
typealias Failure = Error
private let emojis: [String] = ["👍","❤️","✅","🥰","😍","🚀","😅","🍑","🍞","🎅","❄️","🐻","👀","👄","🦷","✍️","🙏","👨‍💻","🐝","🐛","🦉","🦀","🐍","🐞","🧸"]
func receive<S>(subscriber: S) where S : Subscriber, Self.Failure == S.Failure, Self.Output == S.Input {
let subscription = EmojiBeamerSubscription(output: emojis, subscriber: subscriber)
subscriber.receive(subscription: subscription)
}
}
extension EmojiBeamerPublisher {
private final class EmojiBeamerSubscription<S: Subscriber>: Subscription where S.Input == Output, S.Failure == Failure {
var subscriber: S?
let output: [String]
init(output: [String], subscriber: S) {
self.subscriber = subscriber
self.output = output
}
func request(_ demand: Subscribers.Demand) {
Swift.print("Demand: \(demand)") // Here I receive Unlimited demand
var demand = demand
Timer.scheduledTimer(withTimeInterval: 5, repeats: true) { [weak self] timer in
guard let self = self else { return }
guard demand > 0, let subscriber = self.subscriber else {
timer.invalidate()
self.subscriber?.receive(completion: .finished)
self.cancel()
return
}
demand -= 1
demand += subscriber.receive(self.output.randomElement()! + " \(Date())")
}
}
func cancel() {
subscriber = nil
}
}
}
Here is my Custom subscriber:
final class EmojiBeamerSubscriber<Input, Failure: Error>: Subscriber, Cancellable {
var subscription: Subscription?
let receiveValue: (Input) -> Void
init(receiveValue: #escaping (Input) -> Void) {
self.receiveValue = receiveValue
}
func receive(subscription: Subscription) {
self.subscription = subscription
subscription.request(.max(3)) // Here I send only 3 as max demand
}
func receive(_ input: Input) -> Subscribers.Demand {
receiveValue(input)
return .none
}
func receive(completion: Subscribers.Completion<Failure>) {
print("Will handle later:", completion)
}
func cancel() {
self.subscription?.cancel()
self.subscription = nil
}
}
extension Publisher {
func myCustomSink(receiveValueHandler: #escaping (Self.Output) -> Void) -> AnyCancellable {
let myCustomSubscriber = EmojiBeamerSubscriber<Self.Output, Self.Failure>(receiveValue: receiveValueHandler)
subscribe(myCustomSubscriber)
return AnyCancellable(myCustomSubscriber)
}
}
As you can see on my custom subscription I request with demand .max(3) if I don't use collect everything works fine, I get an emoji beamed every 5 second after 3 I got a .finish completion.
Works fine (and sends .max(3) demand):
let emojiBeamer = EmojiBeamerPublisher()
var cancellables = Set<AnyCancellable>()
emojiBeamer
.myCustomSink { value in Swift.print("Random Emoji:: \(value)") }
.store(in: &cancellables)
However if I simply add .collect() to catch all 3 results at once in an array it just requests with .unlimited demand on my subscription, resulting in a never ending subscription because my demand will never reach zero.
Never complete (and sends unlimited demand):
let emojiBeamer = EmojiBeamerPublisher()
var cancellables = Set<AnyCancellable>()
emojiBeamer
.collect()
.myCustomSink { value in Swift.print("Random Emoji:: \(value)") }
.store(in: &cancellables)
Is there something wrong with my implementation? Or Did I misunderstood the purpose of .collect() operator?
Thank you in advance :)

From the documentation:
This publisher requests an unlimited number of elements from the upstream publisher and uses an unbounded amount of memory to store the received values. The publisher may exert memory pressure on the system for very large sets of elements.
So the behaviour you noticed is the correct one, as collect() sends an unlimited demand upstream.
The unlimited demand causes the demand -= 1 instruction to do nothing, so the demand > 0 check will always pass, resulting into an infinite loop that never sends the completion. You will need an extra condition to make the "collected" stream a finite one.
For infinite streams, the collect(_:) overload (the one that allows to pass a number of items to collect) performs better in regards to the demand, but still requests from upstream more elements than one might expect:
When this publisher receives a request for .max(n) elements, it requests .max(count * n) from the upstream publisher.

Related

Understanding actor and making it thread safe

I have an actor that is processing values and is then publishing the values with a Combine Publisher.
I have problems understanding actors, I thought when using actors in an async context, it would automatically be serialised. However, the numbers get processed in different orders and not in the expected order (see class tests for comparison).
I understand that if I would wrap Task around the for loop that then this would be returned serialised, but my understanding is, that I could call a function of an actor and this would then be automatically serialised.
How can I make my actor thread safe so it publishes the values in the expected order even if it is called from a different thread?
import XCTest
import Combine
import CryptoKit
actor AddNumbersActor {
private let _numberPublisher: PassthroughSubject<(Int,String), Never> = .init()
nonisolated lazy var numberPublisher = _numberPublisher.eraseToAnyPublisher()
func process(_ number: Int) {
let string = SHA512.hash(data: Data(String(number).utf8))
.description
_numberPublisher.send((number, string))
}
}
class AddNumbersClass {
private let _numberPublisher: PassthroughSubject<(Int,String), Never> = .init()
lazy var numberPublisher = _numberPublisher.eraseToAnyPublisher()
func process(_ number: Int) {
let string = SHA512.hash(data: Data(String(number).utf8))
.description
_numberPublisher.send((number, string))
}
}
final class TestActorWithPublisher: XCTestCase {
var subscription: AnyCancellable?
override func tearDownWithError() throws {
subscription = nil
}
func testActor() throws {
let addNumbers = AddNumbersActor()
var numbersResults = [(int: Int, string: String)]()
let expectation = expectation(description: "numberOfExpectedResults")
let numberCount = 1000
subscription = addNumbers.numberPublisher
.sink { results in
print(results)
numbersResults.append(results)
if numberCount == numbersResults.count {
expectation.fulfill()
}
}
for number in 1...numberCount {
Task {
await addNumbers.process(number)
}
}
wait(for: [expectation], timeout: 5)
print(numbersResults.count)
XCTAssertEqual(numbersResults[10].0, 11)
XCTAssertEqual(numbersResults[100].0, 101)
XCTAssertEqual(numbersResults[500].0, 501)
}
func testClass() throws {
let addNumbers = AddNumbersClass()
var numbersResults = [(int: Int, string: String)]()
let expectation = expectation(description: "numberOfExpectedResults")
let numberCount = 1000
subscription = addNumbers.numberPublisher
.sink { results in
print(results)
numbersResults.append(results)
if numberCount == numbersResults.count {
expectation.fulfill()
}
}
for number in 1...numberCount {
addNumbers.process(number)
}
wait(for: [expectation], timeout: 5)
print(numbersResults.count)
XCTAssertEqual(numbersResults[10].0, 11)
XCTAssertEqual(numbersResults[100].0, 101)
XCTAssertEqual(numbersResults[500].0, 501)
}
}
``
Using actor does indeed serialize access.
The issue you're running into is that the tests aren't testing whether calls to process() are serialized, they are testing the execution order of the calls. And the execution order of the Task calls is not guaranteed.
Try changing your AddNumbers objects so that instead of the output order reflecting the order in which the calls were made, they will succeed if calls are serialized but will fail if concurrent calls are made. You can do this by keeping a count variable, incrementing it, sleeping a bit, then publishing the count. Concurrent calls will fail, since count will be incremented multiple times before its returned.
If you make that change, the test using an Actor will pass. The test using a class will fail if it calls process() concurrently:
DispatchQueue.global(qos: .default).async {
addNumbers.process()
}
It will also help to understand that Task's scheduling depends on a bunch of stuff. GCD will spin up tons of threads, whereas Swift concurrency will only use 1 worker thread per available core (I think!). So in some execution environments, just wrapping your work in Task { } might be enough to serialize it for you. I've been finding that iOS simulators act as if they have a single core, so task execution ends up being serialized. Also, otherwise unsafe code will work if you ensure the task runs on the main actor, since it guarantees serial execution:
Task { #MainActor in
// ...
}
Here are modified tests showing all this:
class TestActorWithPublisher: XCTestCase {
actor AddNumbersActor {
private let _numberPublisher: PassthroughSubject<Int, Never> = .init()
nonisolated lazy var numberPublisher = _numberPublisher.eraseToAnyPublisher()
var count = 0
func process() {
// Increment the count here
count += 1
// Wait a bit...
Thread.sleep(forTimeInterval: TimeInterval.random(in: 0...0.010))
// Send it back. If other calls to process() were made concurrently, count may have been incremented again before being sent:
_numberPublisher.send(count)
}
}
class AddNumbersClass {
private let _numberPublisher: PassthroughSubject<Int, Never> = .init()
lazy var numberPublisher = _numberPublisher.eraseToAnyPublisher()
var count = 0
func process() {
count += 1
Thread.sleep(forTimeInterval: TimeInterval.random(in: 0...0.010))
_numberPublisher.send(count)
}
}
var subscription: AnyCancellable?
override func tearDownWithError() throws {
subscription = nil
}
func testActor() throws {
let addNumbers = AddNumbersActor()
var numbersResults = [Int]()
let expectation = expectation(description: "numberOfExpectedResults")
let numberCount = 1000
subscription = addNumbers.numberPublisher
.sink { results in
numbersResults.append(results)
if numberCount == numbersResults.count {
expectation.fulfill()
}
}
for _ in 1...numberCount {
Task.detached(priority: .high) {
await addNumbers.process()
}
}
wait(for: [expectation], timeout: 10)
XCTAssertEqual(numbersResults, Array(1...numberCount))
}
func testClass() throws {
let addNumbers = AddNumbersClass()
var numbersResults = [Int]()
let expectation = expectation(description: "numberOfExpectedResults")
let numberCount = 1000
subscription = addNumbers.numberPublisher
.sink { results in
numbersResults.append(results)
if numberCount == numbersResults.count {
expectation.fulfill()
}
}
for _ in 1...numberCount {
DispatchQueue.global(qos: .default).async {
addNumbers.process()
}
}
wait(for: [expectation], timeout: 5)
XCTAssertEqual(numbersResults, Array(1...numberCount))
}
}

How to limit flatMap concurrency in Combine still having all source events processed?

If I specify the maxPublishers parameter then source events after first maxPublishers events won't be flat mapped. While I want to limit only concurrency. That is to continue processing next events after some of the first maxPublishers flat map publishers have completed.
Publishers.Merge(
addImageRequestSubject
.flatMap(maxPublishers: .max(3)) { self.compressImage($0) }
.compactMap { $0 }
.flatMap(maxPublishers: .max(3)) { self.addImage($0) },
addVideoRequestSubject
.flatMap(maxPublishers: .max(3)) { self.addVideo(url: $0) }
).sink(receiveCompletion: { _ in }, receiveValue: {})
.store(in: &cancelBag)
I've also tried to limit concurrency with help of OperationQueue. But maxConcurrentOperationCount seems doesn't have an effect.
Publishers.Merge(
addImageRequestSubject
.receive(on: imageCompressionQueue)
.flatMap { self.compressImage($0) }
.compactMap { $0 }
.receive(on: mediaAddingQueue)
.flatMap { self.addImage($0) },
addVideoRequestSubject
.receive(on: mediaAddingQueue)
.flatMap { self.addVideo(url: $0) }
).sink(receiveCompletion: { _ in }, receiveValue: {})
.store(in: &cancelBag)
private lazy var imageCompressionQueue: OperationQueue = {
var queue = OperationQueue()
queue.maxConcurrentOperationCount = 3
return queue
}()
private lazy var mediaAddingQueue: OperationQueue = {
var queue = OperationQueue()
queue.maxConcurrentOperationCount = 3
return queue
}()
Flat map publishers look this way:
func compressImage(_ image: UIImage) -> Future<Data?, Never> {
Future { promise in
DispatchQueue.global().async {
let result = image.compressTo(15)?.jpegData(compressionQuality: 1)
promise(Result.success(result))
}
}
}
You have stumbled very beautifully right into the use case for the .buffer operator. Its purpose is to compensate for .flatMap backpressure by accumulating values that would otherwise be dropped.
I will illustrate by a completely artificial example:
class ViewController: UIViewController {
let sub = PassthroughSubject<Int,Never>()
var storage = Set<AnyCancellable>()
var timer : Timer!
override func viewDidLoad() {
super.viewDidLoad()
sub
.flatMap(maxPublishers:.max(3)) { i in
return Just(i)
.delay(for: 3, scheduler: DispatchQueue.main)
.eraseToAnyPublisher()
}
.sink { print($0) }
.store(in: &storage)
var count = 0
self.timer = Timer.scheduledTimer(withTimeInterval: 1, repeats: true) {
_ in
count += 1
self.sub.send(count)
}
}
}
So, our publisher is emitting an incremented integer every second, but our flatMap has .max(3) and takes 3 seconds to republish a value. The result is that we start to miss values:
1
2
3
5
6
7
9
10
11
...
The solution is to put a buffer in front of the flatMap. It needs to be large enough to hold any missed values long enough for them to be requested:
sub
.buffer(size: 20, prefetch: .keepFull, whenFull: .dropOldest)
.flatMap(maxPublishers:.max(3)) { i in
The result is that all the numeric values do in fact arrive at the sink. Of course in real life we could still lose values if the buffer is not large enough to compensate for disparity between the rate of value emission from the publisher and the rate of value emission from the backpressuring flatMap.

How do I forward Publisher output to a downstream Subscriber through a custom operator?

I have a potential use for Combine, but I am having a lot of trouble with the implementation details. The goal is to provide an Publisher that will do the following:
Search for a cached value, and emit that value, or:
Refer the subscriber to an upstream publisher that will emit a value, storing it in the appropriate cache location
I understand that this could be done using existing operators, but I would like to learn how to make a custom Operator/Publisher/Subscription pattern, if possible.
I'd like the usage to be similar to the following bit of pseduocode:
URLSession.shared.dataTaskPublisher(for: url)
.cache(with: { someSortOfCachingPolicy })
.sink()
In order to implement this, I am guessing at what Apple does for things like map and flatMap.
I have created a CachePublisher to try to capture the Upstream Publisher:
struct CachePublisher<Upstream: Publisher>: Publisher {
typealias Output = Upstream.Output
typealias Failure = Upstream.Failure
var upstream: Upstream
var getCache: ()->Output?
var setCache: (Output)->Void
func receive<S>(subscriber: S) where S : Subscriber, Self.Failure == S.Failure, Self.Output == S.Input {
let subscription = CachePublisherSubscription(subscriber: subscriber, upstream: upstream, getCache: getCache, setCache: setCache)
subscriber.receive(subscription: subscription)
}
init(_ upstream: Upstream, getCache: #escaping ()->Output?, setCache: #escaping (Output)->Void) {
self.upstream = upstream
self.getCache = getCache
self.setCache = setCache
}
}
This is followed up with a Subscription:
extension CachePublisher {
class CachePublisherSubscription<S: Subscriber>: Subscription where S.Input == Upstream.Output, S.Failure == Upstream.Failure {
var subscriber: S
var upstream: Upstream
var setCache: (Output)->Void
var getCache: ()->Output?
init(subscriber: S, upstream: Upstream, getCache: #escaping ()->Output?, setCache: #escaping (Output)->Void) {
self.subscriber = subscriber
self.upstream = upstream
self.getCache = getCache
self.setCache = setCache
}
func request(_ demand: Subscribers.Demand) {
///check the cache for a value that satisfies the type
///return a value from the upstream publisher if not
if let output = self.getCache() {
subscriber.receive(output)
} else {
//forward an upstream value?
//how? an entire publisher/subscriber chain?
}
}
func cancel() {
}
}
}
And finally, a function so you can pass the upstream publisher to the CachePublisher
extension Publisher {
func cache() -> CachePublisher<Self> {
return CachePublisher(self, getCache: { nil }, setCache: { _ in })
}
}
I have no idea what to put in the required methods, or how to pass the subscriber up the chain to the upstream publisher. Or how to capture values from the upstream publisher.
The idea that came into my head is that downstream subscribers sort of create a nesting doll type structure, but I just don't know how to connect them.
You don't need the whole Publisher/Publishers/Subscription dance, you can customize the subscribe method without needing a custom class. Existing Combine operators to the rescue here :).
extension Publisher {
func cache(read: #escaping Publishers.Cache<Self>.Read,
write: #escaping Publishers.Cache<Self>.Write) -> Publishers.Cache<Self> {
Publishers.Cache(upstream: self, read: read, write: write)
}
}
extension Publishers {
struct Cache<P: Publisher>: Publisher {
typealias Output = P.Output
typealias Failure = P.Failure
typealias Read = () -> Output?
typealias Write = (Output) -> Void
private let upstream: P
private let read: Read
private let write: Write
init(upstream: P, read: #escaping Read, write: #escaping Write) {
self.upstream = upstream
self.read = read
self.write = write
}
func receive<S>(subscriber: S) where S : Subscriber, Self.Failure == S.Failure, Self.Output == S.Input {
if let cachedValue = read() {
Just(cachedValue).setFailureType(to: Failure.self).receive(subscriber: subscriber)
} else {
upstream.handleEvents(receiveOutput: write).receive(subscriber: subscriber)
}
}
}
}
handleEvents kinda breaks the "pure functions" paradigm that is recommended to be followed when writing custom operators pipelines, however as you anyway need to write to the cache, and that's already a side effect, the added impact of calling handleEvents is not that big.
Making the custom Subscription also a Subscriber allows it to connect both directions. When the cache getter produces a result, it is sent to the downstream subscriber. However, when the getter does not, the demand is forwarded to the upstream publisher, which emits a value.
That value is then captured by the Subscriber methods of the custom Subscription and forwarded to the downstream subscriber.
extension CachePublisher {
class CachePublisherSubscription<Downstream: Subscriber>: Subscription, Subscriber where Downstream.Input == Upstream.Output, Downstream.Failure == Upstream.Failure {
typealias Input = Upstream.Output
typealias Failure = Upstream.Failure
var downstream: Downstream
var upstream: Upstream
var upstreamSubscription: Subscription?
var read: Read
var write: Write
init(downstream: Downstream, upstream: Upstream, read: #escaping Read, write: #escaping Write) {
self.downstream = downstream
self.upstream = upstream
self.read = read
self.write = write
upstream.subscribe(self)
}
func request(_ demand: Subscribers.Demand) {
if let cachedValue = read() {
downstream.receive(cachedValue)
} else {
upstreamSubscription?.request(demand)
}
}
// keep a reference to the upstream subscription
func receive(subscription: Subscription) {
self.upstreamSubscription = subscription
}
// pass input downstream
func receive(_ input: Input) -> Subscribers.Demand {
self.write(input)
return downstream.receive(input)
}
// pass completion downstream
func receive(completion: Subscribers.Completion<Failure>) {
downstream.receive(completion: completion)
}
func cancel() {
//TO-DO: Finish cancellation
}
}
}

How to replicate PromiseKit-style chained async flow using Combine + 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()
}

Loop over Publisher Combine framework

I have the following function to perform an URL request:
final class ServiceManagerImpl: ServiceManager, ObservableObject {
private let session = URLSession.shared
func performRequest<T>(_ request: T) -> AnyPublisher<String?, APIError> where T : Request {
session.dataTaskPublisher(for: self.urlRequest(request))
.tryMap { data, response in
try self.validateResponse(response)
return String(data: data, encoding: .utf8)
}
.mapError { error in
return self.transformError(error)
}
.eraseToAnyPublisher()
}
}
Having these 2 following functions, I can now call the desired requests from corresponded ViewModel:
final class AuditServiceImpl: AuditService {
private let serviceManager: ServiceManager = ServiceManagerImpl()
func emptyAction() -> AnyPublisher<String?, APIError> {
let request = AuditRequest(act: "", nonce: String.randomNumberGenerator)
return serviceManager.performRequest(request)
}
func burbleAction(offset: Int) -> AnyPublisher<String?, APIError> {
let request = AuditRequest(act: "burble", nonce: String.randomNumberGenerator, offset: offset)
return serviceManager.performRequest(request)
}
}
final class AuditViewModel: ObservableObject {
#Published var auditLog: String = ""
private let auditService: AuditService = AuditServiceImpl()
init() {
let timer = Timer(timeInterval: 5, repeats: true) { _ in
self.getBurbles()
}
RunLoop.main.add(timer, forMode: .common)
}
func getBurbles() {
auditService.emptyAction()
.flatMap { [unowned self] offset -> AnyPublisher<String?, APIError> in
let currentOffset = Int(offset?.unwrapped ?? "") ?? 0
return self.auditService.burbleAction(offset: currentOffset)
}
.receive(on: RunLoop.main)
.sink(receiveCompletion: { [unowned self] completion in
print(completion)
}, receiveValue: { [weak self] burbles in
self?.auditLog = burbles!
})
.store(in: &cancellableSet)
}
}
Everything is fine when I use self.getBurbles() for the first time. However, for the next calls, print(completion) shows finished, and the code doesn't perform self?.auditLog = burbles!
I don't know how can I loop over the getBurbles() function and get the response at different intervals.
Edit
The whole process in a nutshell:
I call getBurbles() from class initializer
getBurbles() calls 2 nested functions: emptyAction() and burbleAction(offset: Int)
Those 2 functions generate different requests and call performRequest<T>(_ request: T)
Finally, I set the response into auditLog variable and show it on the SwiftUI layer
There are at least 2 issues here.
First when a Publisher errors it will never produce elements again. That's a problem here because you want to recycle the Publisher here and call it many times, even if the inner Publisher fails. You need to handle the error inside the flatMap and make sure it doesn't propagate to the enclosing Publisher. (ie you can return a Result or some other enum or tuple that indicates you should display an error state).
Second, flatMap is almost certainly not what you want here since it will merge all of the api calls and return them in arbitrary order. If you want to cancel any existing requests and only show the latest results then you should use .map followed by switchToLatest.