Use result of publisher in map of another publisher - swift

As example I have a basic published value like
#Published var value: String
I have want to validates this value of my form to give my user an output. For that I will use Combine in my MVVM project.
Now this type of value needs to be validated against my REST API. For my REST API I already have a method to get my results of my like getFirstMailboxRedirectFromAPI which returns AnyPublisher<MailboxAPIResponse, APIError>. MailboxAPIResponse is a decodable object for the api response. So if I just want to display the result, I create a subscriber with .sink add the result to a variable which will be shown in a view. So good so far. Now my problem:
As described in the first section my value is already a Publisher (because of #Published), where I can do some .map stuff for validation with it and returning true or false if everything is fine or not.
So to validate my published value I need to call my other Publisher which uses the API to check if the value already exists. But I don't know how this should work.
This is my code so far but this doesn't work. But this shows you my idea how it should work.
private var isMailboxRedirectNameAvailablePublisher: AnyPublisher<Bool, Never> {
$mailboxRedirectName
.debounce(for: 0.5, scheduler: RunLoop.main)
.setFailureType(to: Error.self)
.flatMap { name in
self.getFirstMailboxRedirectFromAPI(from: name, and: self.domainName)
.map { apiResponse in
return apiResponse.response.data.count == 0
}
}
.eraseToAnyPublisher()
}
So in result the publisher should use the redirectName to call the API and the API gives me the result if the mailbox already exists, then returns a boolean, if it's existing or not.
How can I nest multiple publishers and use the result of the API publisher in my published value publisher?

I simplified a little but key take aways are 1) use switchToLatest to flatten a Publisher of Publishers if you want the operation to restart (flatMap is a merge, so events could arrive out of order). 2) You need to handle the failure types and make sure an inner Publisher never fails or the outer publisher will also complete.
final class M: ObservableObject {
#Published var mailboxRedirectName: String = ""
private var isMailboxRedirectNameAvailablePublisher: AnyPublisher<Bool, Never> {
$mailboxRedirectName
.debounce(for: 0.5, scheduler: DispatchQueue.main)
.map { [weak self] name -> AnyPublisher<Bool, Never> in
guard let self = self else { return Just(false).eraseToAnyPublisher() }
return self
.getFirstMailboxRedirectFromAPI(from: name)
.replaceError(with: false)
.eraseToAnyPublisher()
}
.switchToLatest()
.eraseToAnyPublisher()
}
func getFirstMailboxRedirectFromAPI(from name: String) -> AnyPublisher<Bool, Error> {
Just(true).setFailureType(to: Error.self).eraseToAnyPublisher()
}
}

Related

How to have a publisher emit only to the last subscriber in Combine

