Swift Combine: How can I convert `AnyPublisher<[Foo], *>` to `AnyPublisher<Foo, *>`? - swift

How can I convert a publisher of array a certain element, to just a publisher of said element (but with more events)?
e.g. how can I convert
AnyPublisher<[Int], Never> to AnyPublisher<Int, Never>?
I think maybe what RxSwift offers with its from operator is similar to what I want to do.
I guess I want the inverse of Combine collect?

Here is the code:
func example(publisher: AnyPublisher<[Foo], Never>) -> AnyPublisher<Foo, Never> {
return publisher
.map { $0.publisher }
.switchToLatest()
.eraseToAnyPublisher()
}

What you probably want to do is to use a FlatMap on the publisher of the Foo array, using a function which converts the Foo array to an Observable of Foo (which is where the from comes in).
.flatMap { $0.publisher }

Related

Swift Combine: Merge publishers of different types

I am trying to merge multiple publishers that are of different types.
I have publishers of type string and a publisher of type, however when I merge them using MergeMany or CombineLatest I get a type mismatch error.
Is there anyway to merge publishers of different types? See the code example below:
#Published var str1: String?
#Published var str2: String?
#Published var image: Image?
Publishers.MergeMany($str1, $str2, $image)
.removeDuplicates()
.sink { _ in
//...
}
.store(in: &bag)
Assuming that you had no other #Published properties, the easiest thing to do would be to use objectWillChange:
self.objectWillChange
.receive(on: RunLoop.main)
.sink {
print("objectWillChange...", self.str1, self.str2, self.image)
}
.store(in: &bag)
However, this does not give you the remove duplicates that you mentioned in the comments.
If you want, that, you can get a little more complicated with things:
Publishers.MergeMany(
$str1.dropFirst().removeDuplicates().map({ _ in }).eraseToAnyPublisher(),
$str2.dropFirst().removeDuplicates().map({ _ in }).eraseToAnyPublisher(),
$image.dropFirst().removeDuplicates().map({ _ in }).eraseToAnyPublisher()
)
.receive(on: RunLoop.main)
.sink {
print("MergeMany...", self.str1, self.str2, self.image)
}
.store(in: &bag)
(There may be a prettier way than my map. { _ in } to remove the return type)
The above method seems to work effectively for removing the individual duplicate elements and only triggering the sink when one of them has changed.
#jnpdx's answer gets the job done. I'm focusing on this question from OP:
Is there anyway to merge publishers of different types?
It is important to realize the difference between Merge and CombineLatest operators since they are quite different, semantically.
Check out RxMarbles for Merge and CombineLatest.
Merge requires the inner publishers to have the same Output and emits a single value whenever one emits.
CombineLatest publishes a tuple containing the latest values for each inner publisher.
I would write this like so:
#Published var str1: String?
#Published var str2: String?
#Published var image: Image?
func setUpBindings() {
Publishers.CombineLatest3($str1, $str2, $image)
.sink { _ in
//...
}
.store(in: &bag)
}
Since tuples can't conform to Equatable (yet), you can't leverage removeDuplicates() on the resulting publisher.
Like #jnpdx wrote, an approach could be to pull the .removeDuplicates() inside to each publisher.
Publishers.CombineLatest3(
$str1.removeDuplicates(),
$str2.removeDuplicates(),
$image.removeDuplicates()
)
.sink { (str1, str2, image) in

Use result of publisher in map of another publisher

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()
}
}

Swift flatMap chain unwrap

Have somebody faced the issue working with such flatMap chain (or even longer) when compiler went to infinite loop.
let what = Future<String, Error>.init { (promise) in
promise(.success("123"))
}
.flatMap { (inStr) -> AnyPublisher<Int, Error> in
Future<Int, Error>.init { (promise) in
promise(.success(Int(inStr)!))
}.eraseToAnyPublisher()
}
.flatMap { (inInt) -> AnyPublisher<String, Error> in
Just(String(inInt))
.setFailureType(to: Error.self)
.eraseToAnyPublisher()
}.eraseToAnyPublisher()
The type of Publisher Output and Failure are terrible!
I have flatMap chain with 7 steps and you can imagine the actual type.
Maybe somebody knows how to handle this correctly?
Any help appreciated.
This is really just a mirage. It's exactly equivalent to AnyPublisher<String, Error>:
let what: AnyPublisher<String, Error> = Future<String, Error>() { (promise) in
...
The mirage comes about because of associated types. The final type is Publishers.FlatMap<AnyPublisher<String, Error>, Publishers.FlatMap<AnyPublisher<Int, Error>, Future<String, Error>>>, and from that the AnyPublisher extracts Output and Error. A little bit of unwinding should make it clear that these are just elaborate type aliases for exactly String and Error. Hopefully as Combine becomes more common, the tools will become better at simplifying these type alias in diagnostics. The compiler already does that a lot. It just needs to be a little smarter in these cases. But using an explicit type on the variable (or on the return value of a function), you can get the spelling you want.
Per your comment, that the problem is compile time, that's a common problem with chaining in Swift. It's not Combine-specific and has little to do with the complexity of the types. The most common version of this is having a lot of terms strung together with + (though the Swift team has done a lot of work to improve that one). The solution (as elsewhere) is to break up the expression.
let what = Future<String, Error>.init { (promise) in
promise(.success("123"))
}
let what1 = what
.flatMap { (inStr) -> AnyPublisher<Int, Error> in
Future<Int, Error>.init { (promise) in
promise(.success(Int(inStr)!))
}.eraseToAnyPublisher()
}
let what2 = what1
.flatMap { (inInt) -> AnyPublisher<String, Error> in
Just(String(inInt))
.setFailureType(to: Error.self)
.eraseToAnyPublisher()
}.eraseToAnyPublisher()
You don't have to break it up into individual steps like this of course. But at various points, you'll want to break up the expression. Proving that the types are correct can be an exponential-time problem. The way you deal with exponential-time problems is to make sure that "n" is small.

Swift Combine framework multiple async request responses into one

Here is a pseudo code of what I need to achieve:
func apiRequest1() -> Future<ResultType1, Error> { ... }
func apiRequest2() -> Future<ResultType2, Error> { ... }
func transform(res1: ResultType1, res2: ResultType2) -> ResultType3 { ... }
func combinedApiRequests() -> Future<ResultType3, Error> {
(resultType1, resultType2) = execute apiRequest1() and apiRequest2() asynchronously
resultType3 = transform(resultType1, resultType2)
return a Future publisher with resultType3
}
How would combinedApiRequests() look?
There's no need to return a Future publisher. Future publisher is a specific publisher, but as far as a downstream is concerned, a publisher is defined by its output and failure types. Instead, return a AnyPublisher<ResultType3, Error>.
Zip is a publisher that waits for all results to arrive to emit a value. This is probably what you'd need (more on this later). This is how your function could look:
func combinedApiRequests() -> AnyPublisher<ResultType3, Error> {
Publishers.Zip(apiRequest1, apiRequest2)
.map { transform(res1: $0, res2: $1) }
.eraseToAnyPublisher()
}
There is also CombineLatest publisher. For the first result from each upstream, it behaves the same as Zip, but for subsequent results it differs. In your case, it doesn't matter since Future is a one-shot publisher, but if the upstream publishers emitted multiple values, then you'd have to decide for your specific use case whether to use Zip - which always waits for all upstreams to emit a value before it emits a combined value, or CombineLatest - which emits with each new upstream value and combines it with the latest for other upstreams.

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