Subscribe returning Void instead of AnyCancelable - swift

I currently have a publisher with the type of AnyPublisher<[MyClass], Error> to which I'm attempting to attach a subscriber and capture the resulting AnyCancelable. Xcode's autocomplete says I should be able to do this, but when the code is actually entered, I encounter a compiler error saying that the returned type isn't AnyCancelable, but ()
Here's an example of my code:
let networkController = NetworkController()
let viewState = MyViewState()
let publisher: AnyPublisher<[MyClass], Error> = networkController.createPublisher()
let cancelable: AnyCancellable = publisher.subscribe(viewState)
Cannot convert value of type '()' to specified type 'AnyCancellable'
My goal here is to wrap an existing async function, which could be called numerous times, in the Combine Framework, so that I can have a nice way of having the request get canceled when it's reassigned, like so:
... self.cancelable = cancelable

AnyPublisher has two subscribe methods for attaching a Subscriber to a Publisher. It inherits both from the Publisher protocol. Here they are:
func subscribe<S>(_ subscriber: S)
where S : Subscriber, Self.Failure == S.Failure, Self.Output == S.Input
func subscribe<S>(_ subject: S) -> AnyCancellable
where S : Subject, Self.Failure == S.Failure, Self.Output == S.Output
So, I guess your MyViewState type doesn’t conform to the Subject protocol. Therefore you can’t use the version of subscribe that returns AnyCancellable.
Subject is sub-protocol of Publisher that exposes send methods for injecting values which the Subject then publishes. Do you want your MyViewState class to be a Publisher? I suspect not.
What you probably want to do instead is change your MyViewState type to not even conform to Subscriber. Instead, use AnyPublisher’s sink method (also inherited from Publisher) to connect the publisher to your viewState. The sink method returns a Sink object that conforms to Cancellable:
func sink(
receiveCompletion: ((Subscribers.Completion<Failure>) -> Void)? = nil,
receiveValue: #escaping ((Output) -> Void))
-> Subscribers.Sink<Output, Failure>
You can wrap the returned Cancellable in AnyCancellable if needed. Thus:
let can = publisher.sink(
receiveCompletion: { viewModel.receive(completion: $0) },
receiveValue: { viewModel.receive($0) })
let anyCan = AnyCancellable(can)

Related

How to chain together two Combine publishers in Swift and keep the cancellable object

Does anyone know how to chain together two publishers with Swift + Combine and keep the cancellable so that I can cancel the pipeline later?
I have a method that accepts input from a publisher and outputs to a publisher:
static func connect(inputPublisher: Published<String>.Publisher, outputPublisher: inout Published<String>.Publisher) -> AnyCancellable {
inputPublisher.assign(to: &outputPublisher)
// Unfortunately assign consumes cancellable
}
Note that this method cannot access the wrapped properties directly. The publishers must be passed as arguments.
If I understand correctly, you want to link two publishers but with the option to break that link at some point in the future.
I would try using sink on the inputPublisher, since that function gives me a cancellable, and then a PassthroughSubject, since I wasn't able to figure out how to pass the value from sink directly to outputPublisher.
It would look something like this:
static func connect(inputPublisher: Published<String>.Publisher, outputPublisher: inout Published<String>.Publisher) -> AnyCancellable {
let passthrough = PassthroughSubject<String, Never>()
passthrough.assign(to: &outputPublisher)
let cancellable = inputPublisher.sink { string in
passthrough.send(string)
}
return cancellable
}
Disclaimer: I wrote this on a Playground and it compiles, but I didn't actually run it.
You cannot do that with assign(to:), since it binds the lifetime of the subscription to the lifetime of the Published upstream (inputPublisher in your case) and hence you cannot manually cancel the subscription.
Instead, you can use assign(to:on:), which takes an object and a key path and whenever the upstream emits, it assigns the value to the property represented by the key path on the object.
inputPublisher.assign(to:on:)
class PublishedModel {
#Published var value: String
init(value: String) {
self.value = value
}
}
let outputModel = PublishedModel(value: "")
// this returns an `AnyCancellable` that you can cancel
PublishedModel(value: "initial").$value.assign(to: \.value, on: outputModel)
You can also create a convenience method for this
func assign<Root, Value>(published: Published<Value>.Publisher, to keyPath: ReferenceWritableKeyPath<Root, Value>, on object: Root) -> AnyCancellable {
published.assign(to: keyPath, on: object)
}
let outputModel = PublishedModel(value: "")
let inputModel = PublishedModel(value: "initial")
assign(published: inputModel.$value, to: \.value, on: outputModel)

Store subscription from custom Combine subscriber?

According to the swift combine documentation, the proper way to connect a subscriber to a publisher is with the publishers subscribe<S>(S) protocol extension method. Then the publisher creates a subscription and passes that subscription on to the subscriber. All's well and good so far.
What I can't seem to figure out is how to gain access to that subscription and retain it in the calling code. How is sink() implemented such that it can return that subscription? Unless I'm mistaken, the subscription is responsible for retaining its subscriber, which means I can't store a reference to the subscription in the subscriber. And since Subscription isn't class bound, it can't be weak.
Here's an example of how sink might be implemented
import Combine
import Foundation
extension Publisher {
func mockSink(
receiveValue: #escaping (Output) -> Void,
receiveCompletion: #escaping (Subscribers.Completion<Failure>) -> Void) -> AnyCancellable {
var result : AnyCancellable!
let anySubscriber = AnySubscriber(
receiveSubscription: { subscription in
subscription.request(.unlimited)
result = AnyCancellable({subscription.cancel()})
},
receiveValue: { (value : Output) in receiveValue(value); return .unlimited},
receiveCompletion: receiveCompletion)
subscribe(anySubscriber)
return result
}
}
var subscriptions = Set<AnyCancellable>()
["one", "two", "three"].publisher
.mockSink(receiveValue: {debugPrint($0)}, receiveCompletion: {debugPrint($0)})
.store(in: &subscriptions)
As you can see the ability to return an AnyCancellable arises from the magic of AnySubscriber which returns its subscription in a closure. AnySubscriber is a handy tool to have when implementing custom Publishers and it may be helpful if you want to implement your own Subscriber type. But basically a Subscriber only exposes the subscription if it is designed to.
The cancellable retains the subscription (or possibly is the subscription. Remember Subscriptions are Cancellables.) So when you retain the Cancellable, you are retaining the subscription.

How to forward send calls from one PassthroughSubject to another (i.e. PassthroughSubject chaining)?

Imagine I have an API for a class that uses a PassthroughSubject to receive its input:
class Logger {
let log: PassthroughSubject<String, Never>
}
Normally I can emit a value by invoking logger.log.send("test").
Now say I want to have my own logger that sits between this generic logger and my code:
class MyLogger {
let log: PassthroughSubject<String, Never>
}
This should prefix the string, and then send any updates to Logger. Is there a way to chain the output from one PassthroughSubject (e.g. MyLogger) to another (e.g. Logger)?
I know I can do it this way:
let cancellable = myLogger.log.sink {
logger.log.send("[MyApp] " + $0)
}
However, that doesn't seem like the Combine-way of chaining things together. I was hoping there was an API I could use more similar to this one:
logger.log.subscribe(myLogger.log.map { "[MyApp] " + $0 })
However, that doesn't compile since I think the map is causing it to turn into a publisher instead of a subject:
Instance method 'subscribe' requires that 'Publishers.Map<PassthroughSubject<String, Never>, String>' conform to 'Subject'
Is there a more declarative API for making one PassthroughSubject subscribe to updates from another (with support for mutations in between) besides relying on sink?
If you flip the order of the subscribe, it works:
let cancellable = myLogger.log
.map { "[MyApp] " + $0 }
.subscribe(logger.log)
myLogger.send("test") // will send "[MyApp] test" to logger.log
Having said that, modeling set APIs like this with Combine seems a bit odd, and the Failure data type seems to be ignored completely. E.g. if the log failed, there is no way to communicate back up the chain that it failed.
A better API might be a normal function, which returns an AnyPublisher:
class Logger {
func log(_ string: String) -> AnyPublisher<Void, Never>
}
class MyLogger {
func log(_ string: String) -> AnyPublisher<Void, Never> {
return logger.log(string)
}
}

Mapping Swift Combine Future to another Future

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>>

Cleaning up observables

I am using rxSwift and I have a dictionary of observables which can be subscribed to.
var observables: [String: Observable<Bool>] = [:]
At some point I have to clean up some of those observables. I do it as follows
observables.removeValue(forKey: someKey)
Is it enough to clean up the observables with the above line? Is the observable also killed (or how do I "kill" it)? Someone might already be subscribed to the observable and then even after removing it from the dictionary it would still be alive and might fire, right?
Or is the observable gone the moment I remove it because nobody holds a strong reference to it? What happens in the moment the observable is removed to potential subsribers?
I do not have access to the subscribers from the class where the dictionary with the observables is kept.
You can use takeUntil operator. It will send a completed signal to the observable, so the subscriber will release the retained resources.
For example, you can setup a PublishSubject where you send the observable identifier to complete that observable.
var observables: [String: Observable<Bool>] = [:]
let finishObservable = PublishSubject<String>()
func addObservable(observable: Observable<Bool>, withId identifier: String) -> Observable<Bool> {
let condition = finishObservable.filter { $0 == identifier }
let newObservable = observable.takeUntil(condition)
observables[identifier] = newObservable
return newObservable
}
This way, to clean an observable, you send the observable identifier and then you can remove the completed sequence from the dictionary.
func removeObservable(identifier: String) {
// Complete the observable so it stops sending events and subscriber releases resources
finishObservable.onNext(identifier)
observables.removeValue(forKey: identifier)
}
If you're planning to share subscription between observers, you can also use a ConnectableObservable. I've used this kind of observables when subscribers come and go but you want to share the same subscription. It's usefull if the observable fetches network resources for example.
var disposables: [String: Disposable] = [:]
func addObservable(observable: Observable<Bool>, withId identifier: String) -> Observable<Bool> {
let newObservable: ConnectableObservable = observable.replay(1)
disposables[identifier] = newObservable.connect() // This call triggers the subscription, so you can call it later
return newObservable
}
func removeObservable(identifier: String) {
if let disposable = disposables.removeValue(forKey: identifier) {
disposable.dispose()
}
}