Is there a way to have the publisher emit a value only to the latest subscriber/observer?
An example for that would be; a manager class that can be subscribed to by multiple observers. When an event occurs, I would like only the latest subscriber to be observed. As far as I know, there is no way for the publisher to keep track of its subscribers but my knowledge regarding Combine and reactive programming is limited so I am unsure if this is possible in the first place.
You are right. Unfortunately, there is no way to list/track subscribers of a publisher. To solve your problem, you have to implement a custom publisher. There are two possibilities here. Either you implement a custom publisher with the Publisher protocol, but Apple advises against this (see here), or you create a custom publisher with already existing types, as Apple recommends. I have prepared an example for the second option.
The logic is very simple. We create a publisher with a PassthroughSubject inside (it can also be a CurrentValueSubject). Then we implement the methods typical of a PassthroughSubject and use them to overwrite the same methods of the PassthroughSubject, which is inside our class. In the sink method we store all returning subscriptions BUT before we add a new subscription to the Set, we go through all the already cached subscriptions and cancel them. This way we achieve the goal that only the last subscription works.
// The subscriptions will be cached in the publisher.
// To avoid strong references, I use the WeakBox recommendation from the Swift forum.
struct WeakBox<T: AnyObject & Hashable>: Hashable {
weak var item: T?
func hash(into hasher: inout Hasher) {
hasher.combine(item)
}
}
class MyPublisher<T, E: Error> {
private let subject = PassthroughSubject<T, E>()
private var subscriptions = Set<WeakBox<AnyCancellable>>()
deinit {
subscriptions.removeAll()
}
public func send(_ input: T) {
subject.send(input)
}
public func send(completion: Subscribers.Completion<E>) {
subject.send(completion: completion)
}
public func sink(receiveCompletion receivedCompletion: #escaping (Subscribers.Completion<E>) -> Void, receiveValue receivedValue: #escaping (T) -> Void) -> AnyCancellable {
let subscription = subject
.sink(receiveCompletion: { completion in
receivedCompletion(completion)
}, receiveValue: { value in
receivedValue(value)
})
// Cancel previous subscriptions.
subscriptions.forEach { $0.item?.cancel() }
// Add new subscription.
subscriptions.insert(WeakBox(item: subscription))
return subscription
}
}
I tested the class in Playground as follows.
let publisher = MyPublisher<Int, Never>()
let firstSubscription = publisher
.sink(receiveCompletion: { completion in
print("1st subscription completion \(completion)")
}, receiveValue: { value in
print("1st subscription value \(value)")
})
let secondSubscription = publisher
.sink(receiveCompletion: { completion in
print("2st subscription completion \(completion)")
}, receiveValue: { value in
print("2st subscription value \(value)")
})
let thirdSubscription = publisher
.sink(receiveCompletion: { completion in
print("3st subscription completion \(completion)")
}, receiveValue: { value in
print("3st subscription value \(value)")
})
publisher.send(123)
Console output:
3st subscription value 123
If you comment out the line subscriptions.forEach { $0.cancel() }, then you get:
3st subscription value 123
1st subscription value 123
2st subscription value 123
Hopefully I could help you.

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)

How to properly pull from cache before remote using swift combine

I perform many repeated requests in order to populate a field. I would like to cache the result and use the cached value the next time around.
public func getItem(_ id: String) -> AnyPublisher<Item?, Never> {
if let item = itemCache[id] {
return Just(item).eraseToAnyPublisher()
}
return downloadItem(id: id)
.map { item in
if let item = item {
itemCache[id] = item
}
return item
}
.eraseToAnyPublisher()
}
}
func downloadItem(_ id: String) -> AnyPublisher<Item?, Never> { ... }
And this is called like this:
Just(["a", "a", "a"]).map(getItem)
However, all the requests are calling downloadItem. downloadItem does return on the main queue. I also tried wrapping the entire getItem function into Deferred but that had the same result.
First, the issue was that the function is being evaluated and only a publisher is returned. So the cache check is evaluated each time before the network publisher is ever subscribed to. Using Deferred is the proper fix for that. However, that still didn't solve the problem.
The solution was instead to first cache a shared publisher while the network request is pending so all requests during the network call will use the same publisher, then when it's complete to cache a Just publisher for the all future calls:
public func getItem(_ id: String) -> AnyPublisher<Item?, Never> {
if let publisher = self.publisherCache[id] {
return publisher
}
let publisher = downloadItem(id)
.handleEvents(receiveOutput: {
// Re-cache a Just publisher once the network request finishes
self.publisherCache[id] = Just($0).eraseToAnyPublisher()
})
.share() // Ensure the same publisher is returned from the cache
.eraseToAnyPublisher()
// Cache the publisher to be used while downloading is in progress
self.publisherCache[id] = publisher
return publisher
}
One note, is that downloadItem(id) is async and being recieved on the main loop. When I replaced downloadItem(id) with Just(Item()) for testing, this didn't work beause the entire publisher chain was evaluated on creation. Use Just(Item()).recieve(on: Runloop.main) to fix that while testing.

SwiftUI and Combine, how to create a reusable publisher to check if a string is empty

I'm trying to learn SwiftUI and Combine syntax and am trying to understand how to create a reusable publisher that will check if a String is empty.
I've got a SwiftUI with 5 TextFields which using #Binding to connect them to my data model object.
class DataWhatIsLoanPayment: ObservableObject {
// Input
#Published var pv = ""
#Published var iyr = ""
// a bunch more fields...
// Output
#Published var isvalidform = false
}
I want to enable the Calculate button once all of the fields are filled in (isEmpty == false).
I'm following along with https://peterfriese.dev/swift-combine-love/, and I was able to get my SwiftUI to properly enable/disable my Calculate button by creating an isValidPVPublisher and an isValidIYRPublisher and combing them in an isValidFormPublisher, like so:
private var isValidPVPublisher: AnyPublisher<Bool, Never> {
$pv
.debounce(for: 0.8, scheduler: RunLoop.main)
.removeDuplicates()
.map { input in
return input.isEmpty == false
}
.eraseToAnyPublisher()
}
private var isValidIYRPublisher: AnyPublisher<Bool, Never> {
$iyr
.debounce(for: 0.8, scheduler: RunLoop.main)
.removeDuplicates()
.map { input in
return input.isEmpty == false
}
.eraseToAnyPublisher()
}
private var isValidFormPublisher: AnyPublisher<Bool, Never> {
Publishers.CombineLatest(isValidPVPublisher, isValidIYRPublisher)
.map { pvIsValid, iyrIsValid in
return pvIsValid && iyrIsValid
}
.eraseToAnyPublisher()
}
init() {
isValidFormPublisher
.receive(on: RunLoop.main)
.assign(to: \.isValidForm, on: self)
.store(in: &cancellableSet)
}
However, I'm going to have a lot more than 2 fields, and I'm going to have a lot of other forms in my app in which I will want to check if my fields are empty. And repeating .debounce(for: 0.8, scheduler: RunLoop.main).removeDuplicates().map { input in return input.isEmpty == false }.eraseToAnyPublisher() over and over again is a bad idea.
I want to create a reusable NotEmptyPublisher, or something like that, which takes a field binding, like my $pv and sets up the chain as show in the isValidPVPublisher above. So I can have something like:
// Something like this, but I'm not sure of the syntax...
private var isValidPVPublisher = NotEmptyPublisher(field:$pv)
// instead of ...
private var isValidPVPublisher: AnyPublisher<Bool, Never> {
$pv
.debounce(for: 0.8, scheduler: RunLoop.main)
.removeDuplicates()
.map { input in
return input.isEmpty == false
}
.eraseToAnyPublisher()
}
But I'm having a trouble parsing a lot of Swift syntax that I'm not familiar with and I can't seem to figure out how to do it, and every example I find on the web is just defining the publisher chain inline instead of in a reusable fashion.
How can I create a reusable publisher so that I don't have to repeat these inline publishers which all do the same thing?
Here you are!
extension Publisher where Output == String {
func isStringInhabited() -> Publishers.Map<Self, Bool> {
map { !$0.isEmpty }
}
}
$0 is shorthand for the first argument to the closure, $1 means the second, and so on and so forth.
! is the Bool inversion operator, prefixing ! is shorthand for suffixing == false.
Now, as to your question about reuse, you don't need to overkill things that hard, you can just create a function.
private func isValidTransform<P: Publisher>(input: P) -> some Publisher where P.Output == String {
input
.debounce(for: 0.8, scheduler: RunLoop.main)
.removeDuplicates()
.isStringInhabited()
}
P is a generic, which means it could be any type whatsoever as long as that type conforms to Publisher. The where clause allows us to constrain this conformance further, denoting that we can only operate on Publishers when their Output is String. some Publisher gives us an opaque return type to save us from having to write the type signature of a Publisher that has been transformed multiple times, you can change this to AnyPublisher<Bool, Never> and use .eraseToAnyPublisher() if you like but I recommend only using that erasure at the time of need.

